I am trying to apply a slow-motion effect to my application, pretty much like how you can slow down most graphical effects of Mac OS if you press Shift.
My application uses CoreAnimation, so I thought it should be no biggie: set speed to some slower value (like 0.1) and set it back to 1 once I'm done, and here I go.
It seems, unfortunately, that this is not the right way. The slowdown works great, however when I want to get back to normal speed, it resumes as if the speed was 1 the whole time. This basically means that if I held Shift for long enough, as soon as I release it, the animation instantly completes.
I found a Technical QA page explaining how to pause and resume an animation, but I can't seem to get it right if it's not about entirely pausing the animation. I'm definitely not very good at time warping.
What would be the right way to slow down then resume an animation with CoreAnimation?
Here's the useful code:
-(void)flagsChanged:(NSEvent *)theEvent
{
CALayer* layer = self.layer;
[CATransaction begin];
CATransaction.disableActions = YES;
layer.speed = (theEvent.modifierFlags & NSShiftKeyMask) ? 0.1 : 1;
[CATransaction commit];
}
Tricky problem...
I set up a test with a basic UIView.
I initiate an animation of its layer from it's current point to a target point.
In order to slow down the core animation, I actually had to construct and replace a new animation (since you can't modify the existing animation).
It's especially important to figure out how far along the current animation has already proceeded. This is done by accessing beginTime, currentTime, calculating the elapsed time and then figuring out how long should be the duration of the new animation.
- (void)initiate {
if(!initiated) {
[CATransaction begin];
[CATransaction disableActions];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseInEaseOut]];
CABasicAnimation *ba = [CABasicAnimation animationWithKeyPath:#"position"];
ba.duration = 10.0f;
ba.fromValue = [NSValue valueWithCGPoint:view.layer.position];
ba.toValue = [NSValue valueWithCGPoint:CGPointMake(384, 512)];
[view.layer addAnimation:ba forKey:#"animatePosition"];
[CATransaction commit];
initiated = YES;
}
}
- (void)slow {
[CATransaction begin];
CABasicAnimation *old = (CABasicAnimation *)[view.layer animationForKey:#"animatePosition"];
CABasicAnimation *ba = [CABasicAnimation animationWithKeyPath:#"position"];
CFTimeInterval animationBegin = old.beginTime;
CFTimeInterval currentTime = CACurrentMediaTime();
CFTimeInterval elapsed = currentTime - animationBegin;
ba.duration = [[old valueForKey:#"duration"] floatValue] - elapsed;
ba.duration = [[old valueForKey:#"duration"] floatValue];
ba.autoreverses = [[old valueForKey:#"autoreverses"] boolValue];
ba.repeatCount = [[old valueForKey:#"repeatCount"] floatValue];
ba.fromValue = [NSValue valueWithCGPoint:((CALayer *)[view.layer presentationLayer]).position];
ba.toValue = [old valueForKey:#"toValue"];
ba.speed = 0.1;
[view.layer addAnimation:ba forKey:#"animatePosition"];
[CATransaction commit];
}
- (void)normal {
[CATransaction begin];
CABasicAnimation *old = (CABasicAnimation *)[view.layer animationForKey:#"animatePosition"];
CABasicAnimation *ba = [CABasicAnimation animationWithKeyPath:#"position"];
CFTimeInterval animationBegin = old.beginTime;
CFTimeInterval currentTime = CACurrentMediaTime();
CFTimeInterval elapsed = currentTime - animationBegin;
ba.duration = [[old valueForKey:#"duration"] floatValue] - elapsed;
ba.autoreverses = [[old valueForKey:#"autoreverses"] boolValue];
ba.repeatCount = [[old valueForKey:#"repeatCount"] floatValue];
ba.fromValue = [NSValue valueWithCGPoint:((CALayer *)[view.layer presentationLayer]).position];
ba.toValue = [old valueForKey:#"toValue"];
ba.speed = 1;
[view.layer addAnimation:ba forKey:#"animatePosition"];
[CATransaction commit];
}
Note: The above code works only in 1 direction, not with animations that autoreverse...
Related
I want to realize a round progress, and the progress value can be set dynamically. The code is followed:
- (CGPoint)center {
return CGPointMake(self.view.frame.origin.x + self.view.frame.size.width / 2.0,
self.view.frame.origin.y + self.view.frame.size.height / 2.0);
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
CGMutablePathRef roundPath = CGPathCreateMutable();
CGPathAddArc(roundPath, NULL, self.center.x, self.center.y,
20,
2 * M_PI + M_PI_2,
M_PI_2,
YES);
CAShapeLayer *backgroundLayer = [CAShapeLayer layer];
backgroundLayer.frame = self.view.layer.bounds;
backgroundLayer.path = roundPath;
backgroundLayer.strokeColor = [[NSColor blueColor] CGColor];
backgroundLayer.fillColor = nil;
backgroundLayer.lineWidth = 10.0f;
backgroundLayer.lineJoin = kCALineJoinBevel;
[self.view.layer addSublayer:backgroundLayer];
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.frame = self.view.layer.bounds;
pathLayer.path = roundPath;
pathLayer.strokeColor = [[NSColor whiteColor] CGColor];
pathLayer.fillColor = nil;
pathLayer.lineWidth = 10.0f;
pathLayer.lineJoin = kCALineJoinBevel;
[self.view.layer addSublayer:pathLayer];
self.pathLayer = pathLayer;
[self start];
}
- (void)start {
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 0.01;
pathAnimation.fromValue = [NSNumber numberWithFloat:self.progress];
pathAnimation.toValue = [NSNumber numberWithFloat:self.progress+0.01];
[self.pathLayer setStrokeEnd:self.progress + 0.01];
[pathAnimation setDelegate:self];
[self.pathLayer addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
self.progress += 0.01;
if (self.progress < 1.0) {
[self start];
}
}
I found when I set the duration be 0.1f or even bigger, it will work right.But if I set the duration be 0.01f, the animation will not start from the correct value, it will animate from a bigger value then decrease to the correct value. So the whole animation always flash, anybody got the same question or know why? Thanks very much!
originaluser2 is probably right about the specific cause, but this design is incorrect.
At 60fps, one frame is 0.0167s. You're asking to animate the change in less than a single frame. Each of those animations has its own media timing (kCAMediaTimingFunctionDefault), which means your creating complicated ramp up/ramp down velocities through your animation. And your timing is going to be erratic anyway because you're picking up a little error in every step (animationDidStop can take slightly different amounts of time to run depending on many factors). The whole point of animations is that you don't need to do this kind of stuff. That's why you have an animation engine.
Just animate to progress over the time you want it to take. Don't try to inject many extra animation steps. Injecting steps is what the animation engine does.
CALayer is designed to do most of this stuff for you anyway. You don't need to be creating explicit animations for this; just use implicit. Something like (untested, uncompiled):
- (void)setProgress: (CGFloat)progress {
double velocity = 1.0;
[CATransaction begin];
double delta = fabs(_progress - progress);
[CATransaction setAnimationDuration: delta * velocity];
[self.pathLayer setStrokeEnd: progress];
[CATransaction commit];
}
As Rob says, adding repeated animations isn't the best idea. His solution will probably work out nicely for you. However if you're still insistent on using repeated animations, here's the fix for your current code:
The problem is you're calling this line of code before you add your animation.
[self.pathLayer setStrokeEnd:self.progress + 0.01];
This will create an implicit animation on the layer, and therefore when you come to add your explicit animation – it will cause problems (the flashes you were observing).
The solution is to update the model layer within a CATransaction after you start the animation. You'll also need to set disableActions to YES in order to prevent an implicit animation from being generated.
For example:
- (void)start {
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 0.01;
pathAnimation.fromValue = [NSNumber numberWithFloat:self.progress];
pathAnimation.toValue = [NSNumber numberWithFloat:self.progress+0.01];
[pathAnimation setDelegate:self];
[self.pathLayer addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self.pathLayer setStrokeEnd:self.progress + 0.01];
[CATransaction commit];
}
Although, it's also worth noting you could just create your animation by just using a CATransaction, and the implicit animation for strokeEnd.
For example:
- (void)start {
[CATransaction begin];
[CATransaction setAnimationDuration:0.01];
[CATransaction setCompletionBlock:^{
self.progress += 0.01;
if (self.progress < 1.0) {
[self start];
}
}];
[self.pathLayer setStrokeEnd:self.progress + 0.01];
[CATransaction commit];
}
That way you don't have to create a new explicit animation on each iteration.
APPROACH 1
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
[CATransaction begin];
{
[CATransaction setAnimationDuration:15];//Dynamic Duration
[CATransaction setCompletionBlock:^{
}];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
animation.autoreverses = NO;
animation.removedOnCompletion = NO;
animation.fromValue = #0;
animation.toValue = #1;
animation.timeOffset = 0;
animation.fillMode = kCAFillModeForwards;
[self.pathLayer addAnimation:animation forKey:animationKey];
}
[CATransaction commit];
I have added CAShapeLayer (pathLayer) in my view and I want it to animate around the view with stroke effect, the code above does the job but my problem is to change color in 3 equal proportions. So what I am assuming is to repeat the above code 3 times and change the following lines in respective order.
for 1st
animation.fromValue = #0;
animation.toValue = #(1/3);
animation.timeOffset = 0;
for 2nd
animation.fromValue = #(1/3);
animation.toValue = #(2/3);
animation.timeOffset = 0;// I don't know how to exactly set this active local
time since the duration which is currently 15 is dynamic can be 30 or 10.
for 3rd
animation.fromValue = #(2/3);
animation.toValue = #(3);
animation.timeOffset = 0;// Active local time- Not sure how and which value to set
APPROACH 2
Instead of 3 transactions with offset technique lets start 2nd transaction when 1st completes and 3rd when 2nd. But the fraction of time that is taken to start the new animation when one is completed a lag/jerk is visible.
APPROACH 3
SubClass CAShapeLayer
By doing SubClass, the drawInContext method is called only once, and if some extra property is added and it is changed the drawInContext method is called repeatedly and this way the layer color can be changed after specific progress period of time.
But overriding the drawInContext method doesn't serve the purpose.
Any Suggestions ? I don't want to implement NSTimer separately.
I'm not 100% clear on what you want here, but if the goal is just for the whole stroke to change color as it draws, but in three discrete stages, then I would propose adding the following. I cooked up these examples in a default "Single View Application" template. I've got a button set up with its action pointing at -doStuff:. If the whole stroke color were to change, it might look something like this:
To produce that, the code looked like:
#implementation MyViewController
{
CAShapeLayer* mLayer;
}
- (IBAction)doStuff:(id)sender
{
const NSUInteger numSegments = 3;
const CFTimeInterval duration = 2;
[mLayer removeFromSuperlayer];
mLayer = [[CAShapeLayer alloc] init];
mLayer.frame = CGRectInset(self.view.bounds, 100, 200);
mLayer.fillColor = [[UIColor purpleColor] CGColor];
mLayer.lineWidth = 12.0;
mLayer.lineCap = kCALineCapSquare;
mLayer.strokeEnd = 0.0;
mLayer.path = [[UIBezierPath bezierPathWithRect: mLayer.bounds] CGPath]; // This can be whatever.
[self.view.layer addSublayer: mLayer];
[CATransaction begin];
{
[CATransaction setAnimationDuration: duration];// Dynamic Duration
[CATransaction setCompletionBlock:^{ NSLog(#"Done"); }];
const double portion = 1.0 / ((double)numSegments);
NSMutableArray* values = [NSMutableArray arrayWithCapacity: numSegments];
NSMutableArray* times = [NSMutableArray arrayWithCapacity: numSegments + 1];
for (NSUInteger i = 0; i < numSegments; i++)
{
[values addObject: (__bridge id)[[UIColor colorWithHue: i * portion saturation:1 brightness:1 alpha:1] CGColor]];
[times addObject: #(i * portion)];
}
[times addObject: #(1.0)]; // Have to add this, otherwise the last value wont get used.
{
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath: #"strokeColor"];
animation.keyTimes = times;
animation.values = values;
animation.calculationMode = kCAAnimationDiscrete;
animation.removedOnCompletion = NO;
animation.timeOffset = 0;
animation.fillMode = kCAFillModeForwards;
[mLayer addAnimation: animation forKey: #"strokeColor"];
}
{
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath: #"strokeEnd"];
animation.fromValue = #(0);
animation.toValue = #(1);
animation.removedOnCompletion = NO;
animation.timeOffset = 0;
animation.fillMode = kCAFillModeForwards;
[mLayer addAnimation: animation forKey: #"strokeEnd"];
}
}
[CATransaction commit];
}
#end
Alternately, if the goal is to have three different segments of the stroke, all with different colors, that's a little more complicated, but can still be done with the same basic principals. One thing to note is that, without custom drawing, your CAShapeLayers can't have more than one stroke color (AFAIK), so you'll need to break this up into several sublayers.
This next example puts a shape layer into the view and then adds the sublayers for each part of the stroke and sets up the animation such that it appears theres a single, multi-color stroke being drawn, where each segment is a separate color. Here's roughly what it looked like:
Here's the code:
#implementation MyViewController
{
CAShapeLayer* mLayer;
}
- (IBAction)doStuff:(id)sender
{
const NSUInteger numSegments = 3;
const CFTimeInterval duration = 2;
[mLayer removeFromSuperlayer];
mLayer = [[CAShapeLayer alloc] init];
mLayer.frame = CGRectInset(self.view.bounds, 100, 200);
mLayer.fillColor = [[UIColor purpleColor] CGColor];
mLayer.lineWidth = 12.0;
mLayer.lineCap = kCALineCapSquare;
mLayer.path = [[UIBezierPath bezierPathWithRect: mLayer.bounds] CGPath]; // This can be whatever.
[self.view.layer addSublayer: mLayer];
[CATransaction begin];
{
[CATransaction setAnimationDuration: duration];//Dynamic Duration
[CATransaction setCompletionBlock:^{ NSLog(#"Done"); }];
const double portion = 1.0 / ((double)numSegments);
for (NSUInteger i = 0; i < numSegments; i++)
{
CAShapeLayer* strokePart = [[CAShapeLayer alloc] init];
strokePart.fillColor = [[UIColor clearColor] CGColor];
strokePart.frame = mLayer.bounds;
strokePart.path = mLayer.path;
strokePart.lineCap = mLayer.lineCap;
strokePart.lineWidth = mLayer.lineWidth;
// These could come from an array or whatever, this is just easy...
strokePart.strokeColor = [[UIColor colorWithHue: i * portion saturation:1 brightness:1 alpha:1] CGColor];
strokePart.strokeStart = i * portion;
strokePart.strokeEnd = (i + 1) * portion;
[mLayer addSublayer: strokePart];
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath: #"strokeEnd"];
NSArray* times = #[ #(0.0), // Note: This works because both the times and the stroke start/end are on scales of 0..1
#(strokePart.strokeStart),
#(strokePart.strokeEnd),
#(1.0) ];
NSArray* values = #[ #(strokePart.strokeStart),
#(strokePart.strokeStart),
#(strokePart.strokeEnd),
#(strokePart.strokeEnd) ];
animation.keyTimes = times;
animation.values = values;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
[strokePart addAnimation: animation forKey: #"whatever"];
}
}
[CATransaction commit];
}
#end
I'm not sure I've exactly understood what you were going for, but hopefully one of these is helpful.
Please refer to the code for rotating the piechart when any sice is touched:
-(void)pieChart:(CPTPieChart *)plot sliceWasSelectedAtRecordIndex:(NSUInteger)index{
[self rotatePieChart:plot];
}
-(void)rotatePieChart:(CPTPieChart *)plot {
[CATransaction begin];
{
CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:#"transform"];
CATransform3D transform = CATransform3DMakeRotation(-45, 0, 0, 1);
rotation.toValue = [NSValue valueWithCATransform3D:transform];
rotation.fillMode = kCAFillModeForwards;
rotation.duration = 1.0f;
rotation.cumulative=YES;
rotation.removedOnCompletion = NO;
[plot addAnimation:rotation forKey:#"rotation"];
}
[CATransaction commit];
}
ON first Tap the piechart rotates but on subsequent taps nothing happens.
Instead of rotating the whole plot layer, animate only the startAngle property. I suspect the hit-detection code is getting confused by the rotated coordinate system.
I'm animating the shadow path for CALayer.
The frame resizes correctly, but the shadow does not scale.
Instead the shadow starts at the final size CGSize(20,20) and holds throughout the animation even though I set the shadowPath to an initial value
[CATransaction begin];
[CATransaction setAnimationDuration: 0];
[CATransaction setDisableActions: TRUE];
layer.frame = CGRectMake(0,0,10,10);
layer.shadowPath = [UIBezierPath bezierPathWithRect:layer.bounds].CGPath;
[CATransaction commit];
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:10] forKey:kCATransactionAnimationDuration];
layer.frame = CGRectMake(0,0,20,20);
layer.shadowPath = [UIBezierPath bezierPathWithRect:tile.bounds].CGPath;
[CATransaction commit];
At first, small square with drop shadow.
When button pushed, square and shadow grow bigger together.
The main code is below:
[CATransaction begin];
[CATransaction setAnimationDuration:5.0];
CAMediaTimingFunction *timing = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[CATransaction setAnimationTimingFunction:timing];
layer.frame = CGRectMake(0,0,100,100);
[CATransaction commit];
CABasicAnimation *shadowAnimation = [CABasicAnimation animationWithKeyPath:#"shadowPath"];
shadowAnimation.duration = 5.0;
shadowAnimation.fromValue = (id)[UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 50, 50)].CGPath;
shadowAnimation.toValue = (id)[UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 100, 100)].CGPath;
[layer addAnimation:shadowAnimation forKey:#"shadow"];
You can download this project from GitHub and just run it.
https://github.com/weed/p120812_CALayerShadowTest
This question was very hard for me ! :)
Wanted to add another answer based on Weed's answer. I took weed's answer and tried to put everything in a CATransaction because i wanted to animate multiple layers and be sure animations happened together. Here you guys go if you ever need it. Also, i still don't understand why you have to use the fromValue and toValue in a CATransaction. Why can't you just do the same thing you do with other properties like frame.
[CATransaction begin];
[CATransaction setValue:[CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionEaseOut]
forKey:kCATransactionAnimationTimingFunction];
for (CALayer *layer in self.layers){
CABasicAnimation *shadowAnimation =
[CABasicAnimation animationWithKeyPath:#"shadowPath"];
shadowAnimation.fromValue =
(id)[UIBezierPath bezierPathWithRect:layer.bounds].CGPath;
shadowAnimation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
layer.frame = rectFinal;
shadowAnimation.toValue =
(id)[UIBezierPath bezierPathWithRect:layer.bounds].CGPath;
layer.shadowPath =
[UIBezierPath bezierPathWithRect:layer.bounds].CGPath;
[layer addAnimation:shadowAnimation forKey:nil];
}
[CATransaction commit];
I've successfully put together a mapview with a pin annotation representing the users current position that updates at a set interval. When the location manager updates the user's position, the pin annotation disappears and reappears at the new location. Has anyone played with getting the current user's GPS location to update through the use of an animation, like what is done in Apple's offical mapping application? If so, I'd love some pointers to get this to work. Thanks!
Solution 1:
First, you need to make your view controller implement MKMapViewDelegate if it doesn't already.
Then, implement this method:
- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views {
MKAnnotationView *aV;
for (aV in views) {
CGRect endFrame = aV.frame;
aV.frame = CGRectMake(aV.frame.origin.x, aV.frame.origin.y - 230.0, aV.frame.size.width, aV.frame.size.height);
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.45];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[aV setFrame:endFrame];
[UIView commitAnimations];
}
}
Add your annotations to the MapView and when they are added, this delegate method will be called and will animate the pins from top to bottom as they are added.
The values for timing and positioning can be changed a little bit but I've tweaked it to make it look best/closest to the traditional drop (as far as I've tested).
Solution 2:
Alternatively, if you're making a MKAnnotationView subclass, you can use didMoveToSuperview to trigger the animation. The following does a drop that ends in a slight 'squish' effect
#define kDropCompressAmount 0.1
#implementation MyAnnotationViewSubclass
...
- (void)didMoveToSuperview {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform"];
animation.duration = 0.4;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0, -400, 0)];
animation.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
CABasicAnimation *animation2 = [CABasicAnimation animationWithKeyPath:#"transform"];
animation2.duration = 0.10;
animation2.beginTime = animation.duration;
animation2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
animation2.toValue = [NSValue valueWithCATransform3D:CATransform3DScale(CATransform3DMakeTranslation(0, self.layer.frame.size.height*kDropCompressAmount, 0), 1.0, 1.0-kDropCompressAmount, 1.0)];
animation2.fillMode = kCAFillModeForwards;
CABasicAnimation *animation3 = [CABasicAnimation animationWithKeyPath:#"transform"];
animation3.duration = 0.15;
animation3.beginTime = animation.duration+animation2.duration;
animation3.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
animation3.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
animation3.fillMode = kCAFillModeForwards;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = [NSArray arrayWithObjects:animation, animation2, animation3, nil];
group.duration = animation.duration+animation2.duration+animation3.duration;
group.fillMode = kCAFillModeForwards;
[self.layer addAnimation:group forKey:nil];
}
Hope this helps!
PK