How do you set the duration for UICollectionView Animations?
Asked Answered
S

9

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;
}
Salman answered 16/10, 2012 at 20:21 Comment(1)
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.Headley
O
31

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"];
}
Overgrow answered 26/2, 2014 at 15:24 Comment(3)
You also need to override +layoutAttributesClass to return [AnimationCollectionViewLayoutAttributes class] in the layout class.Fireweed
+ (Class)layoutAttributesClass { return [AnimationCollectionViewLayoutAttributes class]; }Aalto
add above method in your CustomFlowLayout.m class!Aalto
G
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];
}];
Gillyflower answered 5/2, 2016 at 10:26 Comment(2)
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?Fervency
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.Selfexplanatory
E
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.

Estrogen answered 12/8, 2013 at 20:59 Comment(0)
A
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.

Apiculture answered 28/2, 2013 at 21:35 Comment(0)
C
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;
}
Corium answered 14/9, 2015 at 19:26 Comment(0)
S
3

Without subclassing:

[UIView animateWithDuration:2.0 animations:^{
  [self.collection reloadSections:indexSet];
}];
Spaceless answered 15/4, 2018 at 15:33 Comment(1)
I'm surprised this is the bottom answer. Worked for me. I did my performBatchUpdates within an UIView animate closure instead of reloadSectionsLicensee
N
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
})
Ninon answered 9/8, 2017 at 13:30 Comment(0)
J
0

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

Jori answered 3/2, 2015 at 8:44 Comment(0)
T
0

You can set custom animator using Private API named -performBatchUpdates:withAnimator:

(Caution: Apple will reject your app)

__attribute__((objc_direct_members))
@interface UICollectionView (RA_Category)
- (void)ra_performBatchUpdates:(void (^)(void))updates;
@end

@implementation UICollectionView (RA_Category)

- (void)ra_performBatchUpdates:(void (^)())updates {
    UIViewPropertyAnimator *animator = [[UIViewPropertyAnimator alloc] initWithDuration:2.f curve:UIViewAnimationCurveEaseInOut animations:nil];
    
    reinterpret_cast<void (*)(id, SEL, id, id)>(objc_msgSend)(self,
                                                              sel_registerName("performBatchUpdates:withAnimator:"),
                                                              updates,
                                                              animator);
    
    [animator release];
}

@end

or you can override -[UICollectionViewLayout _propertyAnimatorForCollectionViewUpdates:withCustomAnimator:]

@implementation CollectionViewLayout

- (id)_propertyAnimatorForCollectionViewUpdates:(id)arg1 withCustomAnimator:(id)arg2 {
    UIViewPropertyAnimator *animator = [[UIViewPropertyAnimator alloc] initWithDuration:1.f curve:UIViewAnimationCurveEaseInOut animations:nil];
    
    return [animator autorelease];
}

@end
Thies answered 31/12, 2023 at 12:0 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.