Need help with animation on iPhone - iphone

I'm working on an animated clock application for the iPhone, and I have to pivot all the 3 nodes in the view, which I have obtained in the following code:
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
clockarm.layer.anchorPoint = CGPointMake(0.0, 0.0);
[CATransaction commit];
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanFalse forKey:kCATransactionDisableActions];
[CATransaction setValue:[NSNumber numberWithFloat:50.0] forKey:kCATransactionAnimationDuration];
CABasicAnimation *animation;
animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.fromValue = [NSNumber numberWithFloat:-60.0];
animation.toValue = [NSNumber numberWithFloat:2 * M_PI];
animation.timingFunction = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionLinear];
animation.delegate = self;
[clockarm.layer addAnimation:animation forKey:#"rotationAnimation"];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[CATransaction commit];
The problem it's just rotating once, ie. only 360 degree and then stopping. I want to raotate the needles indefinitely. How would I do that?

I've never done it, but here's what I would try. You've already set the delegate to self. Implement the delegate's method animationDidStop:finished: and simply call the method where you'll put the code above. Make sure to call it on the main thread (using performOnMainThread).

This is related to the question asked here, where I pointed out my response to a similar question. In that case, I split the rotation animations into half-circle rotations, made them cumulative, and set the number of half rotations as the repeat count. As I suggest in both, doing this as a CAKeyframeAnimation might produce a cleaner rotation.
In your case, you could set the number of repetitions to a high enough value that the animation would never reach its end. As a fallback, you could follow François P.'s and Squeegy's suggestion and add a callback to restart the animation if it ever completes.

Add the animationDidFinish delegate method and setup the animation again for another rotation. CA wont let you turn things more than 360 degrees.

I would look at the problem slightly differently. I'm not sure why you're using 50 seconds for the time, but I wouldn't rely on this to keep accurate time. I would instead set a timer which fires every second, or half second, and animates the clock to move from it's current position to the new time.
P.S. Your fromValue looks to be in degrees, but it should be radians.

I use "scalingAnimation.repeatCount=1000000000;" for the purpose. Not sure if it's the way for you.

for n no. of repetition try to use value -1, i never do this but may it will help u.

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

Non-Smooth animation with Core Animation

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.

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

How to coordinate animation times on multiple CALayers?

Hi I have two CALayers (*layer1 and *layer2). What I want is to start the animation on layer2 when 50% of the layer1's animation time has elapsed. How can I achieve that?
I tried using CAAnimationGroup but that works only for animations in the same layer. I have found a workaround in which you have to add the delayed animation to a group first and set the beginTime property but this seems a little hackish to me and I would like to know if there is a right way of achieving what I want.
Thank you!
The method of adding the second animation to a group does feel like a bit of a hack, but it works and is perfectly acceptable. What you could do instead if you prefer, though, is call -performSelector:withObject:afterDelay. Something like this:
- (void)startFirstAnimation;
{
CGFloat duration = 10.0;
CABasicAnimation *firstAnim =
[CABasicAnimation animationWithKeyPath:#"position"];
[firstAnim setFromValue:
[NSValue valueWithCGPoint:CGPointMake(30.0f, 30.0f)]];
[firstAnim setToValue:
[NSValue valueWithCGPoint:CGPointMake(200.0f, 200.0f)]];
[firstAnim setDuration:duration];
[firstLayer addAnimation:firstAnim forKey:nil];
[self performSelector:#selector(startSecondAnimation)
withObject:nil afterDelay:duration/2.0];
}
- (void)startSecondAnimation;
{
CABasicAnimation *secondAnim =
[CABasicAnimation animationWithKeyPath:#"position"];
[secondAnim setFromValue:
[NSValue valueWithCGPoint:CGPointMake(100.0f, 30.0f)]];
[secondAnim setToValue:
[NSValue valueWithCGPoint:CGPointMake(200.0f, 200.0f)]];
[secondAnim setDuration:5.0];
[secondLayer addAnimation:secondAnim forKey:nil];
}