Cabasicanimation Does Not Animate Correctly When I Update Model Layer

CABasicAnimation does not animate correctly when I update model layer

I think that it's easiest to explain what is happening for each of the three locations and then a "conclusion" at the end.

I'm also adding some illustrations, showing exactly the behaviour that you are mentioning in your question so that it will be easier to follow for someone who hasn't tried these three things themselves. I'm also extending the illustration to show both a stand alone layer and a backing layer (one that is attached to a view) and I will explain the difference where there is one.

Location 1

In the first location, the model value is updated before the animation is created. Once this is done, the transform property holds the updatedTransform. This means that when you read the transform from the layer for the fromValue, you get the updatedValue back. This in turn means that both to and from value are the same so you can't see the animation.

Sample Image

One thing that could have made this location work as expected is to read the oldValue before assigning the new value and then use that as the fromValue. This will look as expected.

// Location 1
CATransform3D oldValue = layer.transform; // read the old value first
layer.transform = updatedTransform; // then update to the new value

CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform"];
anim.duration = 1.0;
anim.fromValue = [NSValue valueWithCATransform3D:oldValue];
anim.toValue = [NSValue valueWithCATransform3D:updatedTransform];

Location 2

In the second example, the value isn't yet updated when you read the transform for the from value, so the fromValue and toValue are different. After that, the model value is updated to it's final value. There is actually a difference between the stand-alone layer and the backing layer here, but we don't see it. The transform property on CALayer is animatable and will automatically perform an "implicit" animation when the value changes. This means that an animation will be added to the layer for the "transform" key path. The view, however, disables this behaviour when the change happens outside of an animation block, so there is not implicit animation there.

The reason why we don't see the implicit animation is that the "explicit" animation is added afterwards for the same key path. This means that the only explicit animation will be visible, in both cases, even thought there are two animation running on the stand-alone layer (more on that later). If you are feeling cautious, then you could disable the implicit action for the stand-alone layer (more on that later).

Sample Image

Location 3

This leaves us with the last location. In this case the animation is created just as above, with different fromValue and toValue. The only difference is the order of adding the explicit animation and changing the property which triggers an implicit animation. In this case the implicit animation is added after the explicit animation and they both run(!). Both animations actually ran for location 2, but we couldn't see it because the explicit (longer) animation was added before.

Sample Image

Since everything is moving so fast, I slowed down the entire layer to try and illustrate what is happening when two animations are running at once. This way it becomes much easier to see what happens when the implicit animation ends. I've overlaid the well behaving backing layer and the misbehaving stand-alone layer and made them both 50% transparent. The dashed outline is the original frame.

Sample Image

A short description of what is happening: the blue view get's only the explicit animation added to it (which has a 1 second duration). The orange layer first has the same explicit animation added to it and then has an implicit 0.25 second animation added to it. Neither explicit nor the implicit animations are "additive", meaning their toValue and fromValue are used as-is.

Disclaimer: I do not work at Apple and I haven't seen the source code for Core Animation so what I'm about to say is guesswork based on how things behave.

In my understanding (see disclaimer) this is what happens for every screen refresh to produce the animation: for the current time stamp, the layer goes through the animations in the order they were added and updates the presentation values. In this case, the explicit animation sets a rotation transform, then the implicit animation comes and sets another rotation transform that completely overrides the explicit transform.

If an animation is configured to be "additive", it will add to the presentation values instead of overwriting (which is super powerful). Even with additive animations, order still matters. A non-additive animation could come later and overwrite the whole thing.

Since the implicit animation is shorter than the explicit one, we see that for the first part of the total animation, the values are strictly coming from the implicit animation (which was added last). Once the implicit animation finishes, the only remaining animation is the explicit animation which has been running underneath the implicit one, all this time. So when the implicit animation finishes, the explicit animation has already progressed 0.25 seconds and we see that the orange layer jumps back to the same value as the blue view, instead of jumping back to the beginning.

Where should we update the value?

At this point, the question is, how can we prevent two animations from being added and where should we update the value? The location where the value is updated doesn't prevent there from being two animations (but it can affect how the end result looks).

To prevent two actions from being added to the stand-alone layer, we temporarily disable all "actions" (a more general term for an animation):

[CATransaction begin];
[CATransaction setDisableActions:YES]; // actions are disabled for now
layer.transform = updatedTransform;
[CATransaction commit]; // until here

When we do this, only one animation is added to the layer so either location 2 or 3 works. That is simply a matter of taste. If you read the oldValue, the you can also use location 1 (as long as the action is disabled).

If you are animating a backing layer then you don't have to disable actions (the view does that for you) but it also doesn't hurt to do so.


At this point I could keep going about other ways to configure an animation, what an additive animation is, and why you needed to specify both the toValue and fromValue in this case. But I think that I have answered the question you asked and that this answer already is a bit on the long side.

CABasicAnimation not started when adding animation to a layer that is not visible

Okay found a workaround. I used UIView animation methods instead. Because my animation speed can be changed/switched off during animation I needed to use the following code to avoid kicking of multiple animations on the same view.

This code triggeres the animation :

if (self.currentBlinkFrequency == 0) {
self.shouldContinueBlinking = FALSE;
self.animationIsRunning = FALSE;
} else {
self.shouldContinueBlinking = TRUE;
if (self.animationIsRunning == FALSE) {
self.animationIsRunning = TRUE;
[self blinkAnimation:@"blink" finished:YES target:self.signalImage];
}
}

The invoked anomation code for the "blinkAnimation"-method is borrowed from another post in stackoverflow here

- (void)blinkAnimation:(NSString *)animationId finished:(BOOL)finished target:(UIView *)target
{
if (self.shouldContinueBlinking) {
[UIView beginAnimations:animationId context:target];
[UIView setAnimationDuration:self.currentBlinkFrequency];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(blinkAnimation:finished:target:)];
if ([target alpha] == 1.0f)
[target setAlpha:0.0f];
else
[target setAlpha:1.0f];
[UIView commitAnimations];
}
}

Proper way to update CALayer's model layer with a delayed CABasicAnimation

I would try setting the layer's fillMode to .backwards.

CATransaction: Layer Changes But Does Not Animate

Animations on the root layer of a view are disabled by default. Try applying a transform to the view instead, e.g. [view setTransform:CGTransform3D...]. If you must do it at the layer level, add a layer to the root layer and perform your transforms on it instead. Also the view animation block [UIView beginAnimations...] only has an effect when animating view properties--as opposed to layer properties.

Update:

So here is what your code would look like with explicit animation (CATransaction is not required)

CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform"];
CATransform3D rotateTransform = CATransform3DMakeRotation(0.3, 0, 0, 1);
CATransform3D scaleTransform = CATransform3DMakeScale(0.10, 0.10, 0.10);
CATransform3D positionTransform = CATransform3DMakeTranslation(24, 423, 0);
CATransform3D combinedTransform = CATransform3DConcat(rotateTransform, scaleTransform);
combinedTransform = CATransform3DConcat(combinedTransform, positionTransform);
[anim setFromValue:[NSValue valueWithCATransform3D:CATransform3DIdentity]];
[anim setToValue:[NSValue valueWithCATransform3D:combinedTransform]];
[anim setDuration:1.0f];

[layerToAnimate addAnimation:anim forKey:nil];

Keep in mind that this only performs the animation. You actually have to set the transform property of the layer with a call to:

[layerToAnimate setTransform:combinedTransform];

as well. Otherwise it will just snap back to its starting position.

Layer properties are animated implicitly whenever you set them except in the case where you are animating the root layer of a view. In that case animations are turned off by default and I've found that what I always have to do instead is animate the view rather than the layer when I am interested in animating the root. So what this means is that a call to any layer within your layer tree that makes a property change will be animated implicitly. For example:

CALayer *root = [view layer];

CALayer *sublayer = [[root sublayers] objectAtIndex:0];

[sublayer setTransform:combinedTransform];

This is the only way I know (knew) you can actually use implicit animation on a layer. However, what your code has pointed out is that you can turn the layer animation on for the root layer simply by placing the changes to the layer within a UIView animation block. That's pretty interesting and handy. This is quite a helpful discovery.

Maybe this is buried in the docs somewhere, but I have yet to come across it.

CABasicAnimation not interpolating when I set the end value in animationDidStart:

What you're seeing is, I think, expected behavior. You are setting an animatable property after animation of that property has started. That is in fact the most common and recommended way to do exactly what you are seeing happen, i.e. cancel the animation and jump right to the final position now. So you are deliberately canceling your own animation the minute it gets going.

If that's not what you want, don't do that. Just set the animated property to its final value before the animation is attached to the layer - being careful, of course, not to trigger implicit animation as you do so.

CoreAnimation — Layer flickers to final value before animation starts?

The short-ish answer is that you are getting an implicit animation when you update an implicitly animatable property of a layer (backgroundColor in this case).

This 0.25 second animation is taking precedence over your explicit animation because it is added afterwards. This implicit animation happens because the layer is "standalone" (that is; it's not owned by a view).

To get rid of this implicit animation you can create a CATransaction and disable actions for only the scope that updates the layer's property:

CATransaction.begin()
CATransaction.setDisableActions(true)

redLayer.backgroundColor = UIColor.blue.cgColor

CATransaction.commit()

As an alternative to disabling the implicit animation you can also update the layer before adding the explicit animation. I have a rather detailed explanation of the implications of updating the layer before or after adding the animation in this answer if you want to learn more about this.

CABasicAnimation resets to initial value after animation completes

Here's the answer, it's a combination of my answer and Krishnan's.

cabasicanimation.fillMode = kCAFillModeForwards;
cabasicanimation.removedOnCompletion = NO;

The default value is kCAFillModeRemoved. (Which is the reset behavior you're seeing.)

Seamlessly updating a layer's visual property values after a `CAAnimationGroup` completes?

I've given the long version of this answer previously. There is no reason to repeat the detailed explanation here.

But the short answer is that you are seeing the layer's implicit animations being applied on top of the explicit animation. I have previously written a detailed explanation about implicit and explicit animations and multiple simulations animation, if you want to read more about it.


As described in "the long answer". The solution to your problem is to update the properties, but to temporarily disable the implicit animations so that they aren't applied on top of your explicit animations:

[CATransaction begin];
[CATransaction setDisableActions:YES]; // actions are disabled for now

self.timerProgressLayer.opacity = 0.0;
self.timerProgressLayer.transform = CATransform3DMakeScale(1.0, 1.0, 1.0);

[CATransaction commit]; // until here


Related Topics



Leave a reply



Submit