自定义CollectionView布局—滑动的卡片

效果图

这是一种很常见的布局,可以使用CollectionViewFlowLayout,在其代理方法中通过相关设置来达到此效果,但还是比较麻烦。如若直接用CollectionViewLayout来实现,会简单不少,且灵活性较好。

其实此布局主要考虑的问题就两点:

1.实现任意尺寸的分页大小,即每次滑动后某一张卡片都能停在屏幕中间(默认collectionView开启pageEnable后是以其尺寸来作为分页大小)。
2.放大过程中实现每张卡片的缩放。

官方文档得知自定义CollectionViewLayout至少需要重写以下方法:

// 当collectionView的bounds改变的时候是否使当前布局失效以重新布局
1.shouldInvalidateLayoutForBoundsChange:

// 需要在此方法中返回collectionView的内容大小
2.collectionViewContentSize

// 为每个Cell返回一个对应的Attributes,我们需要在该Attributes中设置对应的属性,如Frame等
3.layoutAttributesForItemAtIndexPath:

// 可在此方法中对可见rect中的cell的属性进行相应设置
4.layoutAttributesForElementsInRect:

我们就以横向滚动的布局为例来实现此布局,首先,定义好所需的属性:

@property (nonatomic, assign) CGFloat spacing; //cell间距
@property (nonatomic, assign) CGSize itemSize; //cell的尺寸
@property (nonatomic, assign) CGFloat scale; //缩放率
@property (nonatomic, assign) UIEdgeInsets edgeInset; //边距

接下来是重写上面提到的几个方法
1. shouldInvalidateLayoutForBoundsChange:

// 由于是此布局是平铺的效果,所以当collectionView的bounds变化时,所展现的cell的个数及显示效果可能会发生变化,故此方法应返回YES。

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
    return YES;
}

2. collectionViewContentSize

// 由于此布局一般只有一个section,故在此例中仅考虑只有一个section的情况
// 内容的宽度为: 左边距 + n*cell的宽 + (n-1)*cell的间距 + 右边距。

- (CGSize)collectionViewContentSize {
    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    CGFloat width = count*(self.itemSize.width+self.spacing)-self.spacing+self.edgeInset.left+self.edgeInset.right;
    CGFloat height = self.collectionView.bounds.size.height;
    return CGSizeMake(width, height);
}

3.layoutAttributesForItemAtIndexPath:

// 此方法要求返回一个UICollectionViewLayoutAttributes * 类型的对象
// 该对象包含对应cell外观所需的必要属性,包括center、frame、transform、alpha及其他属性
// 在此方法中只需要做一件事,那就是给cell设置好正确的frame。

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {

    UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
    attribute.size = self.itemSize;

    CGFloat x = self.edgeInset.left + indexPath.item*(self.spacing+self.itemSize.width);
    CGFloat y = 0.5*(self.collectionView.bounds.size.height - self.itemSize.height);
    attribute.frame = CGRectMake(x, y, attribute.size.width, attribute.size.height);

    return attribute;
}

4.layoutAttributesForElementsInRect:

// 可在此方法中设置缩放效果

- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {

    NSArray *indexPaths = [self indexPathsInRect:rect];

    //找到屏幕中间的位置
    CGFloat centerX =  self.collectionView.contentOffset.x + 0.5*self.collectionView.bounds.size.width;
   NSMutableArray *attributes = [NSMutableArray array];
    for (NSIndexPath *indexPath in indexPaths) {
        UICollectionViewLayoutAttributes* attribute = [self layoutAttributesForItemAtIndexPath:indexPath];
        // 判断可见区域和此cell的frame是否有重叠,因为indexPathsInRect返回的indexPath并不是十分准确。
        if (!CGRectIntersectsRect(rect, attribute.frame)) {
            //若不重叠则无需进行以下的步骤
            continue;
        }
        [attributes addObject:attribute];
        //计算每一个cell离屏幕中间的距离
        CGFloat offsetX = ABS(attribute.center.x - centerX);
        //这是设置一个缩放区域的阈值,当cell在此区域之外不进行缩放,改值可视具体情况进行修改。
        CGFloat space = self.itemSize.width+self.spacing;
        if (offsetX<space) {
            CGFloat scale = 1+(1-offsetX/space)*(self.scale-1);
            attribute.transform = CGAffineTransformMakeScale(scale, scale);
           // 设置此属性是为了当cell层叠后,使得位于中间的cell总是位于最前面,若不明白可将此行注释一试便知。 
            attribute.zIndex = 1;
        }
    }
    return attributes;
}

- (NSArray *)indexPathsInRect:(CGRect)rect {

    NSInteger leftIndex = (rect.origin.x-self.edgeInset.left)/(self.itemSize.width+self.spacing);
    NSInteger rightIndex = (CGRectGetMaxX(rect)-self.edgeInset.left)/(self.itemSize.width+self.spacing);

    NSInteger itemCount = [self.collectionView numberOfItemsInSection:0];
    preIndex = preIndex<0 ? 0 : preIndex;
    latIndex = latIndex>=itemCount ? itemCount-1 : latIndex;

    NSMutableArray *indexPaths = [NSMutableArray array];
    for (NSInteger i=leftIndex; i<=rightIndex; i++) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        [indexPaths addObject:indexPath];
    }
    return indexPaths;
}

至此,我们已经实现了在滚动过程中靠近屏幕中间的cell放大的效果,但是还没实现滚动停止时某一个cell正好在屏幕中间,要想实现此效果,需要在targetContentOffsetForProposedContentOffset:withScrollingVelocity方法中实现此逻辑。此方法会给一个系统默认计算好的collectionView应该停下来的位置,返回一个collectionView最后要停下来的位置。

// 需要在此方法中获取默认情况下停止滚动时离屏幕中间最近的那个cell,并计算两者的距离,将此距离补到proposedContentOffset上即可。

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {

    CGRect rect = CGRectMake(proposedContentOffset.x, proposedContentOffset.y, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
    NSArray *attributes = [self layoutAttributesForElementsInRect:rect];

    CGFloat centerX = proposedContentOffset.x + 0.5*self.collectionView.bounds.size.width;
    CGFloat minOffsetX = MAXFLOAT;
    for (UICollectionViewLayoutAttributes* attribute in attributes) {
        CGFloat offsetX = attribute.center.x - centerX;
        if (ABS(offsetX) < ABS(minOffsetX)) {
            minOffsetX = offsetX;
        }
    }

    return CGPointMake(proposedContentOffset.x + minOffsetX, proposedContentOffset.y);
}

现在已经实现了文章开头动图中展示的效果了。再将之前定义的各个属性的set方法重写,以便在设置这些属性的时候进行重新布局。

注意:若想实现多个section以及含有sectionHeader或sectionFooter,请参照此思路来实现,并且重写以下两个方法:

// 含有sectionHeader或sectionFooter应重写此方法
[layoutAttributesForSupplementaryViewOfKind:atIndexPath:
]

// cell含有装饰视图时要重写此方法。
[layoutAttributesForDecorationViewOfKind:atIndexPath:
]

想要源码的可以点击这里获取,GitHub上的版本支持水平和垂直布局且已加以优化。
此外还有Swift版本的哦,请点击这里以获取。
若觉得对你有用的话,还请给个star哈~

IT文库 » 自定义CollectionView布局—滑动的卡片
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址