iOS 在CollectionView上做展开收起动画

Posted 想名真难

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 在CollectionView上做展开收起动画相关的知识,希望对你有一定的参考价值。

1.目标效果:

在一个collectionView上有一个cell, cell上有一个按钮点击收起/展开, 根据用户的操作展示不同的高度.

实现效果如下, 本来传的gif, 但是csdn一致提示违规, 传成静态, 剩下靠脑补:

 

2.遇到的问题:

一开始使用改动是reloadItemsAtIndexPaths 进行刷新, 出现了展开时下面的cell被遮挡, 收起时会有部分区域黑一下, 然后在出现收起的动画, 达不到目标效果

看下官方文档对reloadItemsAtIndexPaths的解释

Reloads just the items at the specified index paths.

Call this method to selectively reload only the specified items. This causes the collection view to discard any cells associated with those items and redisplay them.


重新加载指定数组内的indexPath的元素。

调用此方法可以选择性地仅重新加载指定的元素。这会导致collectionView视图取消与这些项关联的cell,并重新显示它们。

根据官方文档的注释和实际测试, 使用此方法只刷新一个分区数据, 系统会再次触发 - cellForItemAtIndexPath, 由于此时屏幕上已经有一个同类型的cell, 系统会在创建一个新的cell并添加到复用池中, 此时对旧的cell做任何动画都是无效的, 因为现在有一个更高层级的cell盖在上面.

3.解决方案: 

使用performBatchUpdates:completion:

Animates multiple insert, delete, reload, and move operations as a group. 

将多个插入、删除、重新加载和移动操作作为一个组设置动画。

此方法不会触发- cellForItemAtIndexPath, 所以不会重新创建cell, 对cell本身做的动画能够正常显示. 

同时, 此方法会触发- sizeForItemAtIndexPath, 保证cell本身的高度是正确的, collectionView的整体高度和下面的cell会自动下移完成动画.

4.总结:

总结一下在collectionView做动画的流程:

  1. 使用UIView的animationBlock指定动画时长, 把2-4放到block中执行
  2. 更新cell内部的数据
  3. 使用 performBatchUpdates:completion:  刷新collectionView上cell的高度
  4. 调用layout方法刷新UI, 调用collectionView 或者 cell都可以
// 展开收起动画, 
// 1.更新cell内部的数据;
// 2.调用刷新cell高度的方法;
// 3.collectionView进行layout
[UIView animateWithDuration:0.3 animations:^

   [cell updateWithRowNode:rowNode];

   [self.collectionView performBatchUpdates:^
     completion:^(BOOL finished) 
    ];

    [self.collectionView layoutIfNeeded];
    [self.collectionView reloadData];

];

最后放上核心代码, 有删减,

/// 点击展开/收起按钮
- (void)memberCenterStatusCell:(ULMemberCenterStatusCell *)cell didClickFold:(UIButton *)button rowNode:(ULTopOpenMemberRowNode *)rowNode 

    if (rowNode.uiType == ULTopOpenMemberRowNodeUITypeMember) 
        NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell];
        rowNode.isOpen = !rowNode.isOpen;
        ULMemberCenterSectionRowNode *sectionRowNode = [self.dataArray ul_safeObjectAtIndex:indexPath.section];
        sectionRowNode.itemSize = [rowNode currentSize];
        // 展开收起动画, 
        // 1.更新cell内部的数据;
        // 2.调用刷新cell高度的方法;
        // 3.collectionView进行layout
        [UIView animateWithDuration:0.3 animations:^

            [cell updateWithRowNode:rowNode];

           [self.collectionView performBatchUpdates:^

             completion:^(BOOL finished) 

            ];

            [self.collectionView layoutIfNeeded];
            [self.collectionView reloadData];

        ];
    



- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath 

    CGSize result = CGSizeZero;
    ULMemberCenterSectionRowNode *sectionRowNode = [self.dataArray ul_safeObjectAtIndex:indexPath.section];
     result = sectionRowNode.itemSize;
    return result;


- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 
    UICollectionViewCell *resultCell = nil;
    ULMemberCenterSectionRowNode *sectionRowNode = [self.dataArray ul_safeObjectAtIndex:indexPath.section];
    if (sectionRowNode.type == ULMemberCenterSectionRowNodeTypeOpenMember) 
        ULMemberCenterStatusCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[ULMemberCenterStatusCell reuseIdentifier] forIndexPath:indexPath];
        ULTopOpenMemberRowNode *openMemberRowNode = [sectionRowNode.rowNodeArray ul_safeObjectAtIndex:indexPath.item];
        cell.delegate = self;
        [cell updateWithRowNode:openMemberRowNode];
        resultCell = cell;
     
     return resultCell;


还有2个坑, 由于collectionview内部视图的高度发生了变化, 本来有的cell是不能漏出的, 在收起的时候, 这个cell变的可以出现了, 因此出现了2个坑.

第一个坑是有概率出现一个crash,

index path为4-0的 LayoutAttributes 从 0x106d54940 变成了 0x106d54830, 而没有调用 invalidating the layout. 

简略信息:

layout attributes for supplementary item at index path (<NSIndexPath: 0xee9228e53cb9843e> length = 2, path = 4 - 0) changed from <ULMemberCenterLayoutAttributes: 0x106d54940>  to <ULMemberCenterLayoutAttributes: 0x106d54830>  without invalidating the layout

完整内容如下:

layout attributes for supplementary item at index path (<NSIndexPath: 0xee9228e53cb9843e> length = 2, path = 4 - 0) changed from <ULMemberCenterLayoutAttributes: 0x106d54940> index path: (<NSIndexPath: 0xee9228e53cb9853e> length = 2, path = 5 - 0); element kind: (ULMemberCenterFlowLayoutSectionBackground); frame = (0 1355; 375 320); zIndex = -1; to <ULMemberCenterLayoutAttributes: 0x106d54830> index path: (<NSIndexPath: 0xee9228e53cb9843e> length = 2, path = 4 - 0); element kind: (ULMemberCenterFlowLayoutSectionBackground); frame = (0 990; 375 300); zIndex = -1; without invalidating the layout

看的一头雾水, 而且bugly上出现次数不多, , 好在stackoverflow上有个解答, 只需要设置禁止cell的预加载即可.

if (@available(ios 10.0, *)) 
    self.collectionView.prefetchingEnabled = NO;

另一个设置 - shouldInvalidateLayoutForBoundsChange , 亲测对我的场景无效, 所以也没有加. 

https://stackoverflow.com/questions/19207924/uicollectionview-exception-in-uicollectionviewlayoutattributes-from-ios7

第二个坑是, 收起动画的时候, 如果屏幕底部的cell没有渲染, 那么系统在做收起动画的时候, 底部的cell是直接出现在结束位置的, 没有动画,

注意看最底部的3个cell, 在收起动画的时候, 是直接出现在结束位置的, 因为在顶部展开状态下, 这3个cell根本没有创建, 收起的时候, 系统才创建这3个cell, 而且系统创建好直接放到的cell的结束位置.

知道了原因, 修改的方法也有了:

本来collectionView的大小和父视图是一致的, 但是希望collectionView多向下渲染100像素(收起/展开状态下顶部cell的高度差距),  这样在收起动画的时候, 其实下面的100像素上已经有这3个cell了, 这3个cell也能跟随其他cell一起向上位移. 由于collectionView的高度多了100, 所以也需要配合contentInset的底部额外多出100像素,

[self.collectionView makeConstraints:^(MASConstraintMaker *make) 
    make.top.left.right.equalTo(0);
    make.height.equalTo(self.mas_height).offset(100);
];
self.collectionView.contentInset = UIEdgeInsetsMake(0, 0, 100, 0);

 

正常情况下, 最底部的3个cell是不会创建的, 但是由于collectionView的高度变高, 底部的cell已经被提前创建好, 只是用户看不到而已, 

经过这样的奇技淫巧, 动画看上去和谐多了. 打卡下班.

以上是关于iOS 在CollectionView上做展开收起动画的主要内容,如果未能解决你的问题,请参考以下文章

iOS 文本展开收起

iOS-文本内容展开/收起实现方案

APP前端请求后台时,“展开与收起”交互中渐进使用动画

css展开收起样式

HTML中列表收起与展开

在选择画面中收起/展开字段