使用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:(nullableNSString *)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<UICollectionViewLayoutAttributesCGRect)rect;

前者是设置属性的时机,后者是使其生效的时机。

 

A.为了便于动态设置每个section的背景色,提供了一个协议方法

- (UIColorUICollectionViewUICollectionViewLayoutNSInteger)section;

B.计算decorationView尺寸时候,可以根据每个section的首尾item的frame和sectionInset来确定;当同时有Header和footer时候,也可以根据二者来确定。但是需要注意,如果部分header或者footer未实现,在获取布局属性对象时候会为nil

C.在UICollectionViewFlowLayout子类中可使用如下实例方法获取布局属性对象

- (nullableUICollectionViewLayoutAttributesNSIndexPath *)indexPath;
- (nullableUICollectionViewLayoutAttributesNSStringNSIndexPath *)indexPath;
- (nullableUICollectionViewLayoutAttributesNSString*)elementKind atIndexPath:(NSIndexPath *)indexPath;

但是在UICollectionViewLayout中将会返回nil,可以使用UICollectionViewLayoutAttributes的类方法得到实例

+ (instancetype)layoutAttributesForCellWithIndexPath:(NSIndexPath *)indexPath;
+ (instancetype)layoutAttributesForSupplementaryViewOfKind:(NSStringNSIndexPath *)indexPath;
+ (instancetype)layoutAttributesForDecorationViewOfKind:(NSStringNSIndexPath *)indexPath;

 

2.自定义不同尺寸的item的排序

UICollectionViewFlowLayout已经很好的展示了等尺寸的item排列,但是不同尺寸的item排列则显得不规则,如果希望在使用父类的完整功能基础上,每排item按照固定间距排列,可以按照如下记录实现。

为了间距规则,所以我的实现前提是固定横向竖向其中一个方向每排的item数量、边长都相等。如下图显示了填充类型的排序演示效果:

android recyclerview 拖动排序 mvvm_数组

 

 

大致记录一下实现过程:

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<UICollectionViewLayoutAttributesCGRect)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内容偏移量

 

效果图:

android recyclerview 拖动排序 mvvm_git_02

 

备注:目前在自定义排序后,拖拽手势效果仍存在短暂闪烁的问题,会持续修复。

猜测问题在重载返回属性数组的方法中,因为使用UICollectionViewFlowLayout默认排序时候,拖拽效果没有问题。

如果有朋友找到问题所在,请留言,非常感谢。

 

该类库已经在Base项目中更新:https://github.com/ALongWay/base.git