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
Related
I have a UIImageView that when the user taps it, a border of 4 points toggles on and off. I'm trying to animate the border in and out as follows:
CABasicAnimation *widthAnimation = [CABasicAnimation animationWithKeyPath:#"borderWidth"];
widthAnimation.toValue = self.isSelected ? #4.0 : #0.0;
widthAnimation.duration = 0.1;
[self.imageView.layer addAnimation:widthAnimation forKey:#"borderWidth"];
Now, as I've learned from research and scouring SO, CABasicAnimation just changes the presentation layer, but not the actual model. I've also read that using fillMode and removedOnCompletion is bad practice, since it leads to inconsistencies between the model and what the user sees. So, I tried to change the model with the following line:
self.imageView.layer.borderWidth = self.isSelected ? 4.0 : 0.0;
The problem is, this line seems to set the property straight away, so by the time the animation kicks in, the border width is already at it's desired value. I've tried sticking this line at the beginning of the code, end, and everywhere in between, but to no success. I did manage to find a hacky solution: instead of setting the property, I passed the property setter to performSelector: withObject: afterDelay:, with the delay being the duration of the animation. This works most of the time, but sometimes the cycles don't quite match up, and the animation will run first, then it jumps back to the original state, then it snaps to the new state, presumably as a result of performSelector
So is there any way to smoothly animate a border without performSelector?
Any help is greatly appreciated.
Here is an example of CABasicAnimation I made a while ago :
-(void) animateProgressFrom:(CGFloat)fromValue to:(CGFloat)toValue
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"opacity"];
animation.fromValue = #(fromValue);
animation.toValue = #(toValue);
animation.duration = ABS(toValue - fromValue)*3.0;
[self.layer addAnimation:animation forKey:#"opacity"];
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.layer.opacity = toValue;
[CATransaction commit];
}
I think what you needed is the CATransaction at the end of the layer animation.
So, I am fairly new to iOS programming, and have inherited a project from a former coworker. We are building an app that contains a gauge UI. When data comes in, we want to smoothly rotate our "layer" (which is a needle image) from the current angle to a new target angle. Here is what we have, which worked well with slow data:
-(void) MoveNeedleToAngle:(float) target
{
static float old_Value = 0.0;
CABasicAnimation *rotateCurrentPressureTick = [CABasicAnimation animationWithKeyPath:#"transform.rotation");
[rotateCurrentPressureTick setDelegate:self];
rotateCurrentPressureTick.fromValue = [NSSNumber numberWithFloat:old_value/57.2958];
rotateCurrentPressureTick.removedOnCompletion=NO;
rotateCurrentPressureTick.fillMode=kCAFillModeForwards;
rotateCurrentPressureTick.toValue=[NSSNumber numberWithFloat:target/57.2958];
rotateCurrentPressureTick.duration=3; // constant 3 second sweep
[imageView_Needle.layer addAnimation:rotateCurrentPressureTick forKey:#"rotateTick"];
old_Value = target;
}
The problem is we have a new data scheme in which new data can come in (and the above method called) faster, before the animation is complete. What's happening I think is that the animation is restarted from the old target to the new target, which makes it very jumpy.
So I was wondering how to modify the above function to add a continuous/restartable behavior, as follows:
Check if the current animation is in progress and
If so, figure out where the current animation angle is, and then
Cancel the current and start a new animation from the current rotation to the new target rotation
Is it possible to build that behavior into the above function?
Thanks. Sorry if the question seems uninformed, I have studied and understand the above objects/methods, but am not an expert.
Yes you can do this using your existing method, if you add this bit of magic:
- (void)removeAnimationsFromView:(UIView*)view {
CALayer *layer = view.layer.presentationLayer;
CGAffineTransform transform = layer.affineTransform;
[layer removeAllAnimations];
view.transform = transform;
}
The presentation layer encapsulates the actual state of the animation. The view itself doesn't carry the animation state properties, basically when you set an animation end state, the view acquires that state as soon as you trigger the animation. It is the presentation layer that you 'see' during the animation.
This method captures the state of the presentation layer at the exact moment you cancel the animation, and then applies that state to the view.
Now you can use this method in your animation method, which will look something like this:
-(void) MoveNeedleToAngle:(float) target{
[self removeAnimationsFromView:imageView_Needle];
id rotation = [imageView_Needle valueForKeyPath:#"layer.transform.rotation.z"];
CGFloat old_value = [rotation floatValue]*57.2958;
// static float old_value = 0.0;
CABasicAnimation *rotateCurrentPressureTick = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
[rotateCurrentPressureTick setDelegate:self];
rotateCurrentPressureTick.fromValue = [NSNumber numberWithFloat:old_value/57.2958];
rotateCurrentPressureTick.removedOnCompletion=NO;
rotateCurrentPressureTick.fillMode=kCAFillModeForwards;
rotateCurrentPressureTick.toValue=[NSNumber numberWithFloat:target/57.2958];
rotateCurrentPressureTick.duration=3; // constant 3 second sweep
[imageView_Needle.layer addAnimation:rotateCurrentPressureTick forKey:#"rotateTick"];
old_value = target;
}
(I have made minimal changes to your method: there are a few coding style changes i would also make, but they are not relevant to your problem)
By the way, I suggest you feed your method in radians, not degrees, that will mean you can remove those 57.2958 constants.
You can get the current rotation from presentation layer and just set the toValue angle. No need to keep old_value
-(void) MoveNeedleToAngle:(float) targetRadians{
CABasicAnimation *animation =[CABasicAnimation animationWithKeyPath:#"transform.rotation"];
animation.duration=5.0;
animation.fillMode=kCAFillModeForwards;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
animation.removedOnCompletion=NO;
animation.fromValue = [NSNumber numberWithFloat: [[layer.presentationLayer valueForKeyPath: #"transform.rotation"] floatValue]];
animation.toValue = [NSNumber numberWithFloat:targetRadians];
// layer.transform= CATransform3DMakeRotation(rads, 0, 0, 1);
[layer addAnimation:animation forKey:#"rotate"];
}
Another way i found (commented line above) is instead of using fromValue and toValue just set the layer transform. This will produce the same animation but the presentationLayer and the model will be in sync.
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.
I am using a CABasicAnimation to rotate a UIImageView 90 degrees clockwise, but I need to have it rotate a further 90 degrees later on from its position after the initial 90 degree rotation.
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.duration = 10;
animation.additive = YES;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.fromValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(0)];
animation.toValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(90)];
[_myview.layer addAnimation:animation forKey:#"90rotation"];
Using the code above works initially, the image stays at a 90 degree angle. If I call this again to make it rotate a further 90 degrees the animation starts by jumping back to 0 and rotating 90 degrees, not 90 to 180 degrees.
I was under the impression that animation.additive = YES; would cause further animations to use the current state as a starting point.
Any ideas?
tl;dr: It is very easy to misuse removeOnCompletion = NO and most people don't realize the consequences of doing so. The proper solution is to change the model value "for real".
First of all: I'm not trying to judge or be mean to you. I see the same misunderstanding over and over and I can see why it happens. By explaining why things happen I hope that everyone who experience the same issues and sees this answer learn more about what their code is doing.
What went wrong
I was under the impression that animation.additive = YES; would cause further animations to use the current state as a starting point.
That is very true and it's exactly what happens. Computers are funny in that sense. They always to exactly what you tell them and not what you want them to do.
removeOnCompletion = NO can be a bitch
In your case the villain is this line of code:
animation.removedOnCompletion = NO;
It is often misused to keep the final value of the animation after the animation completes. The only problem is that it happens by not removing the animation from the view. Animations in Core Animation doesn't alter the underlying property that they are animating, they just animate it on screen. If you look at the actual value during the animation you will never see it change. Instead the animation works on what is called the presentation layer.
Normally when the animation completes it is removed from the layer and the presentation layer goes away and the model layer appears on screen again. However, when you keep the animation attached to the layer everything looks as it should on screen but you have introduced a difference between what the property says is the transform and how the layer appears to be rotated on screen.
When you configure the animation to be additive that means that the from and to values are added to the existing value, just as you said. The problem is that the value of that property is 0. You never change it, you just animate it. The next time you try and add that animation to the same layer the value still won't be changed but the animation is doing exactly what it was configured to do: "animate additively from the current value of the model".
The solution
Skip that line of code. The result is however that the rotation doesn't stick. The better way to make it stick is to change the model. Set the new end value of the rotation before animating the rotation so that the model looks as it should when the animation gets removed.
byValue is like magic
There is a very handy property (that I'm going to use) on CABasicAnimation that is called byValue that can be used to make relative animations. It can be combined with either toValue and fromValue to do many different kinds of animations. The different combinations are all specified in its documentation (under the section). The combination I'm going to use is:
byValue and toValue are non-nil. Interpolates between (toValue - byValue) and toValue.
Some actual code
With an explicit toValue of 0 the animation happens from "currentValue-byValue" to "current value". By changing the model first current value is the end value.
NSString *zRotationKeyPath = #"transform.rotation.z"; // The killer of typos
// Change the model to the new "end value" (key path can work like this but properties don't)
CGFloat currentAngle = [[_myview.layer valueForKeyPath:zRotationKeyPath] floatValue];
CGFloat angleToAdd = M_PI_2; // 90 deg = pi/2
[_myview.layer setValue:#(currentAngle+angleToAdd) forKeyPath:zRotationKeyPath];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:zRotationKeyPath];
animation.duration = 10;
// #( ) is fancy NSNumber literal syntax ...
animation.toValue = #(0.0); // model value was already changed. End at that value
animation.byValue = #(angleToAdd); // start from - this value (it's toValue - byValue (see above))
// Add the animation. Once it completed it will be removed and you will see the value
// of the model layer which happens to be the same value as the animation stopped at.
[_myview.layer addAnimation:animation forKey:#"90rotation"];
Small disclaimer:
I didn't run this code but am fairly certain that it runs as it should and that I didn't do any typos. Correct me if I did. The entire discussion is still valid.
pass incremental value of angle see my code
static int imgAngle=0;
- (void)doAnimation
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.duration = 5;
animation.additive = YES;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.fromValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(imgAngle)];
animation.toValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(imgAngle+90)];
[self.imgView.layer addAnimation:animation forKey:#"90rotation"];
imgAngle+=90;
if (imgAngle>360) {
imgAngle = 0;
}
}
Above code is just for idea. Its not tested
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.