CATransaction rotate layer 360 degree strange - ios

I want to perform an infinite 360 degrees rotation animation, so I coded:
- (void)rotate
{
__weak typeof(self) weakSelf = self;
[CATransaction begin];
[CATransaction setDisableActions:NO];
[CATransaction setCompletionBlock:^{
[weakSelf rotate];
}];
[CATransaction setAnimationDuration:1.0];
[CATransaction setAnimationTimingFunction:
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
self.squareLayer.transform = CATransform3DRotate(self.squareLayer.transform, M_PI, 0, 0, 1);
[CATransaction commit];
}
M_PI means 180 degrees, so I believe the layer can be rotated from 0 to M_PI * 1, M_PI * 2 ...
But it turns out to be an rotation from 0 to M_PI, then M_PI to 0.
I can make it through a CABasicAnimation:
CABasicAnimation *animatin = [CABasicAnimation animationWithKeyPath:#"transform"];
animatin.duration = 1.0;
animatin.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(0, 0, 0, 1)];
animatin.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(M_PI, 0, 0, 1)];
animatin.repeatCount = HUGE_VALF;
animatin.fillMode = kCAFillModeForwards;
[self.squareLayer addAnimation:animatin forKey:#"a"];
Codes above rotate the layer 360 degrees perfectly.
But I am really confused about the first implementation, why it rotates so strange?

It's an edge case. With implicit animation, you are not able to say "rotate clockwise this amount". You are merely saying "set the transform to this value, and animate that effect." Well, how should that change be animated? The runtime has to make up an answer, and it isn't obvious how to do that. A transform of rotation by M_PI is the same "distance" in both directions, so the answer it makes up is arbitrary. You can see the problem even more clearly if you supply a value of M_PI+0.1 vs. a value of M_PI-0.1; you actually get reverse results: one keeps turning clockwise, the other keeps turning counterclockwise.
With CABasicAnimation, on the other hand, you have a from-value (implicit or explicit) and a to-value, so you are able to express the notion "increase" (i.e. a positive difference means turn clockwise). It's even clearer if you animate by setting just the byValue.

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

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.

CAKeyframeAnimation repeating rotation from last point

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"];

Mimic UIAlertView Bounce?

Whats the best way to mimic the bouncing animation from the UIAlertView on the iPhone? Is there some built-in mechanism for this? The UIAlertView itself won't work for my needs.
I looked into animation curves but from what I can tell the only ones they provide are easeIn, easeOut, and linear.
UIAlertView uses a more sophisticated animation:
scale to larger than 100%
scale to smaller than 100%
scale to 100%
Here's an implementation using a CAKeyFrameAnimation:
view.alpha = 0;
[UIView animateWithDuration:0.1 animations:^{view.alpha = 1.0;}];
CAKeyframeAnimation *bounceAnimation = [CAKeyframeAnimation animationWithKeyPath:#"transform.scale"];
bounceAnimation.values = #[#0.01f, #1.1f, #0.8f, #1.0f];
bounceAnimation.keyTimes = #[#0.0f, #0.5f, #0.75f, #1.0f];
bounceAnimation.duration = 0.4;
[view.layer addAnimation:bounceAnimation forKey:#"bounce"];
I investigated how animations are added to UIAlertView's layer by swizzling -[CALayer addAnimation:forKey:]. Here are the values I got for the scale transform animations it performs:
0.01f -> 1.10f -> 0.90f -> 1.00f
with durations
0.2s, 0.1s, 0.1s.
All the animations use an ease in/ease out timing function. Here is a CAKeyframeAnimation that encapsulates this logic:
CAKeyframeAnimation *bounceAnimation = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
bounceAnimation.fillMode = kCAFillModeBoth;
bounceAnimation.removedOnCompletion = YES;
bounceAnimation.duration = 0.4;
bounceAnimation.values = #[
[NSValue valueWithCATransform3D:CATransform3DMakeScale(0.01f, 0.01f, 0.01f)],
[NSValue valueWithCATransform3D:CATransform3DMakeScale(1.1f, 1.1f, 1.1f)],
[NSValue valueWithCATransform3D:CATransform3DMakeScale(0.9f, 0.9f, 0.9f)],
[NSValue valueWithCATransform3D:CATransform3DIdentity]];
bounceAnimation.keyTimes = #[#0.0f, #0.5f, #0.75f, #1.0f];
bounceAnimation.timingFunctions = #[
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
I believe UIAlertView also performs a simple opacity animation from 0.0f to 1.0f over the total duration of the transform animation (0.4).
You can use 2 animations, one to pop up to very large, and the other one to rescale back to normal size.
(This is the approach use by UIAlertView internally.)
Alternatively, you can use the lower-level CAAnimation and use +[CAMediaTimingFunction functionWithControlPoints::::] to make your own curve.
Here's how I did it for an app I'm working on. The effect I was going for was bouncing when you pressed the view. Experiment with the values to suit your taste and the desired speed of the effect.
- (void) bounceView:(UIView*)bouncer
{
// set duration to whatever you want
float duration = 1.25;
// use a consistent frame rate for smooth animation.
// experiment to your taste
float numSteps = 15 * duration;
// scale the image up and down, halving the distance each time
[UIView animateKeyframesWithDuration:duration
delay:0
options:UIViewKeyframeAnimationOptionCalculationModeCubic
animations:^{
float minScale = 0.50f; // minimum amount of shrink
float maxScale = 1.75f; // maximum amount of grow
for(int i = 0; i< numSteps*2; i+=2)
{
// bounce down
[UIView addKeyframeWithRelativeStartTime:duration/numSteps * i
relativeDuration:duration/numSteps
animations:^{
bouncer.layer.transform = CATransform3DMakeScale(minScale, minScale, 1);
}];
// bounce up
[UIView addKeyframeWithRelativeStartTime:duration/numSteps * (i+1)
relativeDuration:duration/numSteps
animations:^{
bouncer.layer.transform = CATransform3DMakeScale(maxScale, maxScale, 1);
}];
// cut min scale halfway to identity
minScale = minScale + (1.0f - minScale) / 2.0f;
// cut max scale halfway to identity
maxScale = 1.0f + (maxScale - 1.0f) / 2.0f;
}
} completion:^(BOOL finished) {
// quickly smooth out any rounding errors
[UIView animateWithDuration:0.5*duration/numSteps animations:^{
bouncer.layer.transform = CATransform3DIdentity;
}];
}];
}

Resources