自定义UICollectionViewLayout|青训营笔记
这是我参与「第四届青训营 」笔记创作活动的第3天
想看基础的,直接跳到基础
想看怎么自定义的,可以直接跳到实现
需求
我们组选择了华容道项目,容道布局成了难点,本次我们选择了UICollectionViewLayout
每一个人其实都是一样,那么就是一个UICollectionViewCell
我每一个人都有x, y, width, height,是可以根据这个来布局的
对于手势,我们可以发现苹果官方推荐我们使用其管理者持有,也就是UICollectionView
之后我会上线一个个人笔记,有关于学校项目里面的课表需求,也是使用UICollectionViewLayout
(课表这个需求很离谱,我会单独逐步分析出来,如果有人有类似需求,可以去我们仓库看,学校的项目是开源的,已上架App Store)
问题
为什么不用UIView
有人好奇,为什么一定要用这么重量级的东西,而且还要自定义UICollectionViewLayout
其实容道里面的人数不是固定的,如果利用NSMutableArray <__kindof UIView *> * 方式的话,还需要自己去维护这个数组,更加麻烦
手势一共4个方向,4个手势,如果以上面的方式的话,我不得不再去写一个方法判断手势是否在UIView里面
如果这里还有人想问为什么里面的单个UIView不加手势的话,可以考虑先查看一下设计模式与设计理念
IGListKit
有人建议我可以考虑这个第三方库,但我拒绝了,这个项目不会用到
但上面提到的课表中,我可能会使用到这个第三方库
基础
UICollectionView
这个视图,在很多人眼里都很重量级,原因就在他比UITableView还多一个UICollectionViewLayout
collectionView通过layout进行布局,这种布局相对来说比较更个性化。
一般来说,不需要继承这个类(除非要求手势穿透)
这里简单上一个懒加载的代码
- (UICollectionView *)collectionView {
if (_collectionView == nil) {
CGFloat width = self.view.width - 40;
LevelCollectionLayout *layout = [[LevelCollectionLayout alloc] init];
layout.lineSpacing = layout.interitemSpacing = 5;
CGFloat minWidth = width / 4 - layout.interitemSpacing;
layout.sizeForItem = CGSizeMake(minWidth, minWidth);
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, width - layout.interitemSpacing, (minWidth + layout.lineSpacing) * 5 - layout.lineSpacing) collectionViewLayout:layout];
_collectionView.center = self.view.SuperCenter;
_collectionView.backgroundColor = UIColor.clearColor;
[_collectionView registerClass:PersonItem.class forCellWithReuseIdentifier:PersonItemReuseIdentifier];
_collectionView.showsVerticalScrollIndicator = NO;
_collectionView.showsHorizontalScrollIndicator = NO;
}
return _collectionView;
}
这里我们可以发现,我们先得搞到一个layout(后面会说到如何自定义)
然后根据位置和这个layout去创建
注册一个复用的cell(当然这个项目基本不复用,课表需要复用)
UICollectionViewCell
这个就当非常普通的UIView也可以,如果是在复用池的Cell被再次使用,回掉用
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
你就可以根据传来的UICollectionViewLayoutAttributes来变化当前视图布局
(纯Frame布局非常好用)
UICollectionViewLayout
我们必须指定子类,至于为什么,实现的时候就知道了。
这里官方给出了UICollectionViewFlowLayout
这是一种线性布局,什么是线性呢?
- 下一个indexPath的数据在视觉上紧接上一个
- 下一个视图在视觉上紧接上一个
而华容道不同,下一个人的视觉位置可能并不会紧接上一个
而且,在滑动后,更能看出这个需求并不能用UICollectionViewFlowLayout实现
实现
代理
UICollectionViewFlowLayout其实也有代理,不过他的代理实现机制是靠collectionView本身的delegete去回传。
这里可以和UICollectionViewFlowLayout一样的思路,继承UICollectionViewDelegate书写协议,当然也可以不继承。
这里只会讲解不继承,在课表需求中,我们会讲解继承协议,以及如何打断UICollectionView的delegate。
@protocol LevelCollectionLayoutDelegate <NSObject>
@required
/// 确定布局
/// @param collectionView 视图
/// @param layout 布局
/// @param indexPath 位置
- (PersonFrame)collectionView:(UICollectionView *)collectionView layout:(LevelCollectionLayout *)layout frameForItemAtIndexPath:(NSIndexPath *)indexPath;
@end
/// typedef struct _PersonFrame {
/// int x;
/// int y;
/// int width;
/// int height;
/// } PersonFrame;
因为这个需求并没有其他要求,这里只有一个required代理,用于确定一个人的位置。
属性
与UICollectionViewFlowLayout一样,你也可以规定一些属性,
不管是计算属性还是存储属性,这些属性将会在后期进行大规模运算。
当然,这里少不了的属性一定是代理,这里不上代码。
类扩展
在刚刚的Cell中,UICollectionViewLayoutAttributes这个名字超长的类突然有点难让人理解。
简单来说,这里面规定了Cell的所有视觉空间布局信息
就是说,这里面有大小,3D,transform等等对这个Cell空间视觉上的数据支持
这里非常重要的一点是,在使用CollectionView的- performBatchUpdates:方法时,
去改变UICollectionViewLayoutAttributes的值是非常有效的(至少挺好看)
@interface LevelCollectionLayout ()
/// 布局
@property (nonatomic, strong) NSMutableArray <UICollectionViewLayoutAttributes *> *attributes;
@end
所以在很多资料中,都会有这么一个东西存在,就非常好理解了,你有n个数据,那么你就有n个视觉空间布局信息。
复写
我们必须复写一些信息,才能让Layout能执行起来,那么之前也提到了为什么一定要子类呢?
原因就在**UICollectionViewLayout (UISubclassingHooks)**里面的
- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect方法,这个方法必然会掉用,而如果不实现,那么就得不到UICollectionViewLayoutAttributes,就会报错了。
- (NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
return self.attributes;
}
而在一般的项目中,这里就会返回类扩展的那个attributes。
如果项目比较复杂,比如想分section去管理attributes等。
赋值
看了半天,结果发现attributes没值,怎么办呢?
重写- prepareLayout或者- invalidateLayout。这个项目呢,我就选择了prepareLayout。
不过要注意的一点是一定要掉用super
- (void)prepareLayout {
[super prepareLayout];
// 赋值操作
}
赋值对应的是创建,这个根据需求来,比如华容道当前关卡是不会平白无故新增一个人,那就可以用懒加载
如果对于课表来说,我新增一个事务课表,那么这个维护就得靠Layout本身的CRUD了。
那怎么赋值呢,这里我就单独抽出来一个方法吧
- (CGRect)_frameForIndexPath:(NSIndexPath *)indexPath {
if (self.delegate) {
PersonFrame frame = [self.delegate collectionView:self.collectionView layout:self frameForItemAtIndexPath:indexPath];
CGFloat x = frame.x * (self.interitemSpacing + self.sizeForItem.width);
CGFloat y = frame.y * (self.lineSpacing + self.sizeForItem.height);
CGFloat width = frame.width * self.sizeForItem.width + (frame.width - 1) * self.interitemSpacing;
CGFloat height = frame.height * self.sizeForItem.height + (frame.height - 1) * self.lineSpacing;
return CGRectMake(x, y, width, height);
}
return CGRectZero;
}
// attribute.frame = [self _frameForIndexPath:indexPath];
很简单吧?因为attribute本身可以理解为只是一个存储类,存储这些空间布局信息,仅此而已。
总结
好了,这篇文章有点长了,其他的方法都大同小异,需要的时候再去看就好了。
关于如何通过自定义UICollectionViewLayout改变布局,也就是棋子如何走呢,就放到下一个笔记吧。
如果对CollectionView感兴趣的话,可以期待一下讲解课表需求。