你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> UICollectionView詳解之自定義布局

UICollectionView詳解之自定義布局

編輯:IOS開發基礎
想看UICollectionView基礎使用的可以先看我的另一篇文章。這篇主寫關於UIollectionViewLayout自定義布局的一些常用方法,瀑布流布局的自定義,包括頭尾試圖的添加,插入刪除動畫,還有9.0後移動動態布局的使用。

UICollectionViewLayout自定義常用的幾個方法

//預布局方法 所有的布局應該寫在這裡面
- (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文件

#import 

UIKIT_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:,該操作導致移動的時候進行的變換的順序又變回來了,所以只好自己寫了一個代理方法處理數據源,沒管系統的回調。

運行效果

1367159-a4d30f145db87e70.gif



相關代碼下載




  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved