UICollectionView自定義佈局(一)

_執念發表於2017-12-28

前言

最近看了www.raywenderlich.com的關於UICollectionView自定義佈局的的教程,作一下筆記,方便以後查閱。UICollectionView自定義佈局的基本概念可以檢視喵神的WWDC 2012 Session筆記——219 Advanced Collection Views and Building Custom Layouts這篇文章。

wheel.gif

基本原理

下面是輪盤旋轉圖解,黃色的區域是手機螢幕,藍色的圓角矩形是UICollectionViewCells,cell以radius為半徑,以手機螢幕外的一點為圓心旋轉,每個cell之間的夾角為anglePerItem

圖1

正如你看到的,不是所有的cell都在螢幕內,假設第0個cell的旋轉角度是angle,那麼第1個cell的旋轉角度就是angle + anglePerItem,以此類推可以得到第i個cell的旋轉角度:

CGFloat  angleFori = angle + anglePerItem *i
複製程式碼

下面是角度座標是,0°代表的是螢幕中心,向左為負方向,向右為正方向,所以當cell的角度為0°時是垂直居中的。

圖2

自定義佈局屬性

因為系統的UICollectionViewLayoutAttributes沒有angleanchorPoint屬性,所以我們繼承UICollectionViewLayoutAttributes自定義佈局屬性。

@implementation WheelCollectionLayoutAttributes
- (instancetype)init{
    self = [super init];
    if (self) {
        self.anchorPoint = CGPointMake(0.5, 0.5);
        self.angle = 0;
    }
    return self;
}

- (id)copyWithZone:(NSZone *)zone{
    WheelCollectionLayoutAttributes *attribute = [super copyWithZone:zone];
    attribute.anchorPoint = self.anchorPoint;
    attribute.angle = self.angle;
    return attribute;
}
@end
複製程式碼

初始化時錨點anchorPoint是CGPointMake(0.5, 0.5),angle是0,並且重寫- (id)copyWithZone:(NSZone *)zone方法,當物件被拷貝的時候,保證anchorPointangle屬性被賦值。

**注意:**對於自定義的佈局屬性,UICollectionViewCell必須實現下面?這個方法,用來改變cell的錨點,改變錨點會引起layer的position的變化,關於anchorPointposition的關係請看這篇文章

- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes{
    [super applyLayoutAttributes:layoutAttributes];
    //改變錨點(改變錨點會影響center的位置 參考http://www.jianshu.com/p/15f007f40209 )
    WheelCollectionLayoutAttributes *attribute = (WheelCollectionLayoutAttributes*)layoutAttributes;
    self.layer.anchorPoint = attribute.anchorPoint;
    CGFloat centerY = (attribute.anchorPoint.y - 0.5)*CGRectGetHeight(self.bounds);
    self.center = CGPointMake(self.center.x, centerY + self.center.y);
}
複製程式碼

自定義佈局

  1. 首先進行初始化: ?這個方法告訴CollectionView佈局屬性使用自定義的WheelCollectionLayoutAttributes屬性,而不是使用系統預設的UICollectionViewLayoutAttributes屬性。
+ (Class)layoutAttributesClass{
    return [WheelCollectionLayoutAttributes class];
}
複製程式碼
- (instancetype)init{
    self = [super init];
    if (self) {
        /*圓形半徑*/
        self.radius = 500.0;
        /**itemCell的大小*/
        self.itermSize = CGSizeMake(133, 173);

        /*
        每兩個item之間的旋轉角度,由*圖一*可計算得到
        self.anglePerItem可以是任意值
        */
        self.anglePerItem = atan(self.itermSize.width / self.radius);
    }
    return self;
}
複製程式碼
  1. - (void)prepareLayout當collection view第一次顯示在螢幕上時會被呼叫,這個方法初始化佈局屬性,並將佈局屬性儲存下來。
- (void)prepareLayout{
    [super prepareLayout];
    [self.allAttributeArray removeAllObjects];
    NSUInteger numberOfItem = [self.collectionView numberOfItemsInSection:0];
    if (numberOfItem == 0) {
        return;
    }

    /*獲取總的旋轉的角度*/
    CGFloat angleAtExtreme = (numberOfItem - 1) * self.anglePerItem;

    /*隨著UICollectionView的移動,第0個cell初始時的角度*/
    CGFloat angle = -1 *angleAtExtreme * self.collectionView.contentOffset.x / (self.collectionView.contentSize.width - CGRectGetWidth(self.collectionView.bounds));

    /*當前的螢幕中心的的座標*/
    CGFloat centerX = self.collectionView.contentOffset.x + CGRectGetWidth(self.collectionView.bounds)/2.0;

    /*錨點的位置*/
    CGFloat anchorPointY = (self.itermSize.height/2.0 + self.radius) / self.itermSize.height;
    
    for (int i = 0; i < numberOfItem; i++) {
        WheelCollectionLayoutAttributes *attribute = [WheelCollectionLayoutAttributes layoutAttributesForCellWithIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
        attribute.anchorPoint = CGPointMake(0.5, anchorPointY);
        attribute.size = self.itermSize;
        attribute.center = CGPointMake(centerX, CGRectGetMidY(self.collectionView.bounds));
        attribute.angle = angle + self.anglePerItem *i;
        attribute.transform = CGAffineTransformMakeRotation(attribute.angle);
        attribute.zIndex = (int)(-1) *i *1000;
        [self.allAttributeArray addObject:attribute];
    }
}
複製程式碼

起點和終點的狀態.png

由起點和終點的狀態,當self.collectionView.contentOffset.x = 0時,第0個cell的旋轉角度為0,當self.collectionView.contentOffset.x = Max時,第0個cell的旋轉角度最大是(numberOfItem - 1) * self.anglePerItem,所以根據數學知識可以得到第0個cell的旋轉角度和偏移量之間的關係 (向左為負值):

CGFloat angle = -1 *angleAtExtreme * self.collectionView.contentOffset.x / (self.collectionView.contentSize.width - CGRectGetWidth(self.collectionView.bounds));
複製程式碼

所以第i個cell的旋轉的角度為:

attribute.angle = angle + self.anglePerItem *i;
複製程式碼

錨點.png

由上圖可以計算出cell的錨點,X軸方向的值不變,Y軸方向的值發生變化:

CGFloat anchorPointY = (self.itermSize.height/2.0 + self.radius) / self.itermSize.height;
CGFloat anchorPoint = CGPointMake(0.5, anchorPointY);
複製程式碼

3.- (CGSize)collectionViewContentSize返回collectionView的內容大小

- (CGSize)collectionViewContentSize{
    CGFloat numberOfIterm = [self.collectionView numberOfItemsInSection:0];
    return CGSizeMake(numberOfIterm *self.itermSize.width, CGRectGetHeight(self.collectionView.bounds));
}
複製程式碼

4.返回對應的佈局屬性

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
    return self.allAttributeArray;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{
    return self.allAttributeArray[indexPath.item];
}
複製程式碼
  1. collectionView滑動時,重新對collectionView進行佈局
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
    return YES;
}
複製程式碼

最後你可以到GitHub下載Demo。

相關文章