.h文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #import <uikit uikit.h=""> UIKIT_EXTERN NSString *const AC_UICollectionElementKindSectionHeader; UIKIT_EXTERN NSString *const AC_UICollectionElementKindSectionFooter; @class AC_WaterCollectionViewLayout; @protocol AC_WaterCollectionViewLayoutDelegate <nsobject> //代理取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<ac_watercollectionviewlayoutdelegate> delegate; @end</ac_watercollectionviewlayoutdelegate></nsobject></uikit>.h文件没有太多东西,看注释应该都清楚。跟UICollectionViewFlowLayout不同的是没有方向设置,因为瀑布流横向基本少见,所以所以头尾视图也由CGSize改成CGFloat,
.m文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 #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<ac_watercollectionviewlayoutdelegate>)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<nsindexpath *> *)targetIndexPaths withTargetPosition:(CGPoint)targetPosition previousIndexPaths:(NSArray<nsindexpath *> *)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<nsindexpath *> *)indexPaths previousIndexPaths:(NSArray<nsindexpath *> *)previousIndexPaths movementCancelled:(BOOL)movementCancelled NS_AVAILABLE_IOS(9_0) { UICollectionViewLayoutInvalidationContext *context = [ super invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:indexPaths previousIndexPaths:previousIndexPaths movementCancelled:movementCancelled]; if (!movementCancelled){ } return context; } @end</nsindexpath *></nsindexpath *></nsindexpath *></nsindexpath *></ac_watercollectionviewlayoutdelegate>(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里面处理,先贴上代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 //添加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:,该操作导致移动的时候进行的变换的顺序又变回来了,所以只好自己写了一个代理方法处理数据源,没管系统的回调。
相关代码下载