使用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