//预布局方法 所有的布局应该写在这里面 - (void)prepareLayout //此方法应该返回当前屏幕正在显示的视图(cell 头尾视图)的布局属性集合(UICollectionViewLayoutAttributes 对象集合) - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect //根据indexPath去对应的UICollectionViewLayoutAttributes 这个是取值的,要重写,在移动删除的时候系统会调用改方法重新去UICollectionViewLayoutAttributes然后布局 - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath - (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath //返回当前的ContentSize - (CGSize)collectionViewContentSize //是否重新布局 - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds //这4个方法用来处理插入、删除和移动cell时的一些动画 瀑布流代码详解 - (void)prepareForCollectionViewUpdates:(NSArray *)updateItems - (UICollectionViewLayoutAttributes*)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath - (nullable UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath - (void)finalizeCollectionViewUpdates //9.0之后处理移动相关 - (UICollectionViewLayoutInvalidationContext *)invalidationContextForInteractivelyMovingItems:(NSArray*)targetIndexPaths withTargetPosition:(CGPoint)targetPosition previousIndexPaths:(NSArray *)previousIndexPaths previousPosition:(CGPoint)previousPosition NS_AVAILABLE_IOS(9_0) - (UICollectionViewLayoutInvalidationContext *)invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:(NSArray *)indexPaths previousIndexPaths:(NSArray *)previousIndexPaths movementCancelled:(BOOL)movementCancelled NS_AVAILABLE_IOS(9_0)
.h文件
#importUIKIT_EXTERN NSString *const AC_UICollectionElementKindSectionHeader; UIKIT_EXTERN NSString *const AC_UICollectionElementKindSectionFooter; @class AC_WaterCollectionViewLayout; @protocol AC_WaterCollectionViewLayoutDelegate //代理取cell 的高 - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(AC_WaterCollectionViewLayout *)layout heightOfItemAtIndexPath:(NSIndexPath *)indexPath itemWidth:(CGFloat)itemWidth; //处理移动相关的数据源 - (void)moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath; @end @interface AC_WaterCollectionViewLayout : UICollectionViewLayout @property (assign, nonatomic) NSInteger numberOfColumns;//瀑布流有列 @property (assign, nonatomic) CGFloat cellDistance;//cell之间的间距 @property (assign, nonatomic) CGFloat topAndBottomDustance;//cell 到顶部 底部的间距 @property (assign, nonatomic) CGFloat headerViewHeight;//头视图的高度 @property (assign, nonatomic) CGFloat footViewHeight;//尾视图的高度 @property(nonatomic, weak) id delegate; @end
.h文件没有太多东西,看注释应该都清楚。跟UICollectionViewFlowLayout不同的是没有方向设置,因为瀑布流横向基本少见,所以所以头尾视图也由CGSize改成CGFloat,
.m文件
#import "AC_WaterCollectionViewLayout.h" NSString *const AC_UICollectionElementKindSectionHeader = @"AC_HeadView"; NSString *const AC_UICollectionElementKindSectionFooter = @"AC_FootView"; @interface AC_WaterCollectionViewLayout() @property (strong, nonatomic) NSMutableDictionary *cellLayoutInfo;//保存cell的布局 @property (strong, nonatomic) NSMutableDictionary *headLayoutInfo;//保存头视图的布局 @property (strong, nonatomic) NSMutableDictionary *footLayoutInfo;//保存尾视图的布局 @property (assign, nonatomic) CGFloat startY;//记录开始的Y @property (strong, nonatomic) NSMutableDictionary *maxYForColumn;//记录瀑布流每列最下面那个cell的底部y值 @property (strong, nonatomic) NSMutableArray *shouldanimationArr;//记录需要添加动画的NSIndexPath @end @implementation AC_WaterCollectionViewLayout - (instancetype)init { self = [super init]; if (self) { self.numberOfColumns = 3; self.topAndBottomDustance = 10; self.cellDistance = 10; _headerViewHeight = 0; _footViewHeight = 0; self.startY = 0; self.maxYForColumn = [NSMutableDictionary dictionary]; self.shouldanimationArr = [NSMutableArray array]; self.cellLayoutInfo = [NSMutableDictionary dictionary]; self.headLayoutInfo = [NSMutableDictionary dictionary]; self.footLayoutInfo = [NSMutableDictionary dictionary]; } return self; } - (void)prepareLayout { [super prepareLayout]; //重新布局需要清空 [self.cellLayoutInfo removeAllObjects]; [self.headLayoutInfo removeAllObjects]; [self.footLayoutInfo removeAllObjects]; [self.maxYForColumn removeAllObjects]; self.startY = 0; CGFloat viewWidth = self.collectionView.frame.size.width; //代理里面只取了高度,所以cell的宽度有列数还有cell的间距计算出来 CGFloat itemWidth = (viewWidth - self.cellDistance*(self.numberOfColumns + 1))/self.numberOfColumns; //取有多少个section NSInteger sectionsCount = [self.collectionView numberOfSections]; for (NSInteger section = 0; section < sectionsCount; section++) { //存储headerView属性 NSIndexPath *supplementaryViewIndexPath = [NSIndexPath indexPathForRow:0 inSection:section]; //头视图的高度不为0并且根据代理方法能取到对应的头视图的时候,添加对应头视图的布局对象 if (_headerViewHeight>0 && [self.collectionView.dataSource respondsToSelector:@selector(collectionView: viewForSupplementaryElementOfKind: atIndexPath:)]) { UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:AC_UICollectionElementKindSectionHeader withIndexPath:supplementaryViewIndexPath]; //设置frame attribute.frame = CGRectMake(0, self.startY, self.collectionView.frame.size.width, _headerViewHeight); //保存布局对象 self.headLayoutInfo[supplementaryViewIndexPath] = attribute; //设置下个布局对象的开始Y值 self.startY = self.startY + _headerViewHeight + _topAndBottomDustance; }else{ //没有头视图的时候,也要设置section的第一排cell到顶部的距离 self.startY += _topAndBottomDustance; } //将Section第一排cell的frame的Y值进行设置 for (int i = 0; i < _numberOfColumns; i++) { self.maxYForColumn[@(i)] = @(self.startY); } //计算cell的布局 //取出section有多少个row NSInteger rowsCount = [self.collectionView numberOfItemsInSection:section]; //分别计算设置每个cell的布局对象 for (NSInteger row = 0; row < rowsCount; row++) { NSIndexPath *cellIndePath =[NSIndexPath indexPathForItem:row inSection:section]; UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:cellIndePath]; //计算当前的cell加到哪一列(瀑布流是加载到最短的一列) CGFloat y = [self.maxYForColumn[@(0)] floatValue]; NSInteger currentRow = 0; for (int i = 1; i < _numberOfColumns; i++) { if ([self.maxYForColumn[@(i)] floatValue] < y) { y = [self.maxYForColumn[@(i)] floatValue]; currentRow = i; } } //计算x值 CGFloat x = self.cellDistance + (self.cellDistance + itemWidth)*currentRow; //根据代理去当前cell的高度 因为当前是采用通过列数计算的宽度,高度根据图片的原始宽高比进行设置的 CGFloat height = [(id)self.delegate collectionView:self.collectionView layout:self heightOfItemAtIndexPath:cellIndePath itemWidth:itemWidth]; //设置当前cell布局对象的frame attribute.frame = CGRectMake(x, y, itemWidth, height); //重新设置当前列的Y值 y = y + self.cellDistance + height; self.maxYForColumn[@(currentRow)] = @(y); //保留cell的布局对象 self.cellLayoutInfo[cellIndePath] = attribute; //当是section的最后一个cell是,取出最后一排cell的底部Y值 设置startY 决定下个视图对象的起始Y值 if (row == rowsCount -1) { CGFloat maxY = [self.maxYForColumn[@(0)] floatValue]; for (int i = 1; i < _numberOfColumns; i++) { if ([self.maxYForColumn[@(i)] floatValue] > maxY) { NSLog(@"%f", [self.maxYForColumn[@(i)] floatValue]); maxY = [self.maxYForColumn[@(i)] floatValue]; } } self.startY = maxY - self.cellDistance + self.topAndBottomDustance; } } //存储footView属性 //尾视图的高度不为0并且根据代理方法能取到对应的尾视图的时候,添加对应尾视图的布局对象 if (_footViewHeight>0 && [self.collectionView.dataSource respondsToSelector:@selector(collectionView: viewForSupplementaryElementOfKind: atIndexPath:)]) { UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:AC_UICollectionElementKindSectionFooter withIndexPath:supplementaryViewIndexPath]; attribute.frame = CGRectMake(0, self.startY, self.collectionView.frame.size.width, _footViewHeight); self.footLayoutInfo[supplementaryViewIndexPath] = attribute; self.startY = self.startY + _footViewHeight; } } } - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { NSMutableArray *allAttributes = [NSMutableArray array]; //添加当前屏幕可见的cell的布局 [self.cellLayoutInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, UICollectionViewLayoutAttributes *attribute, BOOL *stop) { if (CGRectIntersectsRect(rect, attribute.frame)) { [allAttributes addObject:attribute]; } }]; //添加当前屏幕可见的头视图的布局 [self.headLayoutInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, UICollectionViewLayoutAttributes *attribute, BOOL *stop) { if (CGRectIntersectsRect(rect, attribute.frame)) { [allAttributes addObject:attribute]; } }]; //添加当前屏幕可见的尾部的布局 [self.footLayoutInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, UICollectionViewLayoutAttributes *attribute, BOOL *stop) { if (CGRectIntersectsRect(rect, attribute.frame)) { [allAttributes addObject:attribute]; } }]; return allAttributes; } //插入cell的时候系统会调用改方法 - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewLayoutAttributes *attribute = self.cellLayoutInfo[indexPath]; return attribute; } - (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { UICollectionViewLayoutAttributes *attribute = nil; if ([elementKind isEqualToString:AC_UICollectionElementKindSectionHeader]) { attribute = self.headLayoutInfo[indexPath]; }else if ([elementKind isEqualToString:AC_UICollectionElementKindSectionFooter]){ attribute = self.footLayoutInfo[indexPath]; } return attribute; } - (CGSize)collectionViewContentSize { return CGSizeMake(self.collectionView.frame.size.width, MAX(self.startY, self.collectionView.frame.size.height)); } - (void)prepareForCollectionViewUpdates:(NSArray *)updateItems { [super prepareForCollectionViewUpdates:updateItems]; NSMutableArray *indexPaths = [NSMutableArray array]; for (UICollectionViewUpdateItem *updateItem in updateItems) { switch (updateItem.updateAction) { case UICollectionUpdateActionInsert: [indexPaths addObject:updateItem.indexPathAfterUpdate]; break; case UICollectionUpdateActionDelete: [indexPaths addObject:updateItem.indexPathBeforeUpdate]; break; case UICollectionUpdateActionMove: // [indexPaths addObject:updateItem.indexPathBeforeUpdate]; // [indexPaths addObject:updateItem.indexPathAfterUpdate]; break; default: NSLog(@"unhandled case: %@", updateItem); break; } } self.shouldanimationArr = indexPaths; } //对应UICollectionViewUpdateItem 的indexPathBeforeUpdate 设置调用 - (UICollectionViewLayoutAttributes*)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath { if ([self.shouldanimationArr containsObject:itemIndexPath]) { UICollectionViewLayoutAttributes *attr = self.cellLayoutInfo[itemIndexPath]; attr.transform = CGAffineTransformRotate(CGAffineTransformMakeScale(0.2, 0.2), M_PI); attr.center = CGPointMake(CGRectGetMidX(self.collectionView.bounds), CGRectGetMaxY(self.collectionView.bounds)); attr.alpha = 1; [self.shouldanimationArr removeObject:itemIndexPath]; return attr; } return nil; } //对应UICollectionViewUpdateItem 的indexPathAfterUpdate 设置调用 - (nullable UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath { if ([self.shouldanimationArr containsObject:itemIndexPath]) { UICollectionViewLayoutAttributes *attr = self.cellLayoutInfo[itemIndexPath]; attr.transform = CGAffineTransformRotate(CGAffineTransformMakeScale(2, 2), 0); // attr.center = CGPointMake(CGRectGetMidX(self.collectionView.bounds), CGRectGetMaxY(self.collectionView.bounds)); attr.alpha = 0; [self.shouldanimationArr removeObject:itemIndexPath]; return attr; } return nil; } - (void)finalizeCollectionViewUpdates { self.shouldanimationArr = nil; } - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { CGRect oldBounds = self.collectionView.bounds; if (!CGSizeEqualToSize(oldBounds.size, newBounds.size)) { return YES; } return NO; // // return YES; } //移动相关 - (UICollectionViewLayoutInvalidationContext *)invalidationContextForInteractivelyMovingItems:(NSArray *)targetIndexPaths withTargetPosition:(CGPoint)targetPosition previousIndexPaths:(NSArray *)previousIndexPaths previousPosition:(CGPoint)previousPosition NS_AVAILABLE_IOS(9_0) { UICollectionViewLayoutInvalidationContext *context = [super invalidationContextForInteractivelyMovingItems:targetIndexPaths withTargetPosition:targetPosition previousIndexPaths:previousIndexPaths previousPosition:previousPosition]; if([self.delegate respondsToSelector:@selector(moveItemAtIndexPath: toIndexPath:)]){ [self.delegate moveItemAtIndexPath:previousIndexPaths[0] toIndexPath:targetIndexPaths[0]]; } return context; } - (UICollectionViewLayoutInvalidationContext *)invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:(NSArray *)indexPaths previousIndexPaths:(NSArray *)previousIndexPaths movementCancelled:(BOOL)movementCancelled NS_AVAILABLE_IOS(9_0) { UICollectionViewLayoutInvalidationContext *context = [super invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:indexPaths previousIndexPaths:previousIndexPaths movementCancelled:movementCancelled]; if(!movementCancelled){ } return context; } @end
(void)prepareLayout
方法里面的布局注释我应该写的很详细了,看不懂的多看2遍。这里我再详细说一下startY跟maxYForColumn这两个属性。startY值主要处理下一个视图对象的Y值。maxYForColumn保存当前已经计算了的最下一列的cell的bottom值。布局cell的时候,cell的Y值
取maxYForColumn里面的最小值。当section里面的cell全部布局完的时候,接下来布局尾视图的时候,startY应该取maxYForColumn里面的最大值。
(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
这个方法需要返回当前界面可见的视图的布局对象集合,很多线性布局的效果都是在这个方法里面处理,在下面的UIollectionViewFlowLayout会有一些常见效果的处理代码。
(void)prepareForCollectionViewUpdates:(NSArray )updateItems
当调用插入、删除和移动相关的api的时候回调用该方法(对照上面的代码看)其中的indexPathBeforeUpdate跟indexPathAfterUpdat分别对应
(UICollectionViewLayoutAttributes)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath )itemIndexPath
(UICollectionViewLayoutAttributes)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
处理相对应的UICollectionViewLayoutAttributes属性变动,我的代码中插入是添加的indexPathAfterUpdate,删除是添加的indexPathBeforeUpdate。
关于移动相关的,系统提供的只能9.0之后,如果想9.0之前使用必须的自定义,可以查看这篇文章可拖拽重排的CollectionView自己研究。添加移动相关的代码在ctr处理,回调也在ctr里面处理,先贴上代码
//添加cell长按手势 UILongPressGestureRecognizer *longGest = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longGest:)]; [self.waterCollectionView addGestureRecognizer:longGest]; //对应的action - (void)longGest:(UILongPressGestureRecognizer *)gest { switch (gest.state) { case UIGestureRecognizerStateBegan: { NSIndexPath *touchIndexPath = [self.waterCollectionView indexPathForItemAtPoint:[gest locationInView:self.waterCollectionView]]; if (touchIndexPath) { [self.waterCollectionView beginInteractiveMovementForItemAtIndexPath:touchIndexPath]; }else{ break; } } break; case UIGestureRecognizerStateChanged: { [self.waterCollectionView updateInteractiveMovementTargetPosition:[gest locationInView:gest.view]]; } break; case UIGestureRecognizerStateEnded: { [self.waterCollectionView endInteractiveMovement]; } break; default: break; } } //移动对应的回调 //系统的 - (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath NS_AVAILABLE_IOS(9_0) { // if(sourceIndexPath.row != destinationIndexPath.row){ // NSString *value = self.imageArr[sourceIndexPath.row] ; // [self.imageArr removeObjectAtIndex:sourceIndexPath.row]; // [self.imageArr insertObject:value atIndex:destinationIndexPath.row]; // NSLog(@"from:%ld to:%ld", sourceIndexPath.row, destinationIndexPath.row); // } } //自定义的回调 - (void)moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath { if(sourceIndexPath.row != destinationIndexPath.row){ NSString *value = self.imageArr[sourceIndexPath.row]; [self.imageArr removeObjectAtIndex:sourceIndexPath.row]; [self.imageArr insertObject:value atIndex:destinationIndexPath.row]; NSLog(@"from:%ld to:%ld", sourceIndexPath.row, destinationIndexPath.row); } }
当长按后移动手指的时候系统会一直调用
invalidationContextForInteractivelyMovingItems:withTargetPosition:previousIndexPaths: previousPosition:因为瀑布流的每个cell的frame大小不相同所以要通过代理方法不断的更新数据源的顺序,然后系统不断调用prepareLayout方法进行重新布局,之前我是采用的系统提供的代理collectionView moveItemAtIndexPath: toIndexPath:来处理数据源的,但是发现只有布局的时候是正常的,然是松开手指后,从新加载数据发现乱了,然后打印数据源。发现数据源的顺序并没有改变,还是之前的顺序。
后来发现问题出现在当移动手势结束的时候调用的方法 [self.waterCollectionView endInteractiveMovement];
以下xcode对该方法的介绍
Ends interactive movement tracking and moves the target item to its new location.
Call this method upon the successful completion of movement tracking for a item. For example, when using a gesture recognizer to track user interactions, call this method upon the successful completion of the gesture. Calling this method lets the collection view know to end tracking and move the item to its new location permanently. The collection view responds by calling the collectionView:moveItemAtIndexPath:toIndexPath: method of its data source to ensure that your data structures are updated.
也就是说当手势结束的时候系统会掉一次collectionView:moveItemAtIndexPath:toIndexPath:,该操作导致移动的时候进行的变换的顺序又变回来了,所以只好自己写了一个代理方法处理数据源,没管系统的回调。