自定义UICollectionViewLayout

UICollectionView
首先从collectionView说起,collectionView由三个部分构成:

  • Cells
  • Supplementary Views 追加视图 (类似Header或者Footer)
  • Decoration Views 装饰视图 (用作背景展示)

一方面,collectionView和tableview一样,由提供数据的UICollectionViewDataSource以及处理用户交互的UICollectionViewDelegate支持。另一方面,对于cell的样式和组织方式,由于collectionView比tableView要复杂得多,collectionView使用一个类UICollectionViewLayout对布局和行为进行描述。UICollectionViewLayout也是collectionView的精髓所在。

UICollectionViewLayoutAttributes

UICollectionViewLayoutAttributes是一个非常重要的类,先来看看property列表:

    @property (nonatomic) CGRect frame
    @property (nonatomic) CGPoint center
    @property (nonatomic) CGSize size
    @property (nonatomic) CATransform3D transform3D
    @property (nonatomic) CGFloat alpha
    @property (nonatomic) NSInteger zIndex
    @property (nonatomic, getter=isHidden) BOOL hidden

可以看到,UICollectionViewLayoutAttributes的实例中包含了诸如边框,中心点,大小,形状,透明度,层次关系和是否隐藏等信息。和DataSource的行为十分类似,当UICollectionView在获取布局时将针对每一个indexPath的部件(包括cell,追加视图和装饰视图),向其上的UICollectionViewLayout实例询问该部件的布局信息(在这个层面上说的话,实现一个UICollectionViewLayout的时候,其实很像是zap一个delegate,之后的例子中会很明显地看出),这个布局信息,就以UICollectionViewLayoutAttributes的实例的方式给出。

UICollectionViewLayout

UICollectionViewLayout的功能为向UICollectionView提供布局信息,不仅包括cell的布局信息,也包括追加视图和装饰视图的布局信息。实现一个自定义layout的常规做法是继承UICollectionViewLayout类,然后重载下列方法:

    -(CGSize)collectionViewContentSize
        返回collectionView的内容的尺寸

    -(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
        返回rect中的所有的元素的布局属性
        返回的是包含UICollectionViewLayoutAttributes的NSArray
        UICollectionViewLayoutAttributes可以是cell,追加视图或装饰视图的信息,通过不同的UICollectionViewLayoutAttributes初始化方法可以得到不同类型的UICollectionViewLayoutAttributes:
            layoutAttributesForCellWithIndexPath:
            layoutAttributesForSupplementaryViewOfKind:withIndexPath:
            layoutAttributesForDecorationViewOfKind:withIndexPath:

    -(UICollectionViewLayoutAttributes _)layoutAttributesForItemAtIndexPath:(NSIndexPath _)indexPath
        返回对应于indexPath的位置的cell的布局属性

    -(UICollectionViewLayoutAttributes _)layoutAttributesForSupplementaryViewOfKind:(NSString _)kind atIndexPath:(NSIndexPath *)indexPath
        返回对应于indexPath的位置的追加视图的布局属性,如果没有追加视图可不重载

    -(UICollectionViewLayoutAttributes * )layoutAttributesForDecorationViewOfKind:(NSString_)decorationViewKind atIndexPath:(NSIndexPath _)indexPath
        返回对应于indexPath的位置的装饰视图的布局属性,如果没有装饰视图可不重载

    -(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
        当边界发生改变时,是否应该刷新布局。如果YES则在边界变化(一般是scroll到其他地方)时,将重新计算需要的布局信息。

另外需要了解的是,在初始化一个UICollectionViewLayout实例后,会有一系列准备方法被自动调用,以保证layout实例的正确。

首先,-(void)prepareLayout将被调用,默认下该方法什么没做,但是在自己的子类实现中,一般在该方法中设定一些必要的layout的结构和初始需要的参数等。

之后,-(CGSize) collectionViewContentSize将被调用,以确定collection应该占据的尺寸。注意这里的尺寸不是指可视部分的尺寸,而应该是所有内容所占的尺寸。collectionView的本质是一个scrollView,因此需要这个尺寸来配置滚动行为。

接下来-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect被调用,这个没什么值得多说的。初始的layout的外观将由该方法返回的UICollectionViewLayoutAttributes来决定。

另外,在需要更新layout时,需要给当前layout发送 -invalidateLayout,该消息会立即返回,并且预约在下一个loop的时候刷新当前layout,这一点和UIView的setNeedsLayout方法十分类似。在-invalidateLayout后的下一个collectionView的刷新loop中,又会从prepareLayout开始,依次再调用-collectionViewContentSize-layoutAttributesForElementsInRect来生成更新后的布局。


通过自定义UICollectionViewLayout实现的效果:
这里写图片描述

部分代码:

**
 *  第一个方法
 *
 *  @return 可见区域的内容尺寸
 */
-(CGSize)collectionViewContentSize{
    return [super collectionViewContentSize];
}
/**
 *  重载第二个方法
 *
 *  返回rect中所有元素的布局属性
 *  UICollectionViewAttributes可以是cell、追加视图以及装饰信息,通过以下三个不同的方法可以获取到不同类型的UICollectionViewLayoutAttributes属性
 *  layoutAttributesForCellWithIndexPath:  返回对应cell的UICollectionViewAttributes布局属性
 *  layoutAtttibutesForSupplementaryViewOfKind:withIndexPath: 返回装饰的布局属性 如果没有追加视图可不重载
 *  layoutAttributesForDecorationViewOfKind:withIndexPath: 返回装饰的布局属性  如果没有可以不重载
 *
 *  @return 包含UICollectionViewLayoutAttributes的NSArray
 */
-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{
    //1、获取可见区域
    CGRect visibleRect = CGRectMake(self.collectionView.contentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
    //2、获得这个区域的item
    NSArray *visibleItemArray = [super layoutAttributesForElementsInRect:visibleRect];
    //3、遍历,让靠近中心线的的item方法,离开的缩小
    for (UICollectionViewLayoutAttributes *attributes in visibleItemArray) {
        //1、获取每个item距离可见区域左侧边框的距离 有正负
        CGFloat leftMargin = attributes.center.x - self.collectionView.contentOffset.x;
        //2、获取边框距离屏幕中心的距离(固定的)
        CGFloat halfCenterX = self.collectionView.frame.size.width / 2;
        //3、获取距离中心的偏移量,需要绝对值
        CGFloat absOffset = fabs(halfCenterX - leftMargin);
        //4、获取的实际的缩放比例,距离中心越多,这个值就越小,也就是item的scale越小。中心是最大的。
        CGFloat scale = 1 - absOffset / halfCenterX;
        //5、缩放
        attributes.transform3D = CATransform3DMakeScale(1 + scale * XFMinZoomScale, 1 + scale * XFMinZoomScale, 1);
        //是否需要透明
        if (self.needAlpha) {
            if (scale < 0.6) {
                attributes.alpha = 0.6;
            }else if (scale > 0.99){
                attributes.alpha = 1.0;
            }else{
                attributes.alpha = scale;
            }
        }
    }
    NSArray *attributesArr = [[NSArray alloc]initWithArray:visibleItemArray copyItems:YES];
    return attributesArr;
}
/**
 *  第三个属性重载 滚动的时候一直调用
 *
 *  @param newBounds 这里的newBounds变化的只有x值的变化,也就是偏移量的变化
 *
 *  @return 当边界发生变化的时候,是否应该刷新布局。如果YES那么就是边界发生变化的时候,重新计算布局信息  这里的newBounds变化的只有x值的变化,也就是偏移量的变化
 */
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{

    //把collectionView本身的中心位置(固定的),转换成collectionView整个内容上的point
    CGPoint pInView = [self.collectionView.superview convertPoint:self.collectionView.center toView:self.collectionView];
    //通过坐标获取对应的indexpath
    NSIndexPath *indexPathNow = [self.collectionView indexPathForItemAtPoint:pInView];
    if (indexPathNow.row == 0) {
        if (newBounds.origin.x < SCREEN_WIDTH / 2) {
            if (_index != indexPathNow.row) {
                _index = 0;
                if (self.delegate && [self.delegate respondsToSelector:@selector(collectionViewScrollToIndex:)]) {
                    [self.delegate collectionViewScrollToIndex:_index];
                }
            }
        }
    }else{
        if (_index != indexPathNow.row) {
            _index = indexPathNow.row;
            if (self.delegate && [self.delegate respondsToSelector:@selector(collectionViewScrollToIndex:)]) {
                [self.delegate collectionViewScrollToIndex:_index];
            }
        }
    }
    [super shouldInvalidateLayoutForBoundsChange:newBounds];
    return YES;
}
/**
 *  item自动中心对齐
 *
 *  @param proposedContentOffset <#proposedContentOffset description#>
 *  @param velocity <#velocity description#>
 *
 *  @return 让滚动的item根据距离中心的值,确定哪个必须展示在中心,不会像普通的那样滚动到哪里就停到哪里
 */
-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity{
    //ProposeContentOffset是本来应该停下的位置
    //1、先给一个字段存储最小的偏移量,那么默认就是无限放大
    CGFloat minOffset = CGFLOAT_MAX;
    //2、获取到可见区域的centerX
    CGFloat horizontalCenter = proposedContentOffset.x + self.collectionView.bounds.size.width / 2;
    // 3. 拿到可见区域的rect
    CGRect visibleRec = CGRectMake(proposedContentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
    // 4. 获取到所有可见区域内的item数组
    NSArray *visibleAttributes = [super layoutAttributesForElementsInRect:visibleRec];

    // 遍历数组,找到距离中心最近偏移量是多少
    for (UICollectionViewLayoutAttributes *atts in visibleAttributes)
    {
        // 可见区域内每个item对应的中心X坐标
        CGFloat itemCenterX = atts.center.x;
        // 比较是否有更小的,有的话赋值给minOffset
        if (fabs(itemCenterX - horizontalCenter) <= fabs(minOffset)) {
            minOffset = itemCenterX - horizontalCenter;
        }

    }
    // 这里需要注意的是  上面获取到的minOffset有可能是负数,那么代表左边的item还没到中心,如果确定这种情况下左边的item是距离最近的,那么需要左边的item居中,意思就是collectionView的偏移量需要比原本更小才是,例如原先是1000的偏移,但是需要展示前一个item,所以需要1000减去某个偏移量,因此不需要更改偏移的正负

    // 但是当propose小于0的时候或者大于contentSize(除掉左侧和右侧偏移以及单个cell宽度)  、
    // 防止当第一个或者最后一个的时候不会有居中(偏移量超过了本身的宽度),直接卡在推荐的停留位置
    CGFloat centerOffsetX = proposedContentOffset.x + minOffset;
    if (centerOffsetX < 0) {
        centerOffsetX = 0;
    }

    if (centerOffsetX > self.collectionView.contentSize.width -(self.sectionInset.left + self.sectionInset.right + self.itemSize.width)) {
        centerOffsetX = floor(centerOffsetX);
    }
    return CGPointMake(centerOffsetX, proposedContentOffset.y);
}

demo下载:http://download.csdn.net/detail/qq_34195670/9606956
github下载地址:https://github.com/goingmyway1/UICollectionViewLayout

理论部分参考:https://onevcat.com/2012/08/advanced-collection-view/ 王巍 先生

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值