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

效果图
效果图

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

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

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

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

// 当collectionView滑动的时候,可见区域改变的时候是否使当前布局失效以重新布局
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哈~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容