I have a UIView whose layer has 2 sublayers, one is a CAShapeLayer, and the other a CALayer.
The CAShapeLayer has a path set using bezierPathWithOvalInRect, and is animated using CABasicAnimation. The "strokeEnd" property is animated from 0.0 to 1.0, over a certain duration. This has the effect of seeing the oval draw from beginning to end over the duration.
The CALayer simply has its contents set to a pencil image, and is animated using CAKeyframeAnimation. The "position" property is animated by setting the path property of the CAKeyframeAnimation to the same path as the CAShapeLayer, and the same duration as the CABasicAnimation. This has the effect of the pencil moving along the same path during the same duration, and it looks like the pencil is drawing the oval.
Works beautifully in iOS6. However, in iOS7, the timing is off - the position and strokeEnd animations are not in sync - they are in sync at specific moments, specifically at times 0, duration/4, duration/2, duration*3/4, and duration - but in between, the synchronization is off.
If instead of an ellipse, I use a rectangle or triangle, for example, it works great in iOS6 and iOS7. Only issue is an ellipse in iOS7.
Essentially I need to know how to synchronize 2 different animations, where each animation is animating a different layer.
Here is the code that creates the 2 layers:
self.drawingLayer = [CAShapeLayer layer];
self.drawingLayer.frame = self.view.bounds;
self.drawingLayer.bounds = drawingRect;
self.drawingLayer.path = path.CGPath;
self.drawingLayer.strokeColor = [[UIColor blackColor] CGColor];
self.drawingLayer.fillColor = nil;
self.drawingLayer.lineWidth = 10.0f;
self.drawingLayer.lineJoin = kCALineJoinRound;
self.drawingLayer.lineCap = kCALineJoinRound;
[self.view.layer addSublayer:self.drawingLayer];
UIImage *pencilImage = [UIImage imageNamed:#"pencil.png"];
self.pencilLayer = [CALayer layer];
self.pencilLayer.contents = (id)pencilImage.CGImage;
self.pencilLayer.contentsScale = [UIScreen mainScreen].scale;
self.pencilLayer.anchorPoint = CGPointMake(0.0, 1.0); //bottom left corner
self.pencilLayer.frame = CGRectMake(0.0f, 0.0f, 100.0f, 100.0f);
[self.view.layer addSublayer:self.pencilLayer];
And here is the code that creates and starts the 2 different animations (self.drawingLayer is the CAShapeLayer, and self.pencilLayer is the CALayer)
CABasicAnimation *drawingAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawingAnimation.duration = 10.0;
drawingAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawingAnimation.toValue = [NSNumber numberWithFloat:1.0f];
[self.drawingLayer addAnimation:drawingAnimation forKey:#"strokeEnd"];
CAKeyframeAnimation *pencilAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pencilAnimation.duration = drawingAnimation.duration;
pencilAnimation.path = self.drawingLayer.path;
pencilAnimation.calculationMode = kCAAnimationPaced;
pencilAnimation.delegate = self;
pencilAnimation.fillMode = kCAFillModeForwards;
pencilAnimation.removedOnCompletion = NO;
[self.pencilLayer addAnimation:pencilAnimation forKey:#"position"];
UPDATE
Here's a test app that illustrates the problem.
https://github.com/xjones/AnimatedPaths
Apple has informed me this is a known bug in iOS7, and they asked me to file a bug as well, to help them understand the demand for a fix. The tech support person at Apple told me he does not know of a workaround. If any of you figure out a workaround, I’m all ears...
I filed Apple bug reporter bug #15191834 on Oct 9, 2013. In my testing with iOS 8.1 and Xcode 6.1.1 the bug is still there in the Simulator and in the device. The bug was introduced in iOS 7 and has never been fixed.
Please file a bug so Apple can see others are impacted!
Related
I am working on a kids letter tracing app - see attached screen shot.
I was able to display the letter by using the bezierpath identified by the font glyphs and allow writing the touches inside the bezierpath.
Now, i want to add an help animation so that it shows how to write this letter from start to finish.
How to do that?
Consider we display "A" and i want to show animation shows how to write "A" for kids.
Any pointers / ideas.
thanks much.
I used this animation for my app. Created a circular progress bar that fills up dot clockwise
CABasicAnimation *pathAnim = [CABasicAnimation animationWithKeyPath: #"path"];
pathAnim.toValue = (id)newPath.CGPath;
// -- initialize CAAnimation group with duration of 0.2secs and beginTime will begin after another.
CAAnimationGroup *anims = [CAAnimationGroup animation];
anims.animations = [NSArray arrayWithObjects:pathAnim, nil];
anims.removedOnCompletion = NO;
anims.duration = 0.2f;
anims.beginTime = CACurrentMediaTime() + anims.duration*i;
anims.fillMode = kCAFillModeForwards;
[anims setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[progressLayer addAnimation:anims forKey:nil];
There is no straightforward way to do this. I imagine the following would be easiest to implement:
Create a path per character that will trace how the stroke will be written. If you have limited number of characters like in the alphabet, it's possible to do this by hand. You can implement something like this to capture the path: http://code.tutsplus.com/tutorials/smooth-freehand-drawing-on-ios--mobile-13164
Use your current path (in red in your photo) as a clipping mask so the fill does not overflow.
Stroke the path you created in 1. with a very thick stroke to do the fill.
I can't find a "built-in" way to animate the fill as you want. There is a built-in way to animate the stroke, however:
Use a UIView with a CAShapeLayer backing it. CAShapeLayer has a path property. You can then apply an animation to the layer's strokeStart and/or strokeEnd properties.
Hope it helps.
After loading your bezierpath add init the animation:
CAKeyframeAnimation *shapeKeyframeAnimation = [CAKeyframeAnimation animationWithKeyPath:#"strokeEnd"];
shapeKeyframeAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
shapeKeyframeAnimation.keyTimes = #[#0.0f, #0.6f, #1.0f];
shapeKeyframeAnimation.values = #[#0.0f, #1.0f, #1.0f];
shapeKeyframeAnimation.duration = 4.0f;
shapeKeyframeAnimation.repeatCount = CGFLOAT_MAX;
then add it to your shape layer:
[shapeLayer addAnimation:_loadingKeyframeAnimation forKey:#"stoke"];
I'm using CAKeyframeAnimation to move CALayer on a circle trajectory. Sometimes I need to stop animation and to move animation to the point at which the animation stopped. Here the code:
CAKeyframeAnimation* circlePathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
CGMutablePathRef circularPath = the circle path;
circlePathAnimation.path = circularPath;
circlePathAnimation.delegate = self;
circlePathAnimation.duration = 3.0f;
circlePathAnimation.repeatCount = 1;
circlePathAnimation.fillMode = kCAFillModeForwards;
circlePathAnimation.removedOnCompletion = NO;
circlePathAnimation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:.43 :.78 :.69 :.99];
[circlePathAnimation setValue:layer forKey:#"animationLayer"];
[layer addAnimation:circlePathAnimation forKey:#"Rotation"];
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
CALayer *layer = [anim valueForKey:#"animationLayer"];
if (layer) {
CALayer *presentationLayer = layer.presentationLayer;
layer.position = presentationLayer.position;
}
}
But the presentation layer has no changes in position!!! I read that it is no longer reliable from ios 8. So is there any other way I can find the current position of animated layer?
I wasn't aware that the position property of the presentation layer stopped telling you the location of your layer in iOS 8. That's interesting. Hit testing using the hitTest method on the presentation layer still works, I just tried it on an app I wrote a while back.
Pausing and resuming an animation also still works.
For a simple path like a circle path you could check the time of the animation and then calculate the position using trig.
For more complex paths you'd have to know about the path your layer was traversing. Plotting a single cubic or quadratic bezier curve over time is pretty straightforward, but a complex UIBezierPath/CGPath with a mixture of different shapes in it would be a bear to figure out.
I am trying to achieve the filling of a circle(something like a pie).
I want to fill it from 0 to 360
This is what i have done
CAShapeLayer *circleLayer=[CAShapeLayer layer];
circleLayer.path=[UIBezierPath bezierPathWithArcCenter:CGPointMake(150, 150) radius:100 startAngle:0 endAngle:360 clockwise:YES].CGPath;
circleLayer.strokeColor=[UIColor redColor].CGColor;
circleLayer.fillColor=[UIColor blueColor].CGColor;
[self.view.layer addSublayer:circleLayer];
//adding animation
CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:#"fillColor"];
basicAnimation.fillMode=kCAFillModeForwards;
basicAnimation.fromValue=(id)[UIColor clearColor].CGColor;
basicAnimation.toValue=(id)[UIColor yellowColor].CGColor;
basicAnimation.duration=1.5f;
basicAnimation.repeatCount=10;
basicAnimation.autoreverses=YES;
[circleLayer addAnimation:basicAnimation forKey:#"fillColor"];
I am not able to figure out,how do i acheive the effect of color filling from start angle to end angle.
Thanks
Your description of what you want to do is unclear. Are you trying to create a "clock wipe" effect, where the color change sweeps like the second hand of a clock?
If so, check out this demo app I created on github:
github animation demo project
Look in particular at the description of the Clock Wipe animation in the read me file.
Here is what the clock wipe animation looks like:
This project animates the mask of an image view to do a clock wipe reveal animation, but it would be a simple matter to animate a shape layer as a content layer of a view instead of making it a mask.
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = 0.7f;
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat:1.0f];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[circleLayer addAnimation:animation forKey:#"strokeEnd"];
I have implemented a shape layer subclass for similar requirements. Here is the Github link. For your requirement the arc start radius is 0.0f and end radius is the circle radius. You can also refer the sample project in that link.
I've been having a problem that I can't seem to figure out. In my app I use a custom UIImageView class to represent movable objects. Some are loaded with static .png images and use frame animation, while others are loaded with CAShapeLayer paths from .svg files. For some of those, I'm getting stutter when the layer is animating from one path to another, while the UIImageView is moving.
You can see it in the linked video. When I touch the mermaid horn, a note spawns (svg path) and animates into a fish (another svg path), all while drifting upwards. The stuttering occurs during that animation / drift. It's most noticeable the third time I spawn the fish, around 19 seconds into the video. (The jumping at the end of each animation I need to fix separately so don't worry about that)
http://www.youtube.com/watch?v=lnrNWuvqQ4w
This does not occur when I test the app in iOS Simulator - everything is smooth. On my iPad 2 it stutters. When I turn off the note/fish movement (drifting upward), it animates smooth on the iPad, so obviously it has to do with moving the view at the same time. There's something I'm missing but I can't figure it out.
Here is how I set up the animation. PocketSVG is a class I found on Github that converts an .svg to a Bezier path.
animShape = [CAShapeLayer layer];
[animShape setShouldRasterize:YES];
[animShape setRasterizationScale:[[UIScreen mainScreen] scale]];
PocketSVG *tSVG;
UIBezierPath *tBezier;
PocketSVG *tSVG2;
UIBezierPath *tBezier2;
CAShapeLayer *tLayer2;
CABasicAnimation *animBezier;
CABasicAnimation *animScale;
tSVG = [[PocketSVG alloc] initFromSVGFileNamed:[NSString stringWithFormat:#"%#", tName]];
// Use PocketSVG to convert the SVG to a Bezier Path
tBezier = tSVG.bezier;
animShape.path = tBezier.CGPath;
animShape.lineWidth = 1;
animShape.strokeColor = [[UIColor blackColor] CGColor];
animShape.fillColor = [[UIColor blackColor] CGColor];
animShape.fillRule = kCAFillRuleNonZero;
// Set the frame & bounds to position the piece and set anchor point for rotation
// parentPaper is the Mermaid the note spawns from
CGPoint newCenter;
CGFloat imageScale = fabsf(parentPaper.transform.a);
newCenter = // Code excised, set center based on custom spawn points in relation to parent position and scale
animShape.bounds = CGPathGetBoundingBox(animShape.path);
CGRect lBounds = CGRectMake(newCenter.x, newCenter.y, animShape.bounds.size.width, animShape.bounds.size.height);
[animShape setFrame:lBounds];
halfSize = CGPointMake(animShape.bounds.size.width/2, animShape.bounds.size.height/2);
animShape.anchorPoint = CGPointMake(prp.childSpawnX, prp.childSpawnY);
// Create the end path for animation
tSVG2 = [[PocketSVG alloc] initFromSVGFileNamed:[NSString stringWithFormat:#"%#2", tName]];
tBezier2 = tSVG2.bezier;
tLayer2 = [CAShapeLayer layer];
tLayer2.path = tBezier2.CGPath;
// Create the animation and add it to the layer
animBezier = [CABasicAnimation animationWithKeyPath:#"path"];
animBezier.delegate = self;
animBezier.duration = prp.frameDur;
animBezier.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animBezier.fillMode = kCAFillModeForwards;
animBezier.removedOnCompletion = NO;
animBezier.autoreverses = behavior.autoReverse;
if (prp.animID < 0) {
animBezier.repeatCount = FLT_MAX;
}
else {
animBezier.repeatCount = prp.animID;
}
animBezier.fromValue = (id)animShape.path;
animBezier.toValue = (id)tLayer2.path;
[animShape addAnimation:animBezier forKey:#"animatePath"];
// also scale the animation if spawned from a parent
// i.e. small note to normal-sized fish
if (parentPaper != nil) {
animScale = [CABasicAnimation animationWithKeyPath:#"transform"];
animScale.delegate = self;
animScale.duration = prp.frameDur;
animScale.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animScale.fillMode = kCAFillModeForwards;
animScale.removedOnCompletion = YES;
animScale.autoreverses = NO;
animScale.repeatCount = 1;
animScale.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(imageScale, imageScale, 0.0)];
animScale.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.0, 1.0, 0.0)];
[animShape addAnimation:animScale forKey:#"animateScale"];
}
[self.layer addSublayer:animShape];
And this is basically how I'm moving the UIImageView. I use a CADisplayLink at 60 frames, figure out the new spot based on existing position of animShape.position, and update like this, where self is the UIImageView:
[self.animShape setPosition:paperCenter];
Everything else runs smooth, it's just this one set of objects that stutter and jump about when running on the iPad itself. Any ideas of what I'm doing wrong? Moving it the wrong way, perhaps? I do get confused with layers, frames and bounds still.
To fix this I ended up changing the way the UIImageView moves. Instead of changing the frame position through my game loop, I switched it to a CAKeyframeAnimation on the position since it's a temporary movement that only runs once. I used CGPathAddCurveToPoint to replicate the sine wave movement that I originally had. It was rather simple and I probably should have done it that way to begin with.
I can only surmise that the stuttering was due to moving it through the CADisplayLink loop while its animation separately.
Using the code below I've been trying to get my CALayer to scale around its center - but it scales by left/top (0,0). I saw this question: Anchor Point in CALayer and have tried setting the anchorPoint - but it was [.5, .5] before I set it and setting makes no difference. Also tried setting contentsGravity.
The set up is I have a CAShapeLayer added to self.view.layer I do some drawing in it and then add the CABasicAnimation. The animation runs fine - but it scales toward the top/left - not the center of the view.
Have been tinkering with this for a few hours - it must be something simple but I'm not getting it.
shapeLayer.anchorPoint = CGPointMake(.5,.5);
shapeLayer.contentsGravity = #"center";
printf("anchorPoint %f %f\n", shapeLayer.anchorPoint.x, shapeLayer.anchorPoint.y);
CABasicAnimation *animation =
[CABasicAnimation animationWithKeyPath:#"transform.scale"];
animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)];
[animation setDuration:1.0];
animation.fillMode=kCAFillModeForwards;
animation.removedOnCompletion=NO;
[shapeLayer addAnimation:animation forKey:#"zoom"];
What is the bounds of your layer?
I bet it has zero size; try setting its size to the same size as your shape.
CAShapeLayer draws the shape as kind of an overlay, independent of the contents (and bounds and contentsGravity and etc.) that you'd use in an ordinary CALayer. It can be a bit confusing.
Also make sure that you're adding the animation after the view is created. If you're trying to add animation in the viewDidLoad, it probably won't work. Try adding it to viewDidAppear instead.
I only know Swift, but here's an example:
override func viewDidAppear (animated: Bool) {
addPulsation(yourButton)
}
func addPulsation (button: UIButton) {
let scaleAnimation:CABasicAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.duration = 1.0
scaleAnimation.repeatCount = 100
scaleAnimation.autoreverses = true
scaleAnimation.fromValue = 1.05;
scaleAnimation.toValue = 0.95;
button.layer.addAnimation(scaleAnimation, forKey: "scale")
}
It's an old thread but in case this helps anyone.
This ia a coordinate space problem. You just need to set the drawing space and the space of the the path. This code works for me...
...just put this at the start of the animation creation method (inside a method on the parent view)...
// add animation to remap (without affecting other elements)
let animationLayer = CALayer()
layer?.addSublayer(animationLayer). // (don't use optional for iOS)
// set the layer origin to the center of the view (or any center point for the animation)
animationLayer.bounds.origin .x = -self.bounds.size.width / 2.0
animationLayer.bounds.origin .y = -self.bounds.height / 2.0
// make image layer with circle around the zero point
let imageLayer = CAShapeLayer()
animationLayer.addSublayer(imageLayer)
let shapeBounds = CGRect(x: -bounds.size.width/2.0,
y:-bounds.size.height/2.0,
width: bounds.size.width,
height: bounds.size.height)
imageLayer.path = CGPath(ellipseIn: shapeBounds, transform: nil)
imageLayer.fillColor = fillColour
// **** apply your animations to imageLayer here ****
this code works unchanged on iOS and MacOS - apart from the optional needed on layer for MacOS.
(BTW you need to remove the animation layer after the animation finishes!)
I spent hours to find out how to do it according the center but couldn't find any solutions for OSX. I decided to to move layer using CATransform3DMakeTranslation and combine both transforms.
My code:
NSView *viewToTransform = self.view;
[viewToTransform setWantsLayer:YES];
viewToTransform.layer.sublayerTransform = CATransform3DConcat(CATransform3DMakeScale(-1.0f, -1.0f, 1.0f), CATransform3DMakeTranslation(viewToTransform.bounds.size.width, viewToTransform.bounds.size.height, 0));