CAKeyframeAnimation repeating rotation from last point - ios

In my iphone app I have a UIButton that I rotate 180 degrees into view when another UIButton is pressed, then when clicked again the button rotates a further 180 degrees back to where it started.
This all works fine the very first time the complete 360 degree process occurs, but if I try to start from the start again it snaps 180 degrees then tries to rotate it from that point. Can anyone point me in the right direction? Here's my code so far...
showAnimation= [CAKeyframeAnimation animationWithKeyPath:#"transform.rotation"];
showAnimation.duration = self.showAnimationDuration;
showAnimation.repeatCount = 1;
showAnimation.fillMode = kCAFillModeForwards;
showAnimation.removedOnCompletion = NO;
showAnimation.cumulative = YES;
showAnimation.delegate = self;
float currentAngle =[[[rotateMe.layer presentationLayer] valueForKeyPath:#"transform.rotation.z"] floatValue];
//Rotate 180 degrees from current rotation
showAnimation.values = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:currentAngle],
[NSNumber numberWithFloat:currentAngle + (0.5 * M_PI)],
[NSNumber numberWithFloat:currentAngle + M_PI], nil];
[rotateMe.layer addAnimation:showAnimation forKey:#"show"];
On completion of the animation I then update the rotateMe.transform rotation to the layer's rotation so that it becomes usable.
- (void)animationDidStop:(CAKeyframeAnimation *)anim finished:(BOOL)flag
{
float currentAngle =[[[self.layer presentationLayer] valueForKeyPath:#"transform.rotation.z"] floatValue];
NSLog(#"End: %f", currentAngle);
rotateMe.transform = CGAffineTransformMakeRotation(0);
}
I have achieved the fully working effect with
[UIView animateWithDuration:1.0f]
animations:^{
CGAffineTransform transform = CGAffineTransformRotate(rotateMe.transform, DEGREES_TO_RADIANS(179.999f));
rotateMe.transform = transform;
}
];
But I'd like to make the animation more complex, hence the CAKeyframeAnimation.

You could configure the animation to be additive and animate from 0 to 180 degrees if you need all the key frames. If you don't need the different key frames the you can simply do it with a basic animation and the byValue property. The next time the animation is added it rotate 180 more degrees than whatever rotation is on the view.
If you are setting the actual value in the delegate callback then there is no need for fill mode and not removing the animation on completion.
CABasicAnimation *showAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
showAnimation.byValue = M_PI;
showAnimation.duration = self.showAnimationDuration;
showAnimation.delegate = self;
[rotateMe.layer addAnimation:showAnimation forKey:#"show"];
or using a key frame animation (as explained above: make it additive and animate from 0 to 180 degrees).
CAKeyframeAnimation *showAnimation = [CAKeyframeAnimation animationWithKeyPath:#"transform.rotation.z"];
showAnimation.additive = YES; // Make the values relative to the current value
showAnimation.values = #[0, /*all your intermediate values here... ,*/ M_PI];
showAnimation.duration = self.showAnimationDuration;
showAnimation.delegate = self;
[rotateMe.layer addAnimation:showAnimation forKey:#"show"];

Related

iOS 360 endless rotation

I've this code for endless rotating an UIImageView
[UIView animateWithDuration:0.3f
delay:0.0f
options:UIViewAnimationOptionCurveLinear|UIViewAnimationOptionRepeat
animations: ^{
self.spinner.transform = CGAffineTransformRotate(CGAffineTransformIdentity, -M_PI);
}
completion: ^(BOOL finished) {
}];
But the first I call this method the image, it rotate clockwise instead anti clockwise. If I re-call this method while image is rotating, it change direction and start rotate anti clockwise.
Ideas?
Use a CABasicAnimation instead since it is far more powerful. You have to call the below snippet only once and the animation will run indefinitely:
CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotate.toValue = #(M_PI * 2); // use #(-M_PI * 2) for counter clockwise
rotate.duration = 0.3;
rotate.cumulative = true;
rotate.repeatCount = HUGE_VALF;
[self.spinner.layer addAnimation:rotate forKey:#"rotateAnim"];
Swift:
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
rotate.toValue = M_PI * 2
rotate.duration = 0.3
rotate.cumulative = true
rotate.repeatCount = HUGE
self.spinner.layer.addAnimation(rotate, forKey: "rotateAnim")
From the documentation:
In iOS, a positive value specifies counterclockwise rotation and a
negative value specifies clockwise rotation.
CGAffineTransformRotate(CGAffineTransformIdentity, -M_PI)
Update: M_PI is constant not effected by the negative value. Somehow -M_PI is still taken as positive at the end!!!
If you give -180 it will rotate in clockwise, and if you give 180 it will rotate anti-clockwise

Rotating iOS UILayer like a ticking clock, not smoothly in a circle

I am able to rotate an image a certain number of degrees continuously, but I want to rotate the image a tiny amount, pause, a little more, pause, etc.
The follow code does this continuously:
// rotate
CGFloat finalValue = 360 / 14.f;
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
[rotationAnimation setFromValue:[self degreesToNumber:0]];
[rotationAnimation setToValue:[self degreesToNumber:finalValue]];
[rotationAnimation setDuration:5.0];
[rotationAnimation setRemovedOnCompletion:NO];
[rotationAnimation setFillMode:kCAFillModeForwards];
[self.secondHandImageView.layer addAnimation:rotationAnimation forKey:#"rotate"];
Is there a way to wrap this in a for-loop with the number of angle changes I need and set the duration and delay of the specific animations? Nothing I have tried works. Below is what I am currently trying:
// rotate in ticks, so
NSTimeInterval delay = 0;
CGFloat currentAngle = 0;
CGFloat finalAngle = 360 / 14.f;
// angle difference
CGFloat numberOfTicks = 25.f;
CGFloat angleDelta = finalAngle / numberOfTicks;
for (NSUInteger tick = 0; tick < numberOfTicks; tick++) {
[UIView animateWithDuration:0.1 delay:delay options:UIViewAnimationOptionCurveLinear animations:^{
self.secondHandImageView.layer.transform = CATransform3DMakeRotation(0, 0, angleDelta, 1.0);
} completion:nil];
// update delay
delay += .2; // 200 milliseconds, 5 tickets every second
currentAngle += angleDelta;
}
You code is OK - except that you can't animate layers inside UIView animation blocks. Instead you can animate the view. Replace the (3D)layer transform with a (2D)view transform:
self.secondHandImageView.transform =
CGAffineTransformRotate(self.tickView.transform,angleDelta);

Rotate a refresh button image smoothly so that it completes to the nearest turn iOS

I am trying to animate a refresh button so that it rotates indicating that the refresh is in progress. It needs to be smooth so that if the refresh only takes 0.1 seconds we still do a complete rotation so the user can acknowledge something happened and that its a smooth transition. It should also continue rotating until i stop it however stopping shouldn't abruptly stop it only tell it to complete the current turn.
Originally i did something like this
CABasicAnimation *rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat:M_PI * 2.0 * 10];
rotationAnimation.cumulative = YES;
rotationAnimation.duration = 10;
[self.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
And stopping like so
[self.layer removeAllAnimations];
This Worked fine in the sense that the animation continued past 2pi radians smoothly, however when the refresh took less than 1/10 of the second it wouldnt look very smooth as the animation would get get 10% of the way round and then suddenly stop and the removeAllAnimations method resets the image back to its default.
I managed to get around this an alternative stop method
CALayer *presentLayer = self.layer.presentationLayer;
float currentAngle = [(NSNumber *) [presentLayer valueForKeyPath:#"transform.rotation.z"] floatValue];
[self.layer removeAllAnimations];
if (currentAngle < 0) {
currentAngle = 2 * ABS(currentAngle);
}
float rotationProgressPercent = currentAngle / (2 * M_PI);
CABasicAnimation *rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.fromValue = [NSNumber numberWithFloat:currentAngle];
rotationAnimation.toValue = [NSNumber numberWithFloat:M_PI * 2];
rotationAnimation.cumulative = YES;
rotationAnimation.duration = 1 - rotationProgressPercent;
Basically I get the current angle of the rotation in radians, stop the animation and start a new animation from that position to two pi. I have to do some work with the duration to keep the speed constant, the speed aspect works fine but the problem is that somethings the animation has a very slight lag/twitch to it. I believe this is because the stop animation is asynchronously posting this request to the system (this is just speculation) and that my current angle is stale by the time i go to do my second animation.
Are there any other approaches i can try.
So i eventually found a solution, how this is useful
-(void)startSpinning {
if (animating) {
return;
}
animating = YES;
[self rotateViewWithDuration:1 byAngle:M_PI * 2];
}
- (void)stopSpinning {
animating = NO;
}
- (void)rotateViewWithDuration:(CFTimeInterval)duration byAngle:(CGFloat)angle {
[CATransaction begin];
CABasicAnimation *rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.byValue = [NSNumber numberWithFloat:angle];
rotationAnimation.duration = duration;
rotationAnimation.removedOnCompletion = YES;
[CATransaction setCompletionBlock:^{
if (animating) {
[self rotateViewWithDuration:duration byAngle:angle];
}
}];
[self.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
[CATransaction commit];
}

How to tell if an animation finished completely, or was interrupted?

I have a UIView and two UISwipeGesture classes that animate the X rotation between 0 and -99, giving the flip board effect. If the user swipes down and then immediately swipes back up, the 'swipe down' animation is ended prematurely and it begings the swipe up animation.
How can I tell if it gets ended prematurely due to another animation being added? The animationDidStop:finished message gets sent, but the finished value is always TRUE.
Here's swipe down code:
CATransform3D transform = CATransform3DIdentity;
// This must be set before we calculate the transforms to give the 3D perspective (1.0 / -DISTANCE_FROM_CAMERA)
transform.m34 = 1.0 / -4000;
// Rotate on the X axis
transform = CATransform3DRotate(transform, DegreesToRadians(-99), 1, 0, 0);
// Apply transform in an animation
CABasicAnimation* foldDownAnimatnion = [CABasicAnimation animationWithKeyPath:#"transform"];
foldDownAnimatnion.duration = 1;
foldDownAnimatnion.toValue = [NSValue valueWithCATransform3D:transform];
foldDownAnimatnion.removedOnCompletion = NO;
foldDownAnimatnion.fillMode = kCAFillModeForwards;
foldDownAnimatnion.delegate = self;
// Identify this animation in delegate method
[foldDownAnimatnion setValue:#"foldDown" forKey:#"name"];
[foldDownAnimatnion setValue:theLayer forKey:#"layer"];
[theLayer addAnimation:foldDownAnimatnion forKey:nil];
And my delegate method:
- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)finished
{
if([[animation valueForKey:#"name"] isEqualToString:#"foldDown"])
{
// Why is this always YES??
NSLog(#"Animation finished: %#", (finished)?#"Yes" : #"No");
}
else if([[animation valueForKey:#"name"] isEqualToString:#"foldUp"])
{
NSLog(#"animationDidStop: foldUp");
}
}
Add two BOOL instance variable(BOOL UpAnimationStarted; BOOL DownAnimationStarted;) In the beginning of SwipeUp set UpAnimationStarted to YES. And In the beginning of SwipeDown set DownAnimationStarted to YES. You can use these BOOL values to check animation interruption.
Thanks to #DavidRönnqvist for pointing me in the right direction, I went back to my O'Reilly book on iOS4 and redid the animation, the code now much cleaner:
Swipe Down:
CATransform3D transform = CATransform3DIdentity;
// This must be set before we calculate the transforms to give the 3D perspective (1.0 / -DISTANCE_FROM_CAMERA)
transform.m34 = 1.0 / -4000;
// Rotate on the X axis
transform = CATransform3DRotate(transform, DegreesToRadians(-99), 1, 0, 0);
// Apply transform in an animation
[CATransaction setDisableActions:YES];
theLayer.transform = transform;
CABasicAnimation* foldDownAnimatnion = [CABasicAnimation animationWithKeyPath:#"transform"];
foldDownAnimatnion.duration = 1;
foldDownAnimatnion.delegate = self;
[foldDownAnimatnion setValue:#"foldDown" forKey:#"name"];
[foldDownAnimatnion setValue:theLayer forKey:#"layer"];
[theLayer addAnimation:foldDownAnimatnion forKey:#"foldDown"];
Swipe Up
[theLayer removeAnimationForKey:#"foldDown"];
CATransform3D transform = CATransform3DIdentity;
// This must be set before we calculate the transforms to give the 3D perspective (1.0 / -DISTANCE_FROM_CAMERA)
transform.m34 = 1.0 / -4000;
// Rotate on the X axis
transform = CATransform3DRotate(transform, DegreesToRadians(0), 1, 0, 0);
// Apply transform in an animation
[CATransaction setDisableActions:YES];
theLayer.transform = transform;
CABasicAnimation* animatnion = [CABasicAnimation animationWithKeyPath:#"transform"];
animatnion.duration = 1;
animatnion.delegate = self;
[animatnion setValue:#"foldUp" forKey:#"name"];
[theLayer addAnimation:animatnion forKey:#"foldUp"];
The key really is that I am applying the new transform to the layer, and then animating it. What I was doing before was applying the animation to the layer and making it fill forwards, this caused problems when I removed it mid-way.

CABasicAnimation returns to the original position before the next animation

I want to implement a method to rotate a UIButton based on user inputs. Upon the first input, it should rotate 45 degrees to the left. Upon the second input, it should rotate for another 45degrees from the position it stopped after the first rotation.
But the button goes back to its very original position before starting the 2nd animation. Following is the method I use.
- (void)spinLayer:(CALayer *)inLayer duration:(CFTimeInterval)inDuration
direction:(int)direction
{
CABasicAnimation* rotationAnimation;
// Rotate about the z axis
rotationAnimation =
[CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
// Rotate 360 degress, in direction specified
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 1/2 * direction];
// Perform the rotation over this many seconds
rotationAnimation.duration = inDuration;
rotationAnimation.removedOnCompletion = NO;
rotationAnimation.fillMode = kCAFillModeForwards;
// Set the pacing of the animation
rotationAnimation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
// Add animation to the layer and make it so
[inLayer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
}

Resources