UICollectionView自定义布局(一)

4,290 阅读3分钟

前言

最近看了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。