43

I have a custom flow layout which is adjusting the attributes for cells when they are being inserted and deleted from the CollectionView with the following two functions, but I'm unable to figure out how you would adjust the default animation duration.

- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {
    UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];

    // Assign the new layout attributes
    attributes.transform3D = CATransform3DMakeScale(0.5, 0.5, 0.5);
    attributes.alpha = 0;

    return attributes;
}

- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {

    UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];

    // Assign the new layout attributes
    attributes.transform3D = CATransform3DMakeScale(0.5, 0.5, 0.5);
    attributes.alpha = 0;

    return attributes;
}
Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
James Parker
  • 2,095
  • 3
  • 27
  • 48
  • According the Apple's documentation, "When animating layout changes, the animation timing and parameters are controlled by the collection view." This is in reference to the setCollectionView:animated: method, but I suspect that the same is true for modifying the bounds of the collection view. Sorry I can't be more help, I'm stuck on the same problem. I suspect that the answer lies somewhere within the UICollectionView object itself. – Ash Feb 24 '13 at 14:21

9 Answers9

30

To solve problem without hack that was proposed in the answer by gavrix you could subclass UICollectionViewLayoutAttributes with new property CABasicAnimation *transformAnimation, than create custom transformation with a suitable duration and assign it to attributes in initialLayoutAttributesForAppearingItemAtIndexPath, then in UICollectionViewCell apply the attributes as needed:

@interface AnimationCollectionViewLayoutAttributes : UICollectionViewLayoutAttributes
@property (nonatomic, strong)  CABasicAnimation *transformAnimation;
@end

@implementation AnimationCollectionViewLayoutAttributes
- (id)copyWithZone:(NSZone *)zone
{
    AnimationCollectionViewLayoutAttributes *attributes = [super copyWithZone:zone];
    attributes.transformAnimation = _transformAnimation;
    return attributes;
}

- (BOOL)isEqual:(id)other {
    if (other == self) {
        return YES;
    }
    if (!other || ![[other class] isEqual:[self class]]) {
        return NO;
    }
    if ([(( AnimationCollectionViewLayoutAttributes *) other) transformAnimation] != [self transformAnimation]) {
        return NO;
    }

    return YES;
}
@end

In Layout class

- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {
    AnimationCollectionViewLayoutAttributes* attributes = (AnimationCollectionViewLayoutAttributes* )[super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath];

    CABasicAnimation *transformAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
    transformAnimation.duration = 1.0f;
    CGFloat height = [self collectionViewContentSize].height;

    transformAnimation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0, 2*height, height)];
    transformAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0, attributes.bounds.origin.y, 0)];
    transformAnimation.removedOnCompletion = NO;
    transformAnimation.fillMode = kCAFillModeForwards;
    attributes.transformAnimation = transformAnimation;
    return attributes;
}

+ (Class)layoutAttributesClass { 
    return [AnimationCollectionViewLayoutAttributes class]; 
}

then in UICollectionViewCell apply the attributes

- (void) applyLayoutAttributes:(AnimationCollectionViewLayoutAttributes *)layoutAttributes
{
    [[self layer] addAnimation:layoutAttributes.transformAnimation forKey:@"transform"];
}
Community
  • 1
  • 1
zyxel
  • 1,084
  • 3
  • 15
  • 23
25

change CALayer's speed

@implementation Cell
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
    self.layer.speed =0.2;//default speed  is 1
}
return self;
}
rotoava
  • 625
  • 8
  • 18
  • 5
    This works perfectly, glad to say. It's way easier than the other workarounds. I'll add that a higher number means faster animations. – sudo Jun 10 '15 at 01:27
12

Building on @rotava's answer, you can temporarily set the animation speed by using a batch update of the collection view:

[self.collectionView performBatchUpdates:^{
    [self.collectionView.viewForBaselineLayout.layer setSpeed:0.2];
    [self.collectionView insertItemsAtIndexPaths: insertedIndexPaths];
} completion:^(BOOL finished) {
    [self.collectionView.viewForBaselineLayout.layer setSpeed:1];
}];
Ashley Mills
  • 50,474
  • 16
  • 129
  • 160
  • 1
    I'm wondering if the boolean `finished` is important here. At some calls (I don't remember exactly which ones right now), the `completion` block is called more than once. To be completely sure that the animations have finished, I would do `if ( finished ) { /* ... */ }`. Why isn't that necessary here? Or is it and you just skipped it? – Alejandro Iván Jul 14 '16 at 20:37
  • If `performBatchUpdates` has a chance of being called while previous animations are in progress, setting layer's speed back to 1 will cause previous animations to "jump forward" (as time scaling changes), even to final positions. Provided you do not need any other animations (except for those from `performBatchUpdates `) you can set the layer's speed and leave it that way. – Wojciech Nagrodzki Sep 05 '19 at 11:02
5

After trying [CATransaction setAnimationDuration:] and [UIView setAnimationDuration:] in every possible phase of the layout process without success, I figured out a somewhat hacky way to change the duration of cell animations created by UICollectionView that doesn't rely on private API's.

You can use CALayer's speed property to change the relative media timing of animations performed on a given layer. For this to work with UICollectionView, you can change layer.speed to something less than 1 on the cell's layer. Obviously it's not great to have the cell's layer ALWAYS have a non-unity animation speed, so one option is to dispatch an NSNotification when preparing for cell animations, to which your cells subscribe, that will change the layer speed, and then change it back at an appropriate time after the animations are finished.

I don't recommend using this approach as a long-term solution as it's pretty roundabout, but it does work. Hopefully Apple will expose more options for UICollectionView animations in the future.

roperklacks
  • 1,081
  • 11
  • 13
4

UICollectionView initiates all animations internally using some hardcoded value. However, you can always override that value until animations are committed. In general, process looks like this:

  • begin animations
  • fetch all layout attribues
  • apply attributes to views (UICollectionViewCell's)
  • commit animations

applying attributes is done under each UICollectionViewCell and you can override animationDuration in appropriate method. The problem is that UICollectionViewCell has public method applyLayoutAttributes: BUT it's default implementation is empty!. Basically, UICollectionViewCell has other private method called _setLayoutAttributes: and this private method is called by UICollectionView and this private method calls applyLayoutAttributes: at the end. Default layout attributes, like frame, position, transform are applied with current animationDuration before applyLayoutAttributes: is called. That said, you have to override animationDuration in private method _setLayoutAttributes:

- (void) _setLayoutAttributes:(PSTCollectionViewLayoutAttributes *)layoutAttributes
{
    [UIView setAnimationDuration:3.0];
    [super _setLayoutAttributes:layoutAttributes];
}

This is obviously, not applestore-safe. You can use one of those runtime hacks to override this private method safely.

gavrix
  • 321
  • 2
  • 10
4

You can set the layer's speed property (like in Rotoava's Answer) to change the control the speed of the animation. The problem is you are using arbitrary values because you do not know the actual duration of the insertion animation.

Using this post you can figure out what the default animation duration is.

newAnimationDuration = (1/layer.speed)*originalAnimationDuration
layer.speed = originalAnimationDuration/newAnimationDuration

If you wanted to make the animation 400ms long, in your layout you would:

- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewLayoutAttributes* attributes = [super finalLayoutAttributesForDisappearingItemAtIndexPath:indexPath];
    //set attributes here
    UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
    CGFloat originalAnimationDuration = [CATransaction animationDuration];
    CGFloat newAnimationDuration = 0.4f;
    cell.layer.speed = originalAnimationDuration/newAnimationDuration;
    return attributes;
}

In my case I had cells which could be dragged off screen and I wanted to change the duration of the deletion animation based on the speed of the pan gesture.

In the gesture recognizer (which should be part of your collection view):

- (void)handlePanGesture:(UIPanGestureRecognizer *)sender
{
    CGPoint dragVelocityVector = [sender velocityInView:self.collectionView];
    CGFloat dragVelocity = sqrt(dragVelocityVector.x*dragVelocityVector.x + dragVelocityVector.y*dragVelocityVector.y);
    switch (sender.state) {
    ...
    case UIGestureRecognizerStateChanged:{
        CustomLayoutClass *layout = (CustomLayoutClass *)self.collectionViewLayout;
        layout.dragSpeed = fabs(dragVelocity);
    ...
    }
    ...
}

Then in your customLayout:

- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewLayoutAttributes* attributes = [super finalLayoutAttributesForDisappearingItemAtIndexPath:indexPath];
    CGFloat animationDistance = sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1));
    CGFloat originalAnimationDuration = [CATransaction animationDuration];
    CGFloat newAnimationDuration = animationDistance/self.dragSpeed;
    UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
    cell.layer.speed = originalAnimationDuration/newAnimationDuration;
    return attributes;
}
Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
ndey96
  • 79
  • 6
2

Without subclassing:

[UIView animateWithDuration:2.0 animations:^{
  [self.collection reloadSections:indexSet];
}];
Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
bauerMusic
  • 5,470
  • 5
  • 38
  • 53
  • 2
    I'm surprised this is the bottom answer. Worked for me. I did my `performBatchUpdates` within an `UIView` animate closure instead of `reloadSections` – Sandy Chapman Jul 24 '18 at 18:33
1

An update to @AshleyMills since forBaselineLayout is deprecated

This works

self.collectionView.performBatchUpdates({ () -> Void in
    let indexSet = IndexSet(0...(numberOfSections - 1))
    self.collectionView.insertSections(indexSet)
    self.collectionView.forFirstBaselineLayout.layer.speed = 0.5
}, completion: { (finished) -> Void in
    self.collectionView.forFirstBaselineLayout.layer.speed = 1.0
})
Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
Ryan Heitner
  • 13,119
  • 6
  • 77
  • 119
0

You can change UICollectionView layout.speed property, that should change animation duration of your layout...

Skodik.o
  • 506
  • 4
  • 21