Objective-C - CABasicAnimation applying changes after animation?
Asked Answered
G

3

61

I am using CABasicAnimation to move and resize an image view. I want the image view to be added to the superview, animate, and then be removed from the superview.

In order to achieve that I am listening for delegate call of my CAAnimationGroup, and as soon as it gets called I remove the image view from the superview.

The problem is that sometimes the image blinks in the initial location before being removed from the superview. What's the best way to avoid this behavior?

CAAnimationGroup *animGroup = [CAAnimationGroup animation];
    animGroup.animations = [NSArray arrayWithObjects:moveAnim, scaleAnim, opacityAnim, nil];
    animGroup.duration = .5;
    animGroup.delegate = self;
    [imageView.layer addAnimation:animGroup forKey:nil];
Gospodin answered 17/7, 2012 at 3:30 Comment(0)
B
188

When you add an animation to a layer, the animation does not change the layer's properties. Instead, the system creates a copy of the layer. The original layer is called the model layer, and the duplicate is called the presentation layer. The presentation layer's properties change as the animation progresses, but the model layer's properties stay unchanged.

When you remove the animation, the system destroys the presentation layer, leaving only the model layer, and the model layer's properties then control how the layer is drawn. So if the model layer's properties don't match the final animated values of the presentation layer's properties, the layer will instantly reset to its appearance before the animation.

To fix this, you need to set the model layer's properties to the final values of the animation, and then add the animation to the layer. You want to do it in this order because changing a layer property can add an implicit animation for the property, which would conflict with the animation you want to explicitly add. You want to make sure your explicit animation overrides the implicit animation.

So how do you do all this? The basic recipe looks like this:

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
animation.fromValue = [NSValue valueWithCGPoint:myLayer.position];
layer.position = newPosition; // HERE I UPDATE THE MODEL LAYER'S PROPERTY
animation.toValue = [NSValue valueWithCGPoint:myLayer.position];
animation.duration = .5;
[myLayer addAnimation:animation forKey:animation.keyPath];

I haven't used an animation group so I don't know exactly what you might need to change. I just add each animation separately to the layer.

I also find it easier to use the +[CATransaction setCompletionBlock:] method to set a completion handler for one or several animations, instead of trying to use an animation's delegate. You set the transaction's completion block, then add the animations:

[CATransaction begin]; {
    [CATransaction setCompletionBlock:^{
        [self.imageView removeFromSuperview];
    }];
    [self addPositionAnimation];
    [self addScaleAnimation];
    [self addOpacityAnimation];
} [CATransaction commit];
Brandnew answered 17/7, 2012 at 3:45 Comment(11)
Perfect solution, I thought completion blocks were only available on UIView animation methods.Gospodin
This was actually a category, so I had to use associated-object to store the image, and the retrieve it to remove the image after the animation is done. Using this block, I got rid of all the hacky code :)Gospodin
For me, changing the model layer's property AFTER adding the animation worked.Guardsman
This almost works perfectly, except that the model layer appears briefly before the completion block is called. Is there any way to avoid that?Cowboy
But limitation to the first solution is that it doesn't appears correctly if use with "beginTime", as property appears to change before animation starts. And if we use completionhandler a glitch can be seen for a split of a secondNiehaus
If you're getting a glitch then you didn't update the model layer. Using beginTime is an advanced feature and this answer is intended for beginners.Brandnew
Why is using CATransaction's completion block instead of animationDidStop "easier"..? Both are easy and clean. I personally prefer delegates.Stearne
If you don't already have a suitable object handy to use as the delegate, then you have to create one, and you have to store a strong reference to it somewhere (because the animation doesn't retain its delegate), and you have to clean it up when it's no longer needed. Or you can just hang a block off of the transaction. That seems simpler to me.Brandnew
Thanks for your great sharing! Is there any offical doc about model layer and presentation layer?Bollinger
@DesmondDAI developer.apple.com/library/content/documentation/Cocoa/…Brandnew
@DesmondDAI Also this WWDC video is very informative: developer.apple.com/videos/play/wwdc2011/421Brandnew
K
32

CAAnimations are removed automatically when complete. There is a property removedOnCompletion that controls this. You should set that to NO.

Additionally, there is something known as the fillMode which controls the animation's behavior before and after its duration. This is a property declared on CAMediaTiming (which CAAnimation conforms to). You should set this to kCAFillModeForwards.

With both of these changes the animation should persist after it's complete. However, I don't know if you need to change these on the group, or on the individual animations within the group, or both.

Keeley answered 17/7, 2012 at 3:45 Comment(4)
This just keeps the presentation layer visible. In the case of animating a button, you will not be able to interact with the button after the animation this way.Tuberculosis
This is actually the incorrect way of persisting an animation and forces it to remain in memory. The best way to do this is create your animation, add it to the layer and then set the properties you are animating on the layer to the final values.The animation will override the final values for the duration of the animation and when it is finished you will see the layer in the final state. No need for any fillMode weirdness.Gisborne
Animation objects aren't particularly large, so I'm not concerned about "forc[ing] it to remain in memory". And the OP is removing the image view as soon as the animation is complete anyway, so the idea behind keeping the animation on the view was just to cover the gap in between the animation finishing and the view being removed. I do agree that updating the actual properties of the view to match the final animation state is usually what you want, but there are still cases where persisting the animation is useful, so I'll leave my answer as-is.Keeley
But I find a problem of "updating the actual properties of the view to match the final animation state": Sometimes, the model layer would update first before the implicit animations begin. I guess that because the implicit animations are blocked- implicit animations executed in a background thread. Anyone met the same problem?Secundine
U
2

Heres an example in Swift that may help someone

It's an animation on a gradient layer. It's animating the .locations property.

The critical point as @robMayoff answer explains fully is that:

Surprisingly, when you do a layer animation, you actually set the final value, first, before you start the animation!

The following is a good example because the animation repeats endlessly.

When the animation repeats endlessly, you will see occasionally a "flash" between animations, if you make the classic mistake of "forgetting to set the value before you animate it!"

var previousLocations: [NSNumber] = []
...

func flexTheColors() { // "flex" the color bands randomly
    
    let oldValues = previousTargetLocations
    let newValues = randomLocations()
    previousTargetLocations = newValues
    
    // IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
    theLayer.locations = newValues
    
    // AND NOW ANIMATE:
    CATransaction.begin()
    
    // and by the way, this is how you endlessly animate:
    CATransaction.setCompletionBlock{ [weak self] in
        if self == nil { return }
        self?.animeFlexColorsEndless()
    }
    
    let a = CABasicAnimation(keyPath: "locations")
    a.isCumulative = false
    a.autoreverses = false
    a.isRemovedOnCompletion = true
    a.repeatCount = 0

    a.fromValue = oldValues
    a.toValue = newValues
    
    a.duration = (2.0...4.0).random()
    
    theLayer.add(a, forKey: nil)
    CATransaction.commit()
}

The following may help clarify something for new programmers. Note that in my code I do this:

    // IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
    theLayer.locations = newValues
    
    // AND NOW ANIMATE:
    CATransaction.begin()
    ...set up the animation...
    CATransaction.commit()

however in the code example in the other answer, it's like this:

    CATransaction.begin()
    ...set up the animation...
    // IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
    theLayer.locations = newValues
    CATransaction.commit()

Regarding the position of the line of code where you "set the values, before animating!" ..

It's actually perfectly OK to have that line actually "inside" the begin-commit lines of code. So long as you do it before the .commit().

I only mention this as it may confuse new animators.

Unaware answered 10/12, 2017 at 18:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.