Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CABasicAnimation start from current layer position

This is my second week of Obj-C programming and I'm facing a little problem with animating.

I use this animation:

 CABasicAnimation *fullRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
    fullRotation.toValue = [NSNumber numberWithFloat:((360*M_PI)/180)];
    fullRotation.duration = 4;
    fullRotation.repeatCount= 1000;
    [[stick layer] addAnimation:fullRotation forKey:@"60"];}

This animation starts at the launch of my app , then I click some buttons which change the animation duration when clicked , but the new animations (which have the same code but with different durations) start from the original position of the image "stick". What can I do to make the other animations start from the current position of stick which is doing 360 degrees turns? Thanks.

Part of code for more explanation:

-(void)viewDidAppear:(BOOL)animated{
    CABasicAnimation *fullRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
    fullRotation.toValue = [NSNumber numberWithFloat:((360*M_PI)/180)];
    fullRotation.duration = 4;
    fullRotation.repeatCount= 1000;
    [[stick layer] addAnimation:fullRotation forKey:@"60"];}

- (IBAction)button1:(UIButton *)sender {

 CABasicAnimation *fullRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
    fullRotation.toValue = [NSNumber numberWithFloat:((360*M_PI)/180)];
    fullRotation.duration = 6;
    fullRotation.repeatCount= 1000;
    [[stick layer] addAnimation:fullRotation forKey:@"60"];}
like image 436
Joseph Avatar asked Aug 05 '14 14:08

Joseph


1 Answers

There are a couple of different ways to achieve the result you are after but before looking at those, I should explain the root cause of the problem.

What is happening

What you see on screen during an animation does not necessary match the values of properties on those layers. In fact, adding an animation to a layer does not change the animated property of the layer. The animation and the values that you see on screen happens in the render server which runs in another process than your application. You can't get to those exact values but you can get to an approximation, called the presentation values. Since we can't get to the values of the render server we often only talk about the model values (the actual values on your layer object) and the presentation values (what appears on screen (or at least a very close approximation of it)).

Only specifying a toValue for a CABasicAnimation means the that it animates from the current model value and the specified to value. Note that the documentation says that it's the current presentation value but that is actually incorrect. Since the model value never changes, this means that when the second animation is added, it animates from the unchanged, unrotated model value to the toValue.

(As a side note: since the two animations use the same key the new one replaces the old one. This doesn't really matter since the animations aren't additive so even if the animation wasn't replaced, the new animation would write its value over the old animations value).

Different ways of fixing it

There a many ways to get the behaviour that you want, starting with the simplest version.

Adding an explicit fromValue

As mention above, with only an non-nil toValue, the animation is going to start from the current model value. You can however, add a non-nil fromValue that is the current presentation value.

You can get to the presentation value from the layer's presentationLayer. From it, you can get the current rotation angle using the same key path that you are animating and a little KVC (key-value coding):

NSNumber *currentAngle = [switch.layer.presentationLayer valueForKeyPath:@"transform.rotation"];

If you would only set that as the fromValue of the animation you would get the right start value but the rotation may not be 360 degrees anymore. While you could figure out the angle and create a new toValue, there is another property called byValue which produces a relative change. Specifying byValue and fromValue (but not toValue) means:

Interpolates between fromValue and (fromValue + byValue).

So, you can change your animation code to:

CABasicAnimation *fullRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
fullRotation.fromValue = currentAngle; // the value read from the presentation layer
fullRotation.byValue   = @(2.0*M_PI);
fullRotation.duration  = 6.0;

At this point your code should continue from the right value but the overall speed of the animation may look a bit odd (it should have looked odd even before this code change). For a view that rotates continuously you would probably want it to always rotate with the same speed (as opposed to accelerating and decelerating for each rotation). You can get this by configuring the animation to have a linear timing function:

fullRotation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];

This should give you the behaviour that you are after. Some stylistic remarks would be to specify either INFINITY or HUGE_VALF for the repeat count unless you want to it rotate exactly 1000 times, and to use a more descriptive key when adding the layer (unless you are using the key for something else (like removing the animation from the layer):

fullRotation.repeatCount    = INFINITY;
[stick.layer addAnimation:fullRotation forKey:@"rotate continuously"];

Altering the speed of the layer

I would think twice before using this as a solution in production code but it can be fun as a learning exercise. Since what you are mostly doing is slowing the animation down by changing the duration from 4 seconds to 6 seconds, one thing you can do to create the same effect is to slow down the layer. Note that this will have side effects (all animation on this layer and all of its sublayers will also be affected).

You can't modify the animation once it has been added to a layer but the layer itself also conforms to the CAMediaTiming protocol which means that it has the speed property. Setting the speed to 4.0/6.0 and keeping the old animation on the layer will slow it down making each rotation take 6 seconds instead of 4.

Once again, a bit hacky but fun as a learning exercise.

like image 138
David Rönnqvist Avatar answered Oct 14 '22 19:10

David Rönnqvist