使用UICollectionView,需要使用UICollectionViewLayout控制UICollectionViewCell布局,虽然UICollectionViewLayout提供了高度自定义空间,但是对于日常使用显得太繁琐,于是常见使用UICollectionViewFlowLayout。除了提供UITableView类似的协议方法,后者还提供了协议UICollectionViewDelegateFlowLayout <UICollectionViewDelegate>,定义了返回cell尺寸、间距,section的insets,header、footer尺寸等方法。从iOS9开始,UICollectionViewDelegate增加了cell的move相关的协议方法。但是不同尺寸的cell排序效果仍需要自行实现。
为了支持iOS7以上,实现常见不同尺寸cell自定义排序,故实现了一个UICollectionViewFlowLayout的子类,并提供了一个UICollectionViewLayout分类支持自定义拖拽手势。
查阅了一些资料,实现不同尺寸cell自定义排序的文章已经不少,常见都是描述了实现不具有header和footer的独个section的items排序,但是基本都未完整支持UICollectionViewFlowLayout的功能。
我封装了一个子类,完全支持UICollectionViewFlowLayout的功能,并额外提供了设置section背景颜色和所有section的items规则排序的扩展功能,鉴于代码数量,在此记录一下关键的实现过程。
ALWCollectionViewFlowLayout
1.设置section背景颜色
如果设置了UICollectionView的背景色,但是需要不同section显示不同颜色,就只需要自行在子类实现了。
UICollectionView的内容view可以分为三类:SupplementaryView(header和footer),Cell(item),DecorationView(多用在cell下层)
设置section的背景色,可以控制decorationView的背景色来实现。
UICollectionViewLayout提供了一个如下方法
- (void)registerClass:(nullable Class)viewClass forDecorationViewOfKind:(NSString *)elementKind;
可以在子类init方法中,注册一个UICollectionReusableView的子类ALWCollectionReusableView,作为decorationView。在ALWCollectionReusableView中,重载方法- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes;这里的layoutAttributes对象,具有众多属性,唯独没有一个UIColor。理所当然,再实现一个UICollectionViewLayoutAttributes的子类,增加一个UIColor属性名为backgroundColor,可以设置默认值。然后在applyLayoutAttributes方法中,如下实现:
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes { [super applyLayoutAttributes:layoutAttributes]; if ([layoutAttributes isMemberOfClass:[ALWCollectionViewLayoutAttributes class]]) { self.backgroundColor = ((ALWCollectionViewLayoutAttributes *)layoutAttributes).backgroundColor; } }
以上,只是注册和设置了decorationView的背景色,但是还未设置装饰view的显示frame和在合适的时机使其生效。
这个时机,就是UICollectionViewFlowLayout的如下两个方法
- (void)prepareLayout;
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;
前者是设置属性的时机,后者是使其生效的时机。
A.为了便于动态设置每个section的背景色,提供了一个协议方法
- (UIColor *)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout backgroundColorForSectionAtIndex:(NSInteger)section;
B.计算decorationView尺寸时候,可以根据每个section的首尾item的frame和sectionInset来确定;当同时有Header和footer时候,也可以根据二者来确定。但是需要注意,如果部分header或者footer未实现,在获取布局属性对象时候会为nil
C.在UICollectionViewFlowLayout子类中可使用如下实例方法获取布局属性对象
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath;
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString*)elementKind atIndexPath:(NSIndexPath *)indexPath;
但是在UICollectionViewLayout中将会返回nil,可以使用UICollectionViewLayoutAttributes的类方法得到实例
+ (instancetype)layoutAttributesForCellWithIndexPath:(NSIndexPath *)indexPath;
+ (instancetype)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind withIndexPath:(NSIndexPath *)indexPath;
+ (instancetype)layoutAttributesForDecorationViewOfKind:(NSString *)decorationViewKind withIndexPath:(NSIndexPath *)indexPath;
2.自定义不同尺寸的item的排序
UICollectionViewFlowLayout已经很好的展示了等尺寸的item排列,但是不同尺寸的item排列则显得不规则,如果希望在使用父类的完整功能基础上,每排item按照固定间距排列,可以按照如下记录实现。
为了间距规则,所以我的实现前提是固定横向竖向其中一个方向每排的item数量、边长都相等。如下图显示了填充类型的排序演示效果:
大致记录一下实现过程:
A.重载方法- (void)prepareLayout,在方法中得到item、header、footer的布局属性数组(NSMutableArray<UICollectionViewLayoutAttributes*>)
a.以纵向滑动为例,循环累加每个section的header、item、footer高度
b.根据每行item数量决定高度占用数组元素数量,记录每列item垂直方向当前占用的内容高度
c.以填充排序为例,每个item的y方向偏移量由高度占用数组最小元素决定;x方向由sectionInset、item固定的宽度、横向间距、列的索引共同决定;尺寸方面只需要获取itemSize协议方法返回尺寸中的高度
B.重载方法- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;返回items、header、footer、decorationView的属性数组
C.重载方法- (CGSize)collectionViewContentSize,返回A过程中记录的占用的最大内容尺寸
D.特别注意,不能在super方法返回的数组基础上,再添加Header、footer的布局属性。这样可能会出现如下错误
layout attributes for supplementary item at index path (<NSIndexPath>)
changed from <UICollectionViewLayoutAttributes>
to <UICollectionViewLayoutAttributes>
without invalidating the layout
就是由于布局属性数组中,存在相同indexPath的布局属性对象。
UICollectionViewLayout (DragGesture)
提供支持iOS7以上的拖拽手势,适用于所有子类。iOS9以后,UICollectionView提供了move相关方法,只需要添加手势触发调用相关方向即可。我实现的分类提供了类似的方法,为了不混淆使用,还提供了启用属性,默认关闭。
实现过程:
1.交换init方法,在其中增加collectionView属性的KVO,因为UICollectionView的实例化一般在UICollectionViewLayout之后
2.在UICollectionView实例化后,根据启用属性,添加长按手势和拖动手势到UICollectionView上
3.如果选中了某个item,开始持续关注拖动位置,进入另一个item后,交换二者,实现动画和数据交换。主要涉及UICollectionView的方法
- (void)performBatchUpdates:(void (^ __nullable)(void))updates completion:(void (^ __nullable)(BOOL finished))completion;
4.增加CADisplayLink对象,根据拖动方向和item所处位置,以屏幕刷新频率和预定移动速度,自动移动UICollectionView内容偏移量
效果图:
备注:目前在自定义排序后,拖拽手势效果仍存在短暂闪烁的问题,会持续修复。
猜测问题在重载返回属性数组的方法中,因为使用UICollectionViewFlowLayout默认排序时候,拖拽效果没有问题。
如果有朋友找到问题所在,请留言,非常感谢。
该类库已经在Base项目中更新:https://github.com/ALongWay/base.git