避免在reloadItemsAtIndexPaths后对UICollectionView进行动画处理

103
在调用reloadItemsAtIndexPaths方法后,如何对UICollectionView中的项目进行动画(淡入淡出)?是否有避免这种动画的方法?iOS 6。
10个回答

261
值得注意的是,如果您的目标是 iOS 7 及以上版本,则可以使用新的 UIView 方法 performWithoutAnimation:。我怀疑在幕后执行的操作与其他答案(暂时禁用 UIView 动画 / 核心动画操作)相同,但语法简洁明了。
所以对于这个问题来说... Objective-C:
[UIView performWithoutAnimation:^{
    [self.collectionView reloadItemsAtIndexPaths:indexPaths];
}];


Swift:

UIView.performWithoutAnimation {
    self.collectionView.reloadItemsAtIndexPaths(indexPaths)
}


当然,这个原则可以应用于任何你希望确保变化被动画的情况。


3
对我而言,在iOS 7+上,这比被接受的答案更有效。 - Philippe Sabourin
它禁用所有动画,不仅限于集合视图。 - user2159978
12
@user2159978,没错,这里所有的答案都临时禁用了UIView动画。这些动画只在从传入的代码块中发起的操作期间被禁用,在这种情况下只是集合视图的重新加载。这是完全有效的答案,所以我认为它不应该被点踩。 - Stuart
惊人的解决方案。使用这个替代animateWithDuration:0可以避免在使用自动调整大小的集合视图单元格且没有itemSize时出现快速但可见的布局故障。 - Ethan Gill
2
这个解决方案在iOS 14上不再适用,在这个块中我使用了reloadData,但是在iOS 13中它可以工作。 - Maray97

164

您也可以尝试这个:

UICollectionView *collectionView;

...

[UIView setAnimationsEnabled:NO];

[collectionView performBatchUpdates:^{
    [collectionView reloadItemsAtIndexPaths:indexPaths];
} completion:^(BOOL finished) {
    [UIView setAnimationsEnabled:YES];
}];

编辑:

我还发现,如果你在 performBatchUpdates 中包含一个 UIView 动画块,UIView 动画将被使用而不是默认动画,所以你可以只将动画持续时间设置为 0,像这样:

[UIView animateWithDuration:0 animations:^{
    [collectionView performBatchUpdates:^{
        [collectionView reloadItemsAtIndexPaths:indexPaths];
    } completion:nil];
}];

如果您想在插入和删除过程中使用iOS 7弹性动画,这将非常有用!

1
谢谢。这对我在 UICollectionview 单元格中添加秒表计时器非常有帮助。 - hatunike
与其他动画冲突:/ - Peter Lapisu
6
正如Peter所说,这会干扰其他动画。相反,你应该在完成块之外调用[UIView setAnimationsEnabled:YES]。这样,你只会阻止那一个动画。 - Adlai Holler
2
我添加了一种另外的方法,它完成的是完全相同的任务。 - Sam
2
performBatchUpdates包裹在animateWithDuration中是太棒了!感谢这个技巧! - Robert
显示剩余5条评论

22

在调用reloadItemsAtIndexPaths后,UICollectionView中的项目进行动画处理(淡入淡出动画)。

有没有办法避免这种动画效果?

iOS 6

我猜你正在使用FlowLayout。既然你想要摆脱淡入淡出动画效果,试试这个:

import UIKit

class NoFadeFlowLayout: UICollectionViewFlowLayout {

    override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attrs = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath)
        attrs?.alpha = 1.0
        return attrs
    }

    override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attrs = super.finalLayoutAttributesForDisappearingItem(at: itemIndexPath)
        attrs?.alpha = 1.0
        return attrs
    }

}

这是一个非常古老的问题,所以你可能不再针对iOS 6了。我个人正在处理tvOS 11,并且有同样的问题,所以这里是为任何遇到相同问题的人准备的。



3
这篇答案下面有三到四条评论,内容大致是“哇,这个方法真的很有效!”但是有人决定将这些评论删除。事实上,我认为这种现代化且非hack的方法是最好的解决方式,那些评论正是对此的肯定。 - Matt Mc

10

我写了一个关于UICollectionView的类别来实现这一点。诀窍是在重新加载时禁用所有动画:

if (!animated) {
    [CATransaction begin];
    [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
}

[self reloadItemsAtIndexPaths:indexPaths];

if (!animated) {
    [CATransaction commit];
}

我也收到了苹果的回复,他们说这个应该不会有任何动画效果,如果有的话就是一个 bug。我不知道是我做错了什么还是真的有 bug。 - Marcin
2
你也可以使用CATransaction.setDisableActions(true)作为这个过程的简写。 - CIFilter

7
extension UICollectionView {
    func reloadWithoutAnimation(){
        CATransaction.begin()
        CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
        self.reloadData()
        CATransaction.commit()
    }
}

这是 Swift 3 语法。 - Amjad Tubasi
这个效果很好,甚至比UIView.performWithoutAnimation更好。 - Lance Samaria

5

这里是一个Swift 3版本的performBatchUpdates方法,可以在UICollectionView中进行无动画更新。我发现这比使用collectionView.reloadData()更好,因为它减少了插入记录时单元格交换的次数。

func appendCollectionView(numberOfItems count: Int){

        // calculate indexes for the items to be added
        let firstIndex = dataItems.count - count
        let lastIndex = dataItems.count - 1

        var indexPaths = [IndexPath]()
        for index in firstIndex...lastIndex {
            let indexPath = IndexPath(item: index, section: 0)
            indexPaths.append(indexPath)
        }

   UIView.performWithoutAnimation {

        self.collectionView.performBatchUpdates({ () -> Void in
            self.collectionView.insertItems(at: indexPaths)
        }, completion: { (finished) -> Void in

        })
    }
}

2
- (void)reloadCollectionViewAnimated:(BOOL)animated  {

    if (animated) {
        [self.collectionView performBatchUpdates:^{
            [self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
        } completion:^(BOOL finished) {

        }];
    } else {
        [CATransaction begin];
        [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
        [self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
        [CATransaction commit];
    }

}

1
只是想补充一下,我尝试了所选答案的两个版本,原始方法对我的目的更有效。我正在开发一个无限滚动日历视图,允许用户在给定周进入日历,然后左右滑动并选择单个日期以过滤列表。
在我的实现中,为了保持旧设备的性能,表示日历视图的日期数组必须保持相对较小,这意味着保留大约5周的日期,并将用户放在第3周的中间位置。使用第二种方法的问题是,有第二步需要将集合视图向后滚动到中间位置,而没有动画,这会使得基于块的动画出现非常不连贯的外观。
我的代码:
[UIView setAnimationsEnabled:NO];
[self.collectionView performBatchUpdates:^{
    [self.collectionView deleteItemsAtIndexPaths:indexPathDeleteArray];
    [self.collectionView insertItemsAtIndexPaths:indexPathAddArray];

} completion:NULL];
[UIView setAnimationsEnabled:YES];

NSIndexPath *newIndexPath = [NSIndexPath indexPathForItem:14 inSection:0];
[self.collectionView scrollToItemAtIndexPath:newIndexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];

0

不要使用reloadData(),尝试以下方法重新加载所有可见单元格而不带动画。

self.collectionView.reloadItems(at: self.collectionView.indexPathsForVisibleItems)

0
 func reloadRowsWithoutAnimation(at indexPaths: [IndexPath]) {
        let contentOffset = collectionView.contentOffset
        UIView.setAnimationsEnabled(false)
        collectionView.performBatchUpdates {
            collectionView.reloadItems(at: indexPaths)
        }
        UIView.setAnimationsEnabled(true)
        collectionView.setContentOffset(contentOffset, animated: false)
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接