自定义 UICollectionViewLayout

3,275 阅读3分钟

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];
}

-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
    
    CGRect visibleRect = CGRectMake(self.collectionView.contentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
    
    NSArray *visibleItemArray = [super layoutAttributesForElementsInRect:visibleRect];
    
    for (UICollectionViewLayoutAttributes *attributes in visibleItemArray) {
        
        CGFloat leftMargin = attributes.center.x - self.collectionView.contentOffset.x;
        
        CGFloat halfCenterX = self.collectionView.frame.size.width / 2;
        
        CGFloat absOffset = fabs(halfCenterX - leftMargin);
        
        CGFloat scale = 1 - absOffset / halfCenterX;
        
        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;
}

-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{

    
    CGPoint pInView = [self.collectionView.superview convertPoint:self.collectionView.center toView:self.collectionView];
    
    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;
}

-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity{
    
    
    CGFloat minOffset = CGFLOAT_MAX;
    
    CGFloat horizontalCenter = proposedContentOffset.x + self.collectionView.bounds.size.width / 2;
    
    CGRect visibleRec = CGRectMake(proposedContentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
    
    NSArray *visibleAttributes = [super layoutAttributesForElementsInRect:visibleRec];

    
    for (UICollectionViewLayoutAttributes *atts in visibleAttributes)
    {
        
        CGFloat itemCenterX = atts.center.x;
        
        if (fabs(itemCenterX - horizontalCenter) <= fabs(minOffset)) {
            minOffset = itemCenterX - horizontalCenter;
        }

    }
    

    
    
    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下载:download.csdn.net/detail/qq_3…
github下载地址:github.com/goingmyway1…

理论部分参考:onevcat.com/2012/08/adv… 王巍 先生