UICollectionView:在选择时动画更改单元格大小

66
我想做的是在选中一个UICollectionViewCell时,更改它的大小并动画显示。我已经通过在collectionView: didSelectItemAtIndexPath:中标记单元格为选中状态,然后调用我的UICollectionView上的reloadData来实现了非动画效果的大小变化。
但是这一切发生得太快了,我不知道如何将大小的变化过渡到动画效果。有什么建议吗?
我已经找到了Animate uicollectionview cells on selection,但答案对我来说不够具体,我还没有弄清楚它是否也适用于我的情况。
10个回答

91

在 Chase Roberts 的帮助下,我现在已经找到了解决我的问题的方法。我的代码基本上看起来像这样:

// Prepare for animation

[self.collectionView.collectionViewLayout invalidateLayout];
UICollectionViewCell *__weak cell = [self.collectionView cellForItemAtIndexPath:indexPath]; // Avoid retain cycles
void (^animateChangeWidth)() = ^()
{
    CGRect frame = cell.frame;
    frame.size = cell.intrinsicContentSize;
    cell.frame = frame;
};

// Animate

[UIView transitionWithView:cellToChangeSize duration:0.1f options: UIViewAnimationOptionCurveLinear animations:animateChangeWidth completion:nil];

进一步解释:

  1. 其中最重要的一步是在使用的 UICollectionView.collectionViewLayout 上调用 invalidateLayout 方法。如果忘记这一步,很可能会发生单元格超出集合视图边界的情况——这是您最不希望发生的事情之一。同时,请考虑您的代码应该在某个时间点将修改后单元格的新尺寸呈现给布局视图。在我的情况下(我正在使用 UICollectionViewFlowLayout),我调整了 collectionView:layout:sizeForItemAtIndexPath 方法以反映修改后单元格的新尺寸。如果忘记了这一点,则如果您先调用 invalidateLayout,然后尝试动画更改大小,则单元格大小根本不会发生任何变化。

  2. 最奇怪的(至少对我来说)但也很重要的是,您不能在动画块内部调用 invalidateLayout 方法。如果这样做,动画不会平稳地显示,而是会出现故障和闪烁。

  3. 您必须使用单元格作为 transitionWithView 的参数来调用 UIViewtransitionWithView:duration:options:animations:completion: 方法,而不是整个集合视图,因为您想要动画的是单元格,而不是整个集合。

  4. 我使用了我的单元格的 intrinsicContentSize 方法来返回所需的大小——在我的情况下,我根据单元格的选择状态调整其宽度以显示或隐藏按钮。

希望这会对一些人有所帮助。对我来说,花了几个小时才弄清楚如何使该动画正确工作,因此我尝试解放其他人的时间负担。


4
很好的发布了跟进。 - Chase Roberts
2
避免保留循环的部分,难道不应该在块之外声明吗?而且由于您正在使用uicollectionview(它是iOS 6+),现在首选的限定符是__weak。如果cell出现在您的块中,则会创建一个强引用,因此您并没有避免任何保留循环。尽管如此,非常感谢您的跟进,非常有帮助! - matehat
3
给那些阅读此文的人一条小建议:在单元格中使用AutoLayout可能会破坏它。我不得不改变单元格的高度约束,然后在动画块内使用[cell layoutIfNeeded]。 - Nailer
1
单元格的更改是有动画效果的,但集合视图会跳转到新的布局。 - openfrog
1
这对我没用。我能够改变单元格框架的大小,但是单元格内部的任何内容都不会改变大小。我甚至尝试修改各个子项的框架,但它们始终保持不变。 - Erika Electra
显示剩余6条评论

52

对于集合视图和表格视图来说,调整单元格大小的动画方式是一样的。如果希望对单元格大小进行动画处理,需要有一个反映单元格状态的数据模型(例如,我只使用标志 (CollectionViewCellExpanded, CollectionViewCellCollapsed) 并相应地返回 CGSize。

当您调用并执行空 performBathUpdates 调用时,视图会自动进行动画处理。

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    [collectionView performBatchUpdates:^{
        // append your data model to return a larger size for
        // cell at this index path
    } completion:^(BOOL finished) {

    }];
}

2
很棒的答案。在我的情况下,我只需要更改模型,然后调用performBatchUpdates,并在块内调用invalidateLayout。 - Dorian Roy
我有一个自定义布局,它包含一个瞬态属性,用于确定哪个索引路径是“突出显示”的索引路径。当我设置该索引路径时,视图会重新绘制并将该索引路径处的项目向集合顶部移动,远离其他项目(有点像Passbook)。设置该属性并按照@DorianRoy建议的方式操作非常有帮助。 - Ben Kreeger
1
这是一个更好的解决方案,特别是考虑到更改在单元格重用后仍然保留。 - dezinezync
对我来说有效,使用的是Swift 4.2 - jangelsb

31

我曾通过使用-setCollectionViewLayout:animated:创建相同的集合视图布局的新实例成功地实现了动画效果。

例如,如果您正在使用UICollectionViewFlowLayout,那么:

// Make sure that your datasource/delegate are up-to-date first

// Then animate the layout changes
[collectionView setCollectionViewLayout:[[UICollectionViewFlowLayout alloc] init] animated:YES];

这是迄今为止最流畅的动画效果。非常棒的解决方案。 - phatmann
其他答案对我都没用——其他答案在单元格上执行了动画,但是使布局无效(无论是显式还是在幕后)也会导致其他单元格的放置出现问题。这是唯一对我有用的答案! - Evan R
这段代码在大多数情况下都能正常工作(已在iPhone4S,iOS8上测试):但是,在动画期间,如果您滚动CollectionView,则单元格将被隐藏。 - chipbk10
缺点是 contentOffset 会改变。 - Iulian Onofrei

25

这是我找到的最简单的解决方案-功能非常棒。流畅的动画,不会破坏布局。

Swift

    collectionView.performBatchUpdates({}){}

Objective-C

    [collectionView performBatchUpdates:^{ } completion:^(BOOL finished) { }];

这些块(在Swift中称为闭包)应该故意保持为空!


2
我曾尝试在更新块内使用 reloadDatamoveItemAtIndexPath: toIndexPath:reloadItemsAtIndexPaths: 来解决问题,但后来发现应该将块保持为空。 现在真的非常完美。 - zalogatomek

22
[UIView transitionWithView:collectionView 
                  duration:.5 
                   options:UIViewAnimationOptionTransitionCurlUp 
                animations:^{

    //any animatable attribute here.
    cell.frame = CGRectMake(3, 14, 100, 100);

} completion:^(BOOL finished) {

    //whatever you want to do upon completion

}];

didselectItemAtIndexPath方法中玩弄类似这样的东西。


1
玩弄这个似乎解决了我的问题。现在我正在根据你提出的更优雅的建议来完善我的解决方案,以便完成后尽快发布。非常感谢你迄今为止的帮助。 - Jan Z.
3
看起来它运行了,但视觉效果很糟糕。它似乎会突然停顿下来。有什么想法吗? - Mike M

12

一种良好的实现单元格大小动画效果的方法是在collectionViewCell本身中重写isHighlighted方法。

class CollectionViewCell: UICollectionViewCell {

    override var isHighlighted: Bool {
        didSet {
            if isHighlighted {
                UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut, animations: {
                    self.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
                }, completion: nil)
            } else {
                UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut, animations: {
                    self.transform = CGAffineTransform(scaleX: 1, y: 1)
                }, completion: nil)
            }
        }
    }

}

1
你说:“一个好的动画单元格大小的方法是在collectionViewCell本身中重写isSelected。”,但你重写了isHighlighted。这是两个不同的状态。 - ShadeToD

10

使用 performBatchUpdates 不会使单元格内容的布局动画化。相反,您可以使用以下方法:

    collectionView.collectionViewLayout.invalidateLayout()

    UIView.animate(
        withDuration: 0.4,
        delay: 0.0,
        usingSpringWithDamping: 1.0,
        initialSpringVelocity: 0.0,
        options: UIViewAnimationOptions(),
        animations: {
            self.collectionView.layoutIfNeeded()
        },
        completion: nil
    )

这将得到非常平滑的动画。

这与Ignatius Tremor的回答类似,但是过渡动画对我来说无法正常工作。

请参见https://github.com/cnoon/CollectionViewAnimations以获取完整解决方案。


我点赞了这个回答,但它仍然不完美。动画会在重新绘制单元格之前拉伸单元格。但实际上,由于单元格的高度也是动画效果的一部分,所以动画效果变得更好了。 - Ariel Bogdziewicz
在iOS 16中,这根本不会动画化。 - Tamás Sengel

2
这对我有效。
    collectionView.performBatchUpdates({ () -> Void in
        let ctx = UICollectionViewFlowLayoutInvalidationContext()
        ctx.invalidateFlowLayoutDelegateMetrics = true
        self.collectionView!.collectionViewLayout.invalidateLayoutWithContext(ctx)
    }) { (_: Bool) -> Void in
    }

performBatchUpdates的内部不是必需的,请参见我的答案。 - Pavel Gurov

0

信不信由你,这个可以运行

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPat`h: IndexPath) {
            
            // Make your item selected in your data source here
                    
             UIView.animate(withDuration: 0.5) {
            // call reload cells in the animation block and it'll update the selected cell's height with an animation 
                        collectionView.reloadItems(at: [indexPath])
                    }
    }

顺便说一下,我正在使用自动单元格高度,即(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.estimatedItemSize = UICollectionViewFlowLayout.automaticSize


0
class CollectionViewCell: UICollectionViewCell {
   override var isHighlighted: Bool {
       if isHighlighted {
            UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut, animations: {
                self.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
            }) { (bool) in
                UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut, animations: {
                    self.transform = CGAffineTransform(scaleX: 1, y: 1)
                }, completion: nil)
            }
        }
   }
}

只需要检查isHighlighted变量是否已更改为true,然后使用UIView动画并在完成处理程序中重新进行动画即可。

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