从零开始UICollectionView(4)--横向动画效果布局及page悬停、增删Item及其动画

前言

上一章节我讲完了纵向瀑布流的布局,有朋友私信我问横向怎么做,其实横向就是你在X轴扩充内容,原本是动态计算高度的,现在变成动态计算宽度,原本是记录列长度的数组,现在用来记录行长度。基本的原理都差不多,大家可以多多自己摸索一下。

横向瀑布流
那么本章节,我们的主要重心就放在布局的变式上面,通过各种有趣的布局方式,来获得我们想要的UI效果。
本章节一共进行如下几个功能的书写:
1.page悬停。
2.横向布局及动画效果。
3.增、删Item及其动画效果。

预备工作:

我们首先要写好最基本的UICollectionView的构建代码,这些都在本系列的第一章节有,就不再赘述。然后我们需要写的是横向布局的布局代码,还记得代码的核心写在哪个方法吗?

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

基本布局的核心代码:

-(void)prepareLayout
{
    [super prepareLayout];
    //为了横向布局,这里我们将列数等于数据源个数
    self.columnCount = [self.collectionView numberOfItemsInSection:0];
    self.columnSpace = 20;//列之间的间距加宽10
    self.rowSpace = 10;
    //由于我们需要从第0个Item到最后一个,要他们和UICollectionView的中心点重合完成Page悬停效果,所以我们做适当改动
    self.sectionInsets = UIEdgeInsetsMake(5.0f, self.collectionView.bounds.size.width*0.35, 5.0f, self.collectionView.bounds.size.width*0.35);;

    [self.columnYArray removeAllObjects];
    for (NSInteger index = 0; index < self.columnCount; index++) {
        [self.columnYArray addObject:@(self.sectionInsets.top)];
    }
    //我们假定数据源只有一组。
    //当然也可以有多组,这样的话我们只要用嵌套循环就可以遍历所有的Item了。
    [self.attributesArray removeAllObjects];
    for (NSInteger index = 0; index<[self.collectionView numberOfItemsInSection:0]; index++) {
    
        UICollectionViewLayoutAttributes * attributes = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
    
        [self.attributesArray addObject:attributes];
    }
}

-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewLayoutAttributes * attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

    CGFloat width = self.collectionView.bounds.size.width;
    CGFloat height = self.collectionView.bounds.size.height;

    CGFloat w = width*0.3;
    CGFloat h = height*0.2;

    CGFloat x = self.sectionInsets.left + (self.columnSpace + w)*indexPath.item;
    CGFloat y = height*0.4;

    attributes.frame = CGRectMake(x, y, w, h);

    //这里我们增加了contentX来记录最长X轴距离,砍掉循环查询
    self.contentX = attributes.frame.origin.x + attributes.frame.size.width;

    return attributes;
}
//由于我们变成了横向布局,所以我们需要改变collectionView的滑动范围
-(CGSize)collectionViewContentSize
{
    return CGSizeMake(self.contentX + self.sectionInsets.right, 0);
}

至此,我们的基本效果就出来了:

基本效果

1.page悬停。
要完成page悬停,我们需要计算当前的x轴偏移量与最近的中心点的差值,然后让UICollectionView加上这段偏移量。

而我们的UICollectionViewLayout提供了一个方法- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity;来告诉我们当前UICollectionView将要滑到的位置和方向。于是利用这一点,我们的代码如下:

-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    CGFloat midCenterX = self.collectionView.center.x;
    CGFloat cardWidth = self.collectionView.bounds.size.width*0.3;

    CGFloat realMidX = proposedContentOffset.x + midCenterX;

    //这里我们用滑动内容的中心点对每个完整的Item求余,获得整数Item以外的偏移量
    CGFloat more = fmodf(realMidX-self.sectionInsets.left, cardWidth+self.columnSpace);
    //上一行获取的偏移量对Item中心点的间距,也就是我们的偏移量需要再增加的偏移量。
     //返回这个经过计算的偏移量,系统会帮我们无痕的完整偏移。
    return CGPointMake(proposedContentOffset.x-(more-cardWidth/2.0), 0);    
}

效果如下(注意看底部的滑动块):

Page悬停

2.横向布局及动画效果。

在预备工作中我们完成了横向布局的书写,那么我们需要对这种死板的布局加上一点动画效果。

通过与中心点距离的比例,我们改变Item的scale和alpha,来完成一个越靠近中心点,透明度越低越大,反之越高越小的布局。这里用到的关键就是UICollectionViewLayoutAttributes的transform和alpha属性。

我们对核心方法做一点改变:

-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewLayoutAttributes * attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

    CGFloat width = self.collectionView.bounds.size.width;
    CGFloat height = self.collectionView.bounds.size.height;

    CGFloat w = width*0.3;
    CGFloat h = height*0.2;

    CGFloat x = self.sectionInsets.left + (self.columnSpace + w)*indexPath.item;
    CGFloat y = height*0.4;

    attributes.frame = CGRectMake(x, y, w, h);

    //这里我们增加了contentX来记录最长X轴距离,砍掉循环查询
    self.contentX = attributes.frame.origin.x + attributes.frame.size.width;

    //获取滑动内容实时显示尺寸的中心点
    CGFloat centerX = self.collectionView.contentOffset.x + self.collectionView.frame.size.width * 0.5;
    //获取当前Item的中心距滑动内容实时显示尺寸的中心点的差值并完成比例计算
    CGFloat delta = ABS(attributes.center.x - centerX);
    CGFloat scale = 1.0 - delta / self.collectionView.frame.size.width;
    //通过比例,来进行2D变形和透明度变化。
    attributes.transform = CGAffineTransformMakeScale(scale, scale);
    attributes.alpha = scale;

    return attributes;
}

重新运行后,得到的效果如图:

横向动效

3.增、删Item及其动画效果。

有时候我们需要再UI上通过交互的形式添加新数据或者删除已有的数据,而且需要配备相应的动画效果,那么我们需要用到如下的代码:

首先我们需要完成增和删的操作(这些操作在UICollectionView的点击事件中完成):

-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    __block ViewController * weakself = self;
    if (indexPath.row%2) {
        //奇数Item的点击我们删除数据源尾部,并且调用CollectionView的deleteItemsAtIndexPaths:方法删除Item
        [self.cardCollectionView performBatchUpdates:^{
            NSMutableArray * theArray = [NSMutableArray arrayWithArray:self.dataArray];
            BJCardModel * model = [BJCardModel new];
            model.indexStr = [NSString stringWithFormat:@"%ld",theArray.count];
            [theArray addObject:model];
            weakself.dataArray = [NSArray arrayWithArray:theArray];
        
            NSIndexPath * indexpath = [NSIndexPath indexPathForItem:self.dataArray.count-1 inSection:0];
            [weakself.cardCollectionView insertItemsAtIndexPaths:@[indexpath]];
        
        } completion:nil];
    }else{
        //偶数Item的点击我们在数据源尾部增加新数据,并且调用CollectionView的insertItemsAtIndexPaths:方法新增Item
        [self.cardCollectionView performBatchUpdates:^{
            NSMutableArray * theArray = [NSMutableArray arrayWithArray:self.dataArray];
            [theArray removeLastObject];
            weakself.dataArray = [NSArray arrayWithArray:theArray];
        
            NSIndexPath * indexpath = [NSIndexPath indexPathForItem:self.dataArray.count inSection:0];
            [weakself.cardCollectionView deleteItemsAtIndexPaths:@[indexpath]];
        
        } completion:nil];
    }

}

然后我们再自定义的Layout中,新增两个数组,来记录新增和删除的updateItem,因为在上面我们可以发现

[weakself.cardCollectionView insertItemsAtIndexPaths:@[indexpath]];
[weakself.cardCollectionView deleteItemsAtIndexPaths:@[indexpath]];

改变都是以数组的形式进行改变,尽管我们一次只改变一个。在多数据更改的情况下,我们需要用数组来记录,并在动画执行时对其完成判断:

//这个方法在即将发生改变时进行,并且提供了需要改变的Item数组
- (void)prepareForCollectionViewUpdates:(NSArray *)updateItems
{
    [super prepareForCollectionViewUpdates:updateItems];

    NSLog(@"准备改变");

    UICollectionViewUpdateItem *update = updateItems[0];
    NSLog(@"%ld -- %ld",update.indexPathBeforeUpdate.section,update.indexPathBeforeUpdate.row);
    NSLog(@"%ld -- %ld",update.indexPathAfterUpdate.section,update.indexPathAfterUpdate.row);
    NSLog(@"%ld",update.updateAction);


    self.deleteIndexPaths = [NSMutableArray array];
    self.insertIndexPaths = [NSMutableArray array];

    for (UICollectionViewUpdateItem *update in updateItems)
    {
        if (update.updateAction == UICollectionUpdateActionDelete)
        {
            [self.deleteIndexPaths addObject:update.indexPathBeforeUpdate];
        }
        else if (update.updateAction == UICollectionUpdateActionInsert)
        {
            [self.insertIndexPaths addObject:update.indexPathAfterUpdate];
        }
    }
}

//这个方法在新增时进行,并且提供了需要改变的Item的IndexPath
-(UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    NSLog(@"插入动画 : %ld -- %ld ",itemIndexPath.section,itemIndexPath.row);

    UICollectionViewLayoutAttributes * att = [super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath];

    if ([self.insertIndexPaths containsObject:itemIndexPath]) {
        if (!att) {
            att = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
        }
    
        att.alpha = 0.1f;
    }

    return att;
}

//这个方法在删除时进行,并且提供了需要改变的Item的IndexPath
-(UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    NSLog(@"删除动画 : %ld -- %ld ",itemIndexPath.section,itemIndexPath.row);

    UICollectionViewLayoutAttributes * att = [super finalLayoutAttributesForDisappearingItemAtIndexPath:itemIndexPath];

    if ([self.deleteIndexPaths containsObject:itemIndexPath]) {
        if (!att) {
            att = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
        }
    
        att.alpha = 1.0f;
        att.transform = CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(90));

    }

    return att;
}

//这个方法发生在改变完成时,我们对数组置nil
- (void)finalizeCollectionViewUpdates
{
    [super finalizeCollectionViewUpdates];

    NSLog(@"完成改变");

    self.deleteIndexPaths = nil;
    self.insertIndexPaths = nil;
}

至此我们的动画效果如下:

效果

我的动画效果比较简陋,纯粹是为了告诉大家怎么走通这个流程而进行的,大家可以根据自己的需要完成一些炫酷的动效。


下节预告:我会完成一些由上述章节内容组合成的有趣的UICollectionView布局,到时作为UICollectionView系列的终结篇,将奉上所有Demo。谢谢各位。


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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,448评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,019评论 4 62
  • 1. 你说,陪一个人走遍大江南北,约定浪迹天涯是一种什么样的感觉? 你说,陪在一个人身边三年,与她做遍各种想做的事...
    他们都叫我晨晨阅读 1,168评论 5 6
  • 生活,总有不如意,酸甜苦辣咸并行。 儿时,你还是小孩,不知道什么是追求,此时你内心想要的恐怕只是吃与乐罢了,对父母...
    孤S凉城阅读 503评论 0 0
  • 路在何方,遥远的她为何迷茫,让自己控制不了的圈子里,在次投入了不应该放的感情里…
    潜雪阅读 302评论 0 0