Remember when we want to set the rectangle back in the previous post ? We fail to do that because we didn’t create animation transaction for that position change. In this post, we will explore three different ways of how Cocoa manages this.
Before we investigate any further, let’s take a look at this demo first.
We want to allow user to cancel animation even before it finishes. That is being responsive, to build a good user experience. Here we get three different situations.
- There is a big jump for red rectangle to complete these two animations
- The green one is slightly better. However, it actually feels like running into the wall, lost the original velocity
- The blue one is the best behavior, it has smooth animation from the beginning to the end
Actually, these three animations represent three different ways of handling animations in iOS. Let me walk you through using the example below.
Here we want the circle to animate from left to right, then in the middle of the animation, we add another animation to bring it back to origin.
First
In order for us to understand what is going on behind the scene, we need to know a little detail about how Cocoa manages animations in general.
Every time you set animatable properties you are actually creating a CAAnimation
object, it is operating on the CALayer
level. However you don’t need to deal with all these details since UIKit takes care for you. So if you want to animate the center, say, demoView.center = CGPointMake(500, 0);
, UIKit will create a CAAnimation
like this:
CAAnimation | |
---|---|
fromValue | (0, 0) |
duration | 1.0 |
beginTime | 1000.1 |
- The fromValue is copied from current model value, since at 1000.0, the model value is (0, 0)
- The duration indicates how long this animation gonna take
If we read this piece of information in natural language, it will be “this is the animation gonna animate from position (0, 0) to the current model value over 1 second staring from 1000.1”. In our case, the next model value will be 500. If you are confused about the model value or presentation value here, check out my very first post for details.
Armed with this CAAnimation
object, we can now observe the changes both in model layer and presentation layer.
Time | 1000.0 | 1000.1 | 1000.2 | 1000.3 | 1000.4 | 1000.5 |
---|---|---|---|---|---|---|
Model value | (0, 0) | (500, 0) | (500, 0) | (500, 0) | (500, 0) | start |
Animation value | n/a | (0, 0) | (50, 0) | (100, 0) | (150, 0) | reverse |
Presentation value | (0, 0) | (0, 0) | (50, 0) | (100, 0) | (150, 0) | animation |
- The
CAAnimation
object remains the same until we are about to start reverse animation at 1000.5 - The model value reflects the animation destination value or
toValue
forCAAnimation
- The presentation values reflects the actual rendering value on the screen
Every thing seems fine until this point. However as we set the center demoView.center = CGPointMake(0, 0)
back to origin at 1000.5, things become more interesting.
Remember the very first CAAnimation
object we created ? It get destroyed with the replacement of new CAAnimation
object. Why ? Because we are setting the animatable property again. Let’s repeat one more time, setting animatable property equals to create a new CAAnimation
object.
CAAnimation | |
---|---|
fromValue | (500, 0) |
duration | 1.0 |
beginTime | 1000.5 |
Notice how this sudden change affects the values in the presentation layer:
Time | 1000.0 | 1000.1 | 1000.2 | 1000.3 | 1000.4 | 1000.5 | 1000.6 | 1000.7 |
---|---|---|---|---|---|---|---|---|
Model value | (0, 0) | (500, 0) | (500, 0) | (500, 0) | (500, 0) | (0, 0) | (0, 0) | (0, 0) |
Animation value | n/a | (0, 0) | (50, 0) | (100, 0) | (150, 0) | (500, 0) | (450, 0) | (400, 0) |
Presentation value | (0, 0) | (0, 0) | (50, 0) | (100, 0) | (150, 0) | (500, 0) | (450, 0) | (400, 0) |
So starting from 1000.4, we will animate from 500 back to 0. However, the value for the presentation layer jumps from 150 to 500 within 0.1 second. That’s why we see the sudden leap for the first ball !
Second
The BeginFromCurrentState
is slightly different with the previous one. Instead of setting the fromValue
for the new CAAnimation
object to the current model value, we set it to be the current presentation value.
CAAnimation | |
---|---|
fromValue | (150, 0) |
duration | 1.0 |
beginTime | 1000.5 |
So there is no sudden value change at presentation layer any more. That means you won’t experience any jump for animation !
Time | 1000.0 | 1000.1 | 1000.2 | 1000.3 | 1000.4 | 1000.5 |
---|---|---|---|---|---|---|
Model value | (0, 0) | (500, 0) | (500, 0) | (500, 0) | (500, 0) | (0, 0) |
Animation value | n/a | (0, 0) | (50, 0) | (100, 0) | (150, 0) | (150, 0) |
Presentation value | (0, 0) | (0, 0) | (50, 0) | (100, 0) | (150, 0) | (150, 0) |
However, now we get another problem. It is the root of causing hitting the wall and losing velocity bug for the second circle.
This is a snapshot for our animating circle. Notice the reverse animation, we are animating the smaller distance over the same amount of time. That’s gonna create problem for us since these two circle’s velocity is not the same !
Third
We’ve already known that beginFromCurrentState
solves the sudden position change in presentation layer. However, it introduces another problem of sudden velocity change. Is there a solution for solving both ? Yes, that is additive animation.
- In additive animation,
fromvalue
andtoValue
are interpreted relatively to the model value - presentation value = model value + animation value
- we don’t destroy
CAAnimation
every time we create a new one. Instead, we add newCAAnimation
to the old one.
CAAnimation1 | CAAnimation2 | |||
---|---|---|---|---|
additive | TRUE | additive | TRUE | |
fromValue | (-500, 0) | fromValue | (500, 0) | |
toValue | (0, 0) | toValue | (0, 0) | |
duration | 1.0 | duration | 0.4 | |
beginTime | 1000.1 | beginTime | 1000.5 |
And we add those values in each layer as well:
Time | 1000.0 | 1000.1 | 1000.2 | 1000.3 | 1000.4 | 1000.5 |
---|---|---|---|---|---|---|
Model value | (0, 0) | (500, 0) | (500, 0) | (500, 0) | (500, 0) | (0, 0) |
Animation 1 | n/a | (-500, 0) | (-450, 0) | (-400, 0) | (-350, 0) | (-300, 0) |
Animation 2 | n/a | n/a | n/a | n/a | n/a | (500, 0) |
Presentation value | (0, 0) | (0, 0) | (50, 0) | (100, 0) | (150, 0) | (200, 0) |
So right now the presentation value = model value + animation 1 value + animation 2 value. And you can see here the presentation value get changed smoothly without losing the original velocity.
The whole idea behind the additive animation is to make relative animation, that is, becoming path independent.
Take Home
- Whenever you set animatable property, you create a
CAAnimation
object - Old
CAAnimation
will be destroyed with the replacement of new one unless you are using additive animation - Additive animation is the default behavior for iOS 8+
- Not all properties support additive animation, if not, use
beginFromCurrentState
- To build better user experience, you need to avoid sudden changes of velocity and position in your custom animation
Reference
- WWDC2014 Building Interruptible and Responsive Interactions
- Core Animation Programming Guide
- CALayer Reference