Non-Smooth animation with Core Animation - iphone

This is a weird request, but I'm using Core Animation (CALayers), and I want my animation to be choppy and non-smooth. I want an image I set up to rotate like a second hand on a clock. Here's my code:
UIImage *arrowImage = [UIImage imageNamed:#"arrow.jpg"];
CALayer *arrow = [CALayer layer];
arrow.contents = (id)arrowImage.CGImage;
arrow.bounds = CGRectMake(0, 0, 169.25, 45.25);
arrow.position = CGPointMake(self.view.bounds.size.width / 2, arrowImage.size.height / 2);
arrow.anchorPoint = CGPointMake(0.0, 0.5);
[self.view.layer addSublayer:arrow];
CABasicAnimation *anim1 = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
anim1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
anim1.fromValue = [NSNumber numberWithFloat:0];
anim1.toValue = [NSNumber numberWithFloat:((360*M_PI)/180)];
anim1.duration = 4.0;
[arrow addAnimation:anim1 forKey:#"transform"];
It produces a gliding motion, which I don't want. How do I get around this?
Any help is appreciated.

If you want it to be really choppy, don't use Core Animation at all. On the other hand, if you want something somewhere in between those two extremes, don't use linear media timing. Instead, you might want to try kCAMediaTimingFunctionEaseIn so that the animation accelerates slightly as the hand moves.

The simple way to do this would be to simply apply a transform to your view. The second hand would snap from one position to the next. Just change the rotation by 360/60 = 6 degrees for each second.
If you want the second-hand to do an animation for each tick, you could use a very fast UIView block-based animation. (say with a 1/15 second duration or so.)
Take a look at the UIView class methods who's names start with animateWithDuration.
Something like this:
- (void) moveSecondHand;
{
seconds++;
angle = M_PI*2*seconds/60 - M_PI/2;
CGAffineTransform transform = CGAffineTransformMakeRotation(angle);
[UIView animateWithDuration: 1.0/15
animations: *{
secondHand.transform = transform
}];
}
That's about all it would take. You're trigger that code with a timer once a second. By default animations use ease-in, ease-out timing, which models physical movement pretty well. Try different durations, but 1/15 is probably a good starting point (you want it fast, but not too fast to see.)
If you want a wobble to your animation you will need to get much fancier, and create an animation group that first moves it by the full amount, and then does a repeating animation that overshoots the stopping point by a small amount and then goes back.

Related

iOS "CABasicAnimation" brings component to the original position after animation complete

I'm animating UIImageView using iOS core animation "CABasicAnimation", all works fine for me but the only problem is when i animate to the position, after completeing animation it comes back to original position where it was. how can i overcome this? i need to keep UIImageView in moved position.
NOTE : I've seen few questions with success answers regarding this, but i have no idea why mine is not working like they say.
After rotating a CALayer using CABasicAnimation the layer jumps back to it's unrotated position
Here is my sample code,
CGPoint endPt = CGPointMake(160, 53);
CABasicAnimation *anim5 = [CABasicAnimation animationWithKeyPath:#"position"];
[anim5 setBeginTime:CACurrentMediaTime()+0.4];
anim5.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
anim5.fromValue = [NSValue valueWithCGPoint:imgRef.layer.position];
anim5.toValue = [NSValue valueWithCGPoint:endPt];
anim5.duration = 1.5;
anim5.speed = 2;
[imgRef.layer addAnimation:anim5 forKey:#"position"];
//this is what other answers ask to do
[anim5 setFillMode:kCAFillModeForwards];
[anim5 setRemovedOnCompletion:NO];
BTW [imgRef.layer setPosition:CGPointMake(160, 53)]; won't help me since i'm delaying animation with 4 milliseconds.
The root cause is that the animation just transitions the property between two values, it doesn't actually change the ending value. You need to change the ending value when the animation completes, there are three ways to do that.
1) Use the delegate property on the CAAnimation superclass to be notified of when the animation completes. At that point you can set the property to it's end value. See: https://developer.apple.com/library/mac/#documentation/GraphicsImaging/Reference/CAAnimation_class/Introduction/Introduction.html#//apple_ref/occ/cl/CAAnimation The animationDidStop:finished: is the method you'll need to implement on the delegate.
2) Set a completion block on the surrounding CATransaction. You'll need to manually start the CATransaction rather than having CABasicAnimation do that automatically for you. See: Objective-C - CABasicAnimation applying changes after animation?
3) See OMZ's comment below...
The right answer is to set the layer's position property, but as you've pointed out, it makes it more difficult because you're wanting a 0.4 second delay prior to the position change. Is there any reason you couldn't perform the delay first and then do the animation? Something like this:
- (IBAction)didTapMove:(id)sender
{
[self performSelector:#selector(animate) withObject:nil afterDelay:0.4];
}
- (void)animate
{
CGPoint endPt = CGPointMake(160, 53);
CABasicAnimation *anim5 = [CABasicAnimation animationWithKeyPath:#"position"];
anim5.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
anim5.fromValue = [NSValue valueWithCGPoint:_imageView.layer.position];
anim5.toValue = [NSValue valueWithCGPoint:endPt];
anim5.duration = 1.5;
anim5.speed = 2;
[_imageView.layer addAnimation:anim5 forKey:#"position"];
[_imageView.layer setPosition:CGPointMake(160, 53)];
}
Noticed I've removed your begin time from the animation since the delay is occurring in the perform selector call.

Stop CABasicAnimation at specific point

I'm using a rotation animation created with CABasicAnimation. It rotates a UIView over 2 seconds. But I need to be able to stop it when the UIView is touched. If I remove the animation the view is in the same position as before the animation started.
Here's my animation code:
float duration = 2.0;
float rotationAngle = rotationDirection * ang * speed * duration;
//rotationAngle = 3*(2*M_PI);//(double)rotationAngle % (double)(2*M_PI) ;
CABasicAnimation* rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat: rotationAngle ];
rotationAnimation.duration = duration;
rotationAnimation.cumulative = YES;
rotationAnimation.removedOnCompletion = NO;
rotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
rotationAnimation.fillMode = kCAFillModeForwards;
rotationAnimation.delegate = self;
[self.view.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
How can I stop the UIView's rotation right where it is, when it's touched? I know how to manage the touch part, but I can't figure out how to stop the view at the animation's current angle.
Solution:
I solved the problem by getting the angle of the presentation layer, removing the animation and setting the view's transform. Here's the code:
[self.view.layer removeAllAnimations];
CALayer* presentLayer = self.view.layer.presentationLayer;
float currentAngle = [(NSNumber *)[presentLayer valueForKeyPath:#"transform.rotation.z"] floatValue];
self.view.transform = CGAffineTransformMakeRotation(currentAngle);
Good question! For this, it's helpful to know the Core Animation architecture.
If you check out the diagram in the Core Animation Programming Guide that describes the Core Animation Rendering Architecture, you can see that there's three trees.
You have the model tree. That's where you set the values of what you want to happen. Then there's the presentation tree. That's what is pretty much happening as far as the runtime is concerned. Then, finally is the render tree. That's what the user sees.
In your case, you want to query the values of the presentation tree.
It's easy to do. For the view that you have attached the animation, get the layer and for that layer, query the presentationLayer's values. For example:
CATransform3D myTransform = [(CALayer*)[self.view.layer presentationLayer] transform];
There's no way to "pause" an animation mid flow. All you can do is query the values, remove it, and then re-create it again from where you left off.
It's a bit of a pain!
Have a look at some of my other posts where I go into this in a bit more detail, e.g.
Restoring animation where it left off when app resumes from background
Don't forget also that when you add an animation to a view's layer, you aren't actually changing the underlying view's properties. So what happens? We'll you get weird effects where the animation stops and you see the view in it's original position.
That's where you need to use the CAAnimation delegates. Have a look at my answer to this post where I cover this:
CABasicAnimation rotate returns to original position
You need to set the rotation to the rotation of the presentationLayer and then remove the animation from the layer. You can read about the presentation layer in my blog post about Hit testing animating layers.
The code to set the final rotation would be something like:
self.view.layer.transform = [(CALayer*)[self.view.layer presentationLayer] transform];
[self.view.layer removeAnimationForKey:#"rotationAnimation"];

smooth animation in iOS using CoreAnimation

CoreAnimation is a pretty easy thing, but:
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:30];
MyImageView.frame = CGRectOffset(MyImageView.frame, 100, 0);
[UIView commitAnimations];
I want to move the ImageView by 100 Pixel veeeery slowly. Therefore all positioning values are double I expect the Layoutsystem to position the items with subpixel accuracy.
Butt when I watch this animation i see the ImageView "jumping" pixelwise instead of a smooth traveling.
Any ideas to come to a real subpixelpositioning?
I also tried to set the position with a timer and recalculate the frame-values, but same effect.
Update:
In an other part of my App I use the Accelerometer to update the position of a ImageView, and do basicly calculate the position ad size of the graphic an then do:
MyImageView.frame = newCGRect;
I get around 60 Updates/s from the Accelerometer and added the LowPass-Filter from the Accelerometer example from Apple.
Here the positioning is perfect?!?!
Why does this do not happen with CoreAnimation?
Thanks for any help.
Try using CGAffineTransformTranslate(MyImageView.transform, 100, 0) instead of CGRectOffset.
Reference here:
http://developer.apple.com/library/mac/#documentation/GraphicsImaging/Reference/CGAffineTransform/Reference/reference.html
If you use CABasicAnimation in QuartzCore framework, you can smoothen your animation using "CAMediaTimingFunction". Built-in alternatives worked for me but as far as I know you can define your own timing functions as well.
CABasicAnimation *starShineAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
starShineAnimation.removedOnCompletion = NO;
starShineAnimation.fillMode = kCAFillModeForwards;
starShineAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
....

combining animations on single property

How can I use two separate CABasicAnimations to simultaneously animate the same property?
For example Two CABasicAnimations, both animate position.y.
The first animation will be a bounce (from 100, to 300, duration = 1, autoreverse = yes, repeatcount = 10)
The second animation will be a slow scroll (by 100, duration = 10)
The behavior I am seeing is that if the first animation is in progress & I use:
[pickle.layer addAnimation:[self makescrollanimation] forKey:#"scrollit"];
to add the second... the second is ignored.
I know the second animation works, because if i start with the second one, then the first is ignored.
Thank you-
Matt
If I understand you correctly, you want the view to bounce up and down while the whole bouncing motion shifts slowly downwards. You can do this by making the animations additive using their additive property. For example:
CABasicAnimation *bounceAnimation = [CABasicAnimation animationWithKeyPath:#"position.y"];
bounceAnimation.duration = 1;
bounceAnimation.fromValue = [NSNumber numberWithInt:100];
bounceAnimation.toValue = [NSNumber numberWithInt:300];
bounceAnimation.repeatCount = 10;
bounceAnimation.autoreverses = YES;
bounceAnimation.fillMode = kCAFillModeForwards;
bounceAnimation.removedOnCompletion = NO;
bounceAnimation.additive = YES;
[view.layer addAnimation:bounceAnimation forKey:#"bounceAnimation"];
CABasicAnimation *scrollAnimation = [CABasicAnimation animationWithKeyPath:#"position.y"];
scrollAnimation.duration = 10;
scrollAnimation.fromValue = [NSNumber numberWithInt:0];
scrollAnimation.toValue = [NSNumber numberWithInt:100];
scrollAnimation.fillMode = kCAFillModeForwards;
scrollAnimation.removedOnCompletion = NO;
scrollAnimation.additive = YES;
[view.layer addAnimation:scrollAnimation forKey:#"scrollAnimation"];
should accomplish the animation you desire, if I'm reading your question correctly. The scroll animation should be able to be triggered at any point during the bounce animation.
I wrote up a quick summary about how to use additive animation
In short, in a transaction set the new model value, then you animate from the old model value minus the new model value, and the destination is NSZeroPoint, NSZeroRect, or the identity transform. You get beautiful curves instead of a jolt from changing an animation in-flight.
You can use CAAnimationGroup if you want to bundle multiple animations across different properties, but I'm not sure this is possible using two CABasicAnimations. You can only apply one property animation per property to a view at a time.
One way I could think of accomplishing this would be to nest your view in another UIView, and perform the 'scrollit' animation on the enclosing view, while continuing to bounce the enclosed view.
Love to know a better answer!

how can I use animation in cocos2d?

I am trying to develop a Roulette game for iPhone. How can I animation (spin) the Roulette board?
It's quite simple in Cocos2D:
[sprite runAction:[RotateBy actionWithDuration:dur angle:360]];
to make one turn or
id action = [RepeatForever actionWithAction: [RotateBy actionWithDuration:dur angle:360]];
[sprite runAction:action];
and
[sprite stopAction:action];
if you need to rotate continuously.
Don't forget to make sure that sprite transformAnchor is set to center of the image. And I guess next question should arise - how to make it stop smoothly ;)
More on actions: http://lethain.com/entry/2008/oct/03/notes-on-cocos2d-iphone-development (it's for older version, so it uses deprecated 'do' instead of 'runAction')
I have no idea how to do this in cocos2d (or even what that is), but you can do this using Core Animation either with CALayers or UIViews. Probably the simplest way would be to create a UIImageView containing an image of your roulette wheel and animate that.
To accomplish this, first set up your UIImageView by initializing it with your roulette wheel image. When you want the wheel to spin, use the following code:
CATransform3D rotationTransform = CATransform3DMakeRotation(1.0f * M_PI, 0, 0, 1.0);
CABasicAnimation* rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
rotationAnimation.toValue = [NSValue valueWithCATransform3D:rotationTransform];
rotationAnimation.duration = 0.25f;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = 10;
[rotatingImage.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
assuming that rotatingImage is your UIImageView.
In this example, the wheel would rotate 5 times, with each rotation taking 0.5 seconds. The rotations are split in half because Core Animation will go to the next closest state, so the most you can rotate something is a half rotation before the animation wants to rotate in the other direction. That is, the pi radian (180 degree) rotation here goes a half-circle, but if you used (1.5f * pi) for your rotation angle, it would only go a quarter-circle. Likewise, if you used (0.999f * pi), the circle would rotate in a clockwise manner.
You'll want to implement acceleration and deceleration of your wheel, and for those a CAKeyframeAnimation would take the place of the CABasicAnimation in this example.
You can rotate a view, by some number of radians, regardless of whether it is less than a full rotation or many multiples of a full rotation, without having to split the rotation into pieces. As an example, the following code will spin a view, once per second, for a specified number of seconds. You can easily modify it to spin a view by a certain number of rotations, or by some number of radians.
- (void) runSpinAnimationWithDuration:(CGFloat) duration;
{
CABasicAnimation* rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0 /* full rotation*/ * rotations * duration ];
rotationAnimation.duration = duration;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = 1.0;
rotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
[myView.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
}
There are a couple of ways you can do it.
If you have the frame-by-frame animation for the wheel, check out AtlasDemo (part of cocos2d distribution).
Otherwise, take a look at Sprite's RotateBy: method.