Is CABasicAnimation practice different for UI objects and CALayers? - ios

I was just wondering if this is the correct way to animate a CALayer with a CABasicAnimation.
On Stack Overflow I have been taught how to animate UI objects by setting a new position before running a CABasicAnimation:
Animating a UI Object Example
gameTypeControl.center = CGPointMake(gameTypeControl.center.x, -slidingUpValue/2);
CABasicAnimation *removeGameTypeControl = [CABasicAnimation animationWithKeyPath:#"transform.translation.y"];
[removeGameTypeControl setFromValue:[NSNumber numberWithFloat:slidingUpValue]];
[removeGameTypeControl setToValue:[NSNumber numberWithFloat:0]];
[removeGameTypeControl setDuration:1.0];
[removeGameTypeControl setTimingFunction:[CAMediaTimingFunction functionWithControlPoints:0.8 :-0.8 :1.0 :1.0]];
[[gameTypeControl layer] addAnimation:removeGameTypeControl forKey:#"removeGameTypeControl"];
Now I've been tried this method on a CALayer but it seems to work differently. For me to get the same result. I have the set the ToValue to the new y position instead of using the value 0 like I've done with my UI object animations.
Animating a CALayer Example
serveBlock2.position = CGPointMake((screenBounds.size.height/4)*3, -screenBounds.size.width/2);
CABasicAnimation *updateCurrentServe2 = [CABasicAnimation animationWithKeyPath:#"position.y"];
updateCurrentServe2.fromValue = [NSNumber numberWithFloat:slidingUpValue/2];
[updateCurrentServe2 setToValue:[NSNumber numberWithFloat:-screenBounds.size.width/2]];
[updateCurrentServe2 setDuration:1.0];
[serveBlock2 addAnimation:updateCurrentServe2 forKey:#"serveBlock2 updateCurrentServe2"];
Is this correct? Am I doing this right?

The problem is that if serveBlock2 is not a view's immediately underlying layer, setting its position, as you do in the first line of the second example, starts a different animation (an implicit animation). The way to prevent that is by turning off implicit animations. Thus this example from my book:
CompassLayer* c = (CompassLayer*)self.compass.layer;
[CATransaction setDisableActions:YES]; // <=== this is important
c.arrow.transform = CATransform3DRotate(c.arrow.transform, M_PI/4.0, 0, 0, 1);
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"transform"];
anim.duration = 0.8;
[c.arrow addAnimation:anim forKey:nil];
That way, I don't have to have a fromValue or a toValue! The old value and the new value are known automatically from the presentation layer and the model layer.

Related

Add single CAAnimation to several CALayers

I have one animation, and I want it to be added to two layers:
[view1.layer addAnimation:theAnimation
forKey:#"layer1_animation"];
[view2.layer addAnimation:theAnimation
forKey:#"layer2_animation"];
But when I run my application, only the 1'st view is animating. As documentation tells, layer copies its animations:
This object is copied by the render tree, not referenced. Therefore, subsequent modifications to the object are not propagated into the render tree.
So my code supposed to work correctly. Is it a bug or I'm doing it wrong?
More code:
CGFloat currentY = self.view1.layer.position.y;
CABasicAnimation *yAnimation = [CABasicAnimation animationWithKeyPath:#"position.y"];
yAnimation.fromValue = #(currentY - 2.f);
yAnimation.toValue = #(currentY + 2.f);
yAnimation.duration = 5.0;
yAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
yAnimation.repeatCount = HUGE_VALF;
yAnimation.autoreverses = YES;
yAnimation.fillMode = kCAFillModeBackwards;
[self.view1.layer addAnimation:yAnimation
forKey:#"view1_y_animation"];
[self.view2.layer addAnimation:yAnimation
forKey:#"view2_y_animation"];
The problem was in my code. When initialised fromValue and toValue of the animation, I used y-position of view1. So view1 covered view2 and it was invisible.

setDuration only affects one CALayer when animating multiple CALayers

I'm trying to animate a change in position for numerous CALayers simultaneously. Implicit animations seems to work fine, but if I try to explicitly specify a value for the duration properties of each CALayer, only one is animated while the other is changed successfully but without any animation. Here is the code I am working with:
CGPoint initialPt = CGPointMake(160.0, 10);
CGPoint pt2 = CGPointMake(160.0, 185.0);
CGPoint pt3 = CGPointMake(160.0, 270.0);
CABasicAnimation *firstAnim = [CABasicAnimation animationWithKeyPath:#"postion"];
[firstAnim setFromValue:[NSValue valueWithCGPoint:initialPt]];
[firstAnim setToValue:[NSValue valueWithCGPoint:pt2]];
[firstAnim setDuration:1.0];
[layer1 setPosition:p2];
[layer1 addAnimation:firstAnim forKey:#"Slide"];
CABasicAnimation *secondAnim = [CABasicAnimation animationWithKeyPath:#"position"];
[secondAnim setFromValue:[NSValue valueWithCGPoint:initialPt]];
[secondAnim setToValue:[NSValue valueWithCGPoint:pt3]];
[secondAnim setDuration:1.0];
[layer2 setPosition:p3];
[layer2 addAnimation:secondAnim forKey:#"Slide2"];
I've already tried using CATransaction as well but nothing changed. If I comment out the setDuration method, both layers animate the position change without issue, except that they are at a fixed duration. It seems that for some reason setting the animation explicitly muddles things up but I'm lost as to why. Any help would be much appreciated! Thanks.
The problem is you type the wrong word!
CABasicAnimation *firstAnim = [CABasicAnimation animationWithKeyPath:#"postion"]; change the 'postion' to 'position'
should be
CABasicAnimation *firstAnim = [CABasicAnimation animationWithKeyPath:#"position"]; change the 'postion' to 'position'
'postion' to position

CABasicAnimation - Animating between two angles

I'm attempting to create an animated CALayer that uses a CABasicAnimation in order to animate an object that rotates at discrete intervals between two angles. Note the word discrete: I'm not trying to create a continuous animation between two points, but rather I want to calculate fixed increments between each angle in order to create a discrete movement feel.
Here's a diagram:
I've looked into setting the byValue attribute of the CABasicAnimation, but I can't seem to get it to work since you can't use fromValue, ToValue, and byValue in one animation. fromValue is always from zero, so I guess that could be dropped, but it still never animates correctly when using the current endAngle as the toValue and a byValue of 0.1 (arbitrarily chosen but should work for testing). Any ideas on how to implement this? Here's code I'm using:
anim = [CABasicAnimation animationWithKeyPath:#"currentEndAngle"];
anim.duration = 0.5f;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
anim.toValue = [NSNumber numberWithFloat:self.endAngle];
anim.byValue = [NSNumber numberWithFloat:0.1f];
anim.repeatCount = HUGE_VALF;
[anim setDelegate:self];
[self addAnimation:anim forKey:#"animKey"];
Thanks!

Chaining the animation of a layer and its sublayer (sublayer model effects superlayer animation)

I'm trying to chain the animation of a layer and its sublayer. However, the problem I'm having is that the model update of the sublayer's position for its animation is visually apparent during its superlayer's animation. Here's the code I'm using to chain animations:
// Superlayer.
CFTimeInterval now = [self.artworkContainer.layer convertTime:CACurrentMediaTime() fromLayer:nil];
CABasicAnimation *slideDown = [CABasicAnimation animationWithKeyPath:#"position"];
slideDown.duration = SLIDE_DOWN_DURATION; //SLIDE_DOWN_DURATION;
slideDown.beginTime = now;
slideDown.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
slideDown.fromValue = [self.artworkContainer.layer valueForKey:#"position"];
CGPoint finalPointOfContainer = CGPointMake(self.artworkContainer.layer.position.x, self.artworkContainer.layer.position.y+verticalDistanceOfFirstFall);
slideDown.toValue = [NSValue valueWithCGPoint:finalPointOfContainer];
self.artworkContainer.layer.position = finalPointOfContainer;
[self.artworkContainer.layer addAnimation:slideDown forKey:#"position"];
// Sublayer
CABasicAnimation *moveActualArtwork = [CABasicAnimation animationWithKeyPath:#"position"];
moveActualArtwork.duration = timeOfSecondFall;
moveActualArtwork.beginTime = now + SLIDE_DOWN_DURATION;
moveActualArtwork.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
moveActualArtwork.fromValue = [self.artWork.layer valueForKey:#"position"];
CGPoint finalPointOfArtwork = CGPointMake(self.artWork.layer.position.x, self.artWork.layer.position.y+verticalDistanceOfSecondFall);
moveActualArtwork.toValue = [NSValue valueWithCGPoint:finalPointOfArtwork];
self.artWork.layer.position = finalPointOfArtwork; // If this is commented out, the superlayer animation looks correct (of course this isn't a true fix because the sublayer snaps back to its original position without a model update).
moveActualArtwork.delegate = self;
[self.artWork.layer addAnimation:moveActualArtwork forKey:#"position"];
I found the answer as I was writing the question. I needed to set the fillMode property of the second animation: moveActualArtwork.fillMode = kCAFillModeBackwards;
If one were to think of the fromValue and toValue as positions on a timeline, the problem was that before the sublayer hit the fromValue spot on the timeline, it was working off of its model layer to determine its position. By using kCAFillModeBackwards, the fromValue is effectively extended to cover the timeline previous to it so iOS knows what to render before the animation actually begins.
I found the answer reviewing the WWDC 2011 video Session 421 - Core Animation Essentials, at 45:31.

CABasicAnimation rotate returns to original position

i'm rotating a CALayer using CABasicAnimation and works fine. The problem is, when I try to rotate the same layer, it returns back to its original position before it will rotate. My expected output is that, for the next rotation, it should start from where it has ended. Here's my code:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
animation.fromValue = 0;
animation.toValue = [NSNumber numberWithFloat:3.0];
animation.duration = 3.0;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.autoreverses = NO;
[calayer addAnimation:animation forKey:#"rotate"];
Is there anything missing on my code? thanks
What's happening is that you're seeing the animation in the presentation layer. However, that doesn't update the actual position of your layer. So, once the animation finishes, you see the layer as it was because it hasn't changed.
It's really worth reading the "Core Animation Rendering Architecture". Otherwise this can be very confusing.
To fix it, set a delegate to your CABasicAnimation as follows:
[animation setDelegate:self];
Then, create a method to set your target properties that you want when the animation completes. Now, here's the confusing part. You should do this on animationDidStart not animationDidStop. Otherwise, the presentation layer animation will finish, and you'll get a flicker as you see the calayer in the original position then it jumps - without animation - to the target position. Try it with animationDidStop and you'll see what I mean.
I hope that's not too confusing!
- (void)animationDidStart:(CAAnimation *)theAnimation
{
[calayer setWhateverPropertiesExpected];
}
EDIT:
I later discovered that Apple recommend a much better way to do this.
Oleg Begemann has a nice description of the correct technique in his blog post Prevent Layers from Snapping Back to Original Values When Using Explicit CAAnimations
Basically what you do is before you start the animation, you take a note of the layer's current value, i.e., the original value:
// Save the original value
CGFloat originalY = layer.position.y;
Next, set the toValue on the layer's model. Therefore the layer model has the final value of whatever animation you are about to do:
// Change the model value
layer.position = CGPointMake(layer.position.x, 300.0);
Then, you set the animation up with the animation fromValue being that original value that you noted above:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position.y"];
// Now specify the fromValue for the animation because
// the current model value is already the correct toValue
animation.fromValue = #(originalY);
animation.duration = 1.0;
// Use the name of the animated property as key
// to override the implicit animation
[layer addAnimation:animation forKey:#"position"];
Note that code in edit above was copy/pasted from Ole Begemann's blog for clarity
If you want the animation to start from where it has ended, then set the fromValue property to the CALayer's current rotation.
Obtaining that value is tricky, but this SO post shows you how: https://stackoverflow.com/a/6706604/1072846

Resources