So i am using Core Graphics to rotate a view
-(void)viewDidLoad
{
[super viewDidLoad];
timer = 0.0f;
// Do any additional setup after loading the view, typically from a nib.
[NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:#selector(callEverySecond) userInfo:nil repeats:YES];
}
-(void)callEverySecond
{
if (timer>360) {
timer=0.0f;
[self rotateHand:timer];
}
else
{
timer++;
[self rotateHand:timer];
}
}
-(void)rotateHand:(CGFloat)angle
{
[UIView animateWithDuration:0.01 animations:^{
CGAffineTransform matrix = CGAffineTransformMakeRotation(angle*M_PI/180);
[[self.rotate layer]setAffineTransform:matrix];
}];
[[self.rotate layer]needsDisplay];
}
Now the problem is that the rotation speed is non uniform and slows downs and speeds up randomly.
What is wrong here.
A timer with interval 0.01 wants to fire 100 times per second. iOS only updates the screen 60 times per second.
Instead of using an NSTimer to “manually” perform animation, use a CADisplayLink. A display link fires once per screen update (by default) and fires in sync with screen updates.
Also, the interval between screen updates is 1/60 = 0.01666667 seconds. Therefore your UIView animation duration is less than the time between screen updates, so it can't produce any visible animation. Get rid of your UIView animation.
If you just want to make a layer rotate continuously, you don't have to use an NSTimer or a CADisplayLink. Instead, use a CABasicAnimation on the transform.rotation property:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
animation.repeatCount = HUGE_VALF;
animation.duration = 3.6;
animation.fromValue = #0.0;
animation.toValue = #(2 * M_PI);
[self.rotate.layer addAnimation:animation forKey:animation.keyPath];
NSTimer is not meant to have very good precision. Besides, calling it 100 times/second is overkill for animation purposes. If you want a smooth rotation with a turn every second, use a repeating CAAnimation instead.
Related
If I need simply to pause/resume rotation then I use the following code:
- (void)pauseAnimations
{
CFTimeInterval paused_time = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil];
self.layer.speed = 0.0;
self.layer.timeOffset = paused_time;
}
- (void)resumeAnimations
{
CFTimeInterval paused_time = [self.layer timeOffset];
self.layer.speed = 1.0f;
self.layer.timeOffset = 0.0f;
self.layer.beginTime = 0.0f;
CFTimeInterval time_since_pause = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil] - paused_time;
self.layer.beginTime = time_since_pause;
}
But I want to stop the current layer animation before rotation and resume after it. And this code stops the rotation process too.
How to solve this issue?
You can't pause a layer's animation's all but one by setting the layer's speed to 0.
You might be able to fetch the active animations from the layer and pause the individual animations using the technique you are using to pause animations on the whole layer (setting the speed to 0).
Note that you can't save a pointer to the animation objects that you create because the system actually adds a copy of the animation to the layer, not the original animation. You would probably need to fetch the animation using the CALayer animationForKey method.
Disclaimer: I have never tried this. My answer is based on general knowledge of how Core Animation works.
I have an animation that animates a layer along a curved path and then back. I would like the layer to pause before the auto-reversing kicks in. Is there an easy way to do this with Core Animation?
Here is the code I'm using to start the animation.
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
animation.duration = animationDuration;
animation.path = curvedPath;
animation.rotationMode = kCAAnimationRotateAuto;
animation.delegate = nil;
animation.autoreverses = YES;
[self.myView.layer addAnimation:animation forKey:#"cardAnimation"];
The only way I can think of to pause in an auto-reversing animation is to start a timer for 1/2 the animation time.
When the timer fires, set the speed of your animation to 0, and launch another timer. When the second timer fires, set the speed of your animation back to 1 (or whatever it was before) again. That should look good if you're using ease-in, ease-out animation timing, since the animation slows to a stop before reversing.
Failing that, you'd probably have to create separate forward and reverse animations, set a delegate for the animation, and in the completion method, pause and then start the reverse animation.
What I want to do is call a method after a delay for a game which will reload a weapon, but if I do that and the user pauses the game that selector isn't getting paused so the user can cheat by pausing.
So Im wondering if there is a way to pause it and then continue.
I know how to cancel the selector with this: cancelPreviousPerformRequestsWithTarget: but I want it to be able to continue after the user resumes the game.
Is this the right approach or should I consider another strategy?
This is absolutely not a good way to solve this problem, but it did the trick for me.
I've got a scaling animation, where a view grows to scale 6.0, waits for 2 seconds and then scales back to 1.0.
The wait for 2 seconds is my part where I used the performSelector:withObject:afterDelay: which I couldn't pause.
This is the pause part:
[UIView animateWithDuration:0 delay:2.0 options:0 animations:^{
self.transform = CGAffineTransformMakeScale(6.0 + 0.001, 6.0 + 0.001);
} completion:^(BOOL finished) {
[self animateOutWithCompletion:completion];
}];
Make sure you are animation a change, thats why I add 0.001 (which you wont see, as it's a that little change).
UIView animations can be paused using this category:
#interface UIView (AnimationsHandler)
- (void)pauseAnimations;
- (void)resumeAnimations;
#end
#implementation UIView (AnimationsHandler)
- (void)pauseAnimations
{
CFTimeInterval paused_time = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil];
self.layer.speed = 0.0;
self.layer.timeOffset = paused_time;
}
- (void)resumeAnimations
{
CFTimeInterval paused_time = [self.layer timeOffset];
self.layer.speed = 1.0f;
self.layer.timeOffset = 0.0f;
self.layer.beginTime = 0.0f;
CFTimeInterval time_since_pause = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil] - paused_time;
self.layer.beginTime = time_since_pause;
}
Feel free to use this code, but as I said before, this is not a good way to solve this problem. It's just making use of the way UIView animations work and the possibility to pause these animations.
I'm currently animating between 4 images like this:
UIImageView *tom3BeforeImage;
tom3Images = [NSArray arrayWithObjects: [UIImage imageNamed:#"floortom_before1.png"],[UIImage imageNamed:#"floortom_before2.png"],[UIImage imageNamed:#"floortom_before3.png"],[UIImage imageNamed:#"floortom_before4.png"], nil ];
tom3BeforeImage.animationImages = tom3Images;
tom3BeforeImage.animationDuration = 0.75;
[tom3BeforeImage startAnimating];
It works fine, except that the animation is choppy between the images. I need the duration to be exactly .75 seconds, so speeding it up is not an option.
What's the best way to have the animation be smoother between the images, kind of like blending between each image change?
Thanks!
If you're using frame based UIImageView animation, and the animation must be .75 seconds, then the only way I know of to make it smoother is to create more frames. Try 30 frames/second, or about 22 frames. That should give very smooth motion.
If you want some sort of cross-dissolve between frames then you won't be able to use UIView frame animation. you'll have to use UIView block animation (using animateWithDuration:animations: or its cousins.)
You could create a sequence of cross-dissolves between your frames where the total duration of the sequence is .75 seconds. Have each transition trigger the next transition in it's completion block.
Something like this:
You'll need 2 image views, stacked on top of each other. You'll fade one out and the other in at the same time. You'll need to set the opaque flag to NO on both.
Lets call them tom3BeforeImage1 and tom3BeforeImage2
Add an int instance variable imageCount and make your array of images, tom3Images, and instance variable as well:
- (void) animateImages;
{
CGFloat duration = .75 / ([tom3Images count] -1);
//Start with the current image fully visible in tom3BeforeImage1
tom3BeforeImage1.image = tom3Images[imageCount];
tom3BeforeImage1.alpha = 1.0;
//Get the next image ready, at alpha 0, in tom3BeforeImage2
tom3BeforeImage2.image = tom3Images[imageCount+1];
tom3BeforeImage2.alpha = 0;
imageCount++
[UIView animateWithDuration: duration
delay: 0
options: UIViewAnimationOptionCurveLinear
animations:
^{
//Fade out the current image
tom3BeforeImage1.alpha = 0.0;
//Fade in the new image
tom3BeforeImage2.alpha = 1.0;
}
completion:
^{
//When the current animation step completes, trigger the method again.
if (imageCount < [tom3Images count] -1)
[self animateImages];
}
];
}
Note that I banged out the code above in the forum editor without having had enough coffee. It likely contains syntax errors, and may even have logic problems. This is just to get you thinking about how to do it.
Edit #2:
I'm not sure why, but I decided to flesh this out into a full-blown example project. The code above works passably well after debugging, but since it's fading one image out at the same time it's fading another one in, the background behind both image views shows through.
I reworked it to have logic that only fades the top image in and out. It puts the first frame in the top image view and the second frame in the bottom image view, then fades out the top image view.
The project is up on github, called Animate-Img. (link)
Then it installs the third frame in the top image view and fades it IN,
Then it installs the 4th fame in the bottom image view and fades out the top to expose the bottom, etc, etc.
I ended up creating a generalized method
- (void) animateImagesWithDuration: (CGFloat) totalDuration
reverse: (BOOL) reverse
crossfade: (BOOL) doCrossfade
withCompletionBlock: (void (^)(void)) completionBlock;
It will animate a set of images, into a pair of image views, optionally reversing the animation once it's done. It takes a completion block that gets called once the animation is finished.
The animate button actually calls a method that repeats the whole animation sequence. It's currently set to only run it once, but changing a constant will make the program repeat the whole sequence, if desired.
I do had the requirement to have animation with array of images. Initially when i used animationImages property of imageview, I got the desired animation but the transition between the images were not smooth, I then used CAKeyframeAnimation to achieve the smooth transition, the catch is to use timingFunction along with correct calculationMode. I am not sure this is the exact the answer for the question but this is one way to make the animation smoother,
below is the code for that
For more info on calculation mode please see Apple Documentation
- (void) animate
{
NSMutableArray * imageArray = [[NSMutableArray alloc] init];
int imageCount = 8;
for (int i=0; i<=imageCount; i++) {
[imageArray addObject:(id)[UIImage imageNamed:[NSString stringWithFormat:#“image%d”,i]].CGImage];
}
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"contents"];
animation.calculationMode = kCAAnimationLinear;// Make sure this is kCAAnimationLinear or kCAAnimationCubic other mode doesnt consider the timing function
animation.duration = 8.0;
animation.values = imageArray;
animation.repeatCount = 1; // Change it for repetition
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards; // To keep the last frame when animation ends
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[imageView.layer addAnimation:animation forKey:#"animation"];
}
UPDATE- Swift 3
func animate() {
var imageArray = [CGImage]()
let imageCount: Int = 3
for i in 0...imageCount {
imageArray.append((UIImage(named: String(format:"image\(i)"))?.cgImage!)!)
}
let animation = CAKeyframeAnimation(keyPath: "contents")
animation.calculationMode = kCAAnimationLinear
// Make sure this is kCAAnimationLinear or kCAAnimationCubic other mode doesnt consider the timing function
animation.duration = CFTimeInterval(floatLiteral: 8.0)
animation.values = imageArray
animation.repeatCount = 1
// Change it for repetition
animation.isRemovedOnCompletion = false
animation.fillMode = kCAFillModeForwards
// To keep the last frame when animation ends
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animImageView.layer.add(animation, forKey: "animation")
}
I am using the following CABasicAnimation. But, its very slow..is there a way to speed it up ? Thanks.
- (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 * 2.0 * direction];
// Perform the rotation over this many seconds
rotationAnimation.duration = inDuration;
// Set the pacing of the animation
rotationAnimation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// Add animation to the layer and make it so
[inLayer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
}
Core Animation animations can repeat a number of times, by setting the repeatCount property on the animation.
So if you'd like to have an animation run for a total 80 seconds, you need to figure out a duration for one pass of the animation – maybe one full spin of this layer – and then set the duration to be that value. Then let the animation repeat that full spin several times to fill out your duration.
So something like this:
rotationAnimation.repeatCount = 8.0;
Alternatively, you can use repeatDuration to achieve a similar affect:
rotationAnimation.repeatDuration = 80.0;
In either case, you need to set the duration to the time of a single spin, and then repeat it using ONE of these methods. If you set both properties, the behavior is undefined. You can check out the documentation on CAMediaTiming here.