I am running into an issue when I create an explicit animation to change the value of a CAShapeLayer's path from an ellipse to a rect.
In my canvas controller I setup a basic CAShapeLayer and add it to the root view's layer:
CAShapeLayer *aLayer;
aLayer = [CAShapeLayer layer];
aLayer.frame = CGRectMake(100, 100, 100, 100);
aLayer.path = CGPathCreateWithEllipseInRect(aLayer.frame, nil);
aLayer.lineWidth = 10.0f;
aLayer.strokeColor = [UIColor blackColor].CGColor;
aLayer.fillColor = [UIColor clearColor].CGColor;
[self.view.layer addSublayer:aLayer];
Then, when I animate the path I get a strange glitch / flicker in the last few frames of the animation when the shape becomes a rect, and in the first few frames when it animates away from being a rect. The animation is set up as follows:
CGPathRef newPath = CGPathCreateWithRect(aLayer.frame, nil);
[CATransaction lock];
[CATransaction begin];
[CATransaction setAnimationDuration:5.0f];
CABasicAnimation *ba = [CABasicAnimation animationWithKeyPath:#"path"];
ba.autoreverses = YES;
ba.fillMode = kCAFillModeForwards;
ba.repeatCount = HUGE_VALF;
ba.fromValue = (id)aLayer.path;
ba.toValue = (__bridge id)newPath;
[aLayer addAnimation:ba forKey:#"animatePath"];
[CATransaction commit];
[CATransaction unlock];
I have tried many different things like locking / unlocking the CATransaction, playing with various fill modes, etc...
Here's an image of the glitch:
http://www.postfl.com/outgoing/renderingglitch.png
A video of what I am experiencing can be found here:
http://vimeo.com/37720876
I received this feedback from the quartz-dev list:
David Duncan wrote:
Animating the path of a shape layer is only guaranteed to work when
you are animating from like to like. A rectangle is a sequence of
lines, while an ellipse is a sequence of arcs (you can see the
sequence generated by using CGPathApply), and as such the animation
between them isn't guaranteed to look very good, or work well at all.
To do this, you basically have to create an analog of a rectangle by
using the same curves that you would use to create an ellipse, but
with parameters that would cause the rendering to look like a
rectangle. This shouldn't be too difficult (and again, you can use
what you get from CGPathApply on the path created with
CGPathAddEllipseInRect as a guide), but will likely require some
tweaking to get right.
Unfortunately this is a limitation of the otherwise awesome animatable path property of CAShapeLayers.
Basically it tries to interpolate between the two paths. It hits trouble when the destination path and start path have a different number of control points - and curves and straight edges will have this problem.
You can try to minimise the effect by drawing your ellipse as 4 curves instead of a single ellipse, but it still isn't quite right. I haven't found a way to go smoothly from curves to polygons.
You may be able to get most of the way there, then transfer to a fade animation for the last part - this won't look as nice, though.
Related
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 have a UIView, with view.layer.mask set to an instance of CAShapeLayer. The shape layer contains a path, and now I want to add a hole to this shape by adding a second shape with even/odd rule, and fade-in the appearance of this hole.
The problem is that adding to path doesn't seem to be animatable:
[UIView animateWithDuration:2 animations:^() {
CGPathAddPath(parentPath, nil, holePath);
[v.layer.mask didChangeValueForKey:#"path"];
}];
How would I animate this?
After some fiddling, found a workaround:
Create a layer with two sublayers with two desired shapes, and use it as a mask
Animate opacity of the first sublayer (without a hole) from 1 to 0.
This works because child CAShapeLayer instances appear to be used as a union. When you hide the first sublayer without a hole, only the hole will be uncovered, the shared area will not change.
CGMutablePathRef p = CGPathCreateMutable();
// First path
CGPathAddPath(p, nil, outerPath);
CAShapeLayer *mask1 = [CAShapeLayer layer];
mask1.path = p;
// 2nd path with a hole
CGPathAddPath(p, nil, innerPath);
CAShapeLayer *mask2 = [CAShapeLayer layer];
mask2.path = p;
mask2.fillRule = kCAFillRuleEvenOdd;
CGPathRelease(p);
// Create actual mask that hosts two submasks
CALayer *mask = [CALayer layer];
[mask addSublayer:mask1];
[mask addSublayer:mask2];
myView.layer.mask = mask;
mask.frame = v.layer.bounds;
mask1.frame = mask.bounds;
mask2.frame = mask.bounds;
// ...
// Hide mask #1
CABasicAnimation *a = [CABasicAnimation animationWithKeyPath:#"opacity"];
a.fromValue = #1.0;
a.toValue = #0.0;
a.duration = 1.0;
a.fillMode = kCAFillModeForwards; // Don't reset back to original state
a.removedOnCompletion = NO;
[mask1 addAnimation:a forKey:#"hideMask1"];
You can't use UIView animation to animate CALayers.
Most layer property changes do animation by default (implicit animation). As I recall, shape layer path changes are an exception to that.
You'll need to create a CAAnimation object where the property you are animating is the path on your mask layer.
However, that probably won't give the effect you want. The reason is that when you change a path on a shape layer, Core Animation tries to animate the change in shape of the path. Furthermore, path changes only work properly when the starting and ending paths have the same number and type of control points.
I'm not sure how you'd achieve a cross-fade between 2 different masks without a lot of work.
Off the top of my head, the only way I can think of to do this would be to create a snapshot of the new view appearance with the changed mask (probably using Core Image filters) and then do a cross-fade of a layer that displays that snapshot. Once the crossfade is complete, you would install the new path in your mask layer without animation and then remove the snapshot bitmap, revealing the real view underneath.
There might be a simpler way to achieve what you're after but I don't know what that would be. Maybe one of the CA experts that contributes to SO could chime in here.
I want to animate an object along a path. I got it to work, but the movement is not linear. (it slows down a lot along minor bends in the path.)
Here are the main steps.
1) Import the path from an SVG file using PockeSVG
-(CGMutablePathRef)makeMutablePathFromSVG:(NSString *)svgFileName{
PocketSVG *myVectorDrawing0 = [[PocketSVG alloc] initFromSVGFileNamed:svgFileName];
UIBezierPath *myBezierPath0 = myVectorDrawing0.bezier;
return CGPathCreateMutableCopy([myBezierPath0 CGPath]);
}
2) Create the CAKeyFrameAnimation
CAKeyframeAnimation *moveAlongPath = [CAKeyframeAnimation animationWithKeyPath:#"position"];
CGMutablePathRef animationPath = CGPathCreateMutableCopy(pathForwardTo5);
[moveAlongPath setPath:animationPath];
[moveAlongPath setDuration:1.0f];
moveAlongPath.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
moveAlongPath.delegate = self;
[[trackButton layer] addAnimation:moveAlongPath forKey:#"moveButtonAlongPath"];
CFRelease(animationPath);
I tried using other CAMediaTimingFunctions and they all exhibit this kind of behavior.
Question: The animation itself works, but it doesn't follow a smooth and consistent speed. Any idea why?
The slow downs that you are seeing is due to the default calculation mode where each segment of the animation takes the same time. This means that very short segments are going to move slower.
If you look a the documentation for the path property, you will see how to achieve a constant velocity along the path:
How the animation proceeds along the path is dependent on the value in the calculationMode property. To achieve a smooth, constant velocity animation along the path, set the calculationMode property to kCAAnimationPaced or kCAAnimationCubicPaced.
So, to get a constant pace throughout the animation you should set the calculationMode to kCAAnimationPaced.
I'm drawing an arc by creating a CAShapeLayer and giving it a Bezier path like so:
self.arcLayer = [CAShapeLayer layer];
UIBezierPath *remainingLayerPath = [UIBezierPath bezierPathWithArcCenter:self.center
radius:100
startAngle:DEGREES_TO_RADIANS(135)
endAngle:DEGREES_TO_RADIANS(45)
clockwise:YES];
self.arcLayer.path = remainingLayerPath.CGPath;
self.arcLayer.position = CGPointMake(0,0);
self.arcLayer.fillColor = [UIColor clearColor].CGColor;
self.arcLayer.strokeColor = [UIColor blueColor].CGColor;
self.arcLayer.lineWidth = 15;
This all works well, and I can easily animate the arc from one side to the other. As it stands, this gives a very squared edge to the ends of my lines. Can I round the edges of these line caps with a custom radius, like 3 (one third the line width)? I have played with the lineCap property, but the only real options seem to be completely squared or rounded with a larger corner radius than I want. I also tried the cornerRadius property on the layer, but it didn't seem to have any effect (I assume because the line caps are not treated as actual layer corners).
I can only think of two real options and I'm not excited about either of them. I can come up with a completely custom Bezier path tracing the outside of the arc, complete with my custom rounded edges. I'm concerned however about being able to animate the arc in the same fashion (right now I'm just animating the stroke from 0 to 1). The other option is to leave the end caps square and mask the corners, but my understanding is that masking is relatively expensive, and I'm planning on doing some fairly intensive animations with this view.
Any suggestions would be helpful. Thanks in advance.
I ended up solving this by creating two completely separate layers, one for the left end cap and one for the right end cap. Here's the right end cap example:
self.rightEndCapLayer = [CAShapeLayer layer];
CGRect rightCapRect = CGRectMake(remainingLayerPath.currentPoint.x, remainingLayerPath.currentPoint.y, 0, 0);
rightCapRect = CGRectInset(rightCapRect, self.arcWidth / -2, -1 * endCapRadius);
self.rightEndCapLayer.frame = rightCapRect;
self.rightEndCapLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.rightEndCapLayer.bounds
byRoundingCorners:UIRectCornerBottomLeft | UIRectCornerBottomRight
cornerRadii:CGSizeMake(endCapRadius, endCapRadius)].CGPath;
self.rightEndCapLayer.fillColor = self.remainingColor.CGColor;
// Rotate the end cap
self.rightEndCapLayer.anchorPoint = CGPointMake(.5, 0);
self.rightEndCapLayer.transform = CATransform3DMakeRotation(DEGREES_TO_RADIANS(45), 0.0, 0.0, 1.0);
[self.layer addSublayer:self.rightEndCapLayer];
Using the bezier path's current point saves from doing a lot of math to calculate where the end point should appear. Moving the anchoring point also allows the layers to not overlap, which is important if your arc is at all transparent.
This still isn't entirely ideal, as animations have to be chained through multiple layers. It's better than the alternatives I could come up with though.
Ok, I figured out how to do this based on various posts here on SO, and it works great. I'm working on an overlay which will basically mask the whole window except for a small region. This is for drawing attention to a specific area of my app. I'm using a bunch of calls to moveToPoint: and addLineToPoint: like so (this is in my CALayer subclass' drawInContext:):
....
// inner path (CW)
[holePath moveToPoint:CGPointMake(x, y)];
[holePath addLineToPoint:CGPointMake(x + w, y)];
[holePath addLineToPoint:CGPointMake(x + w, y + h)];
[holePath addLineToPoint:CGPointMake(x, y+h)];
// outer path (CCW)
[holePath moveToPoint:CGPointMake(xBounds, yBounds)];
[holePath addLineToPoint:CGPointMake(xBounds, yBounds + hBounds)];
[holePath addLineToPoint:CGPointMake(xBounds + wBounds, yBounds + hBounds)];
[holePath addLineToPoint:CGPointMake(xBounds + wBounds, yBounds)];
// put the path in the context
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 0, 0);
CGContextAddPath(ctx, holePath.CGPath);
CGContextClosePath(ctx);
// set the color
CGContextSetFillColorWithColor(ctx, self.overlayColor.CGColor);
// draw the overlay
CGContextDrawPath(ctx, kCGPathFillStroke);
(holePath is an instance of UIBezierPath.)
So far so good. The next step is animation. In order to do this (I also found this technique here on SO) I made a method as follows
-(CABasicAnimation *)makeAnimationForKey:(NSString *)key {
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:key];
anim.fromValue = [[self presentationLayer] valueForKey:key];
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
anim.duration = 0.5;
return anim;
}
and overrode actionForKey:, initWithLayer: and needsDisplayForKey: (returning the result of makeAnimationForKey: in actionForKey:. Now, I get a nice "hole layer", which has a property holeRect which is animatable using implicit CAAnimations! Unfortunately, it's SUPER choppy. I get something like 2 or 3 frames per second. I thought that perhaps the problem was the background, and tried replacing it with a snapshot, but no dice. Then, I used Instruments to profile, and discovered that the HUGE hog here is the call to CGContextDrawPath().
tl;dr I guess my question comes down to this: is there a simpler way to create this layer with a hole in it which will redraw faster? My hunch is that, if I could simplify the path I'm using, drawing the path would be lighter. Or possibly masking? Please help!
Ok, I tried phix23's suggestion, and it totally did the trick! At first, I was subclassing CALayer and adding a CAShapeLayer as a sublayer, but I couldn't get it to work properly and I'm pretty tired at this point, so I gave up and just replaced my subclass completely with CAShapeLayer! I used the above code in its own method, returning the UIBezierPath, and animated like so:
UIBezierPath* oldPath = [self pathForHoleRect:self.holeRect];
UIBezierPath* newPath = [self pathForHoleRect:rectToHighlight];
self.holeRect = rectToHighlight;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.duration = 0.5;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.fromValue = (id)oldPath.CGPath;
animation.toValue = (id)newPath.CGPath;
[self.holeLayer addAnimation:animation forKey:#"animatePath"];
self.holeLayer.path = newPath.CGPath;
Interesting side note -- path is NOT implicitly animatable. I guess that makes sense.
Redrawing is expensive, animation is not. When you set the value for "holeRect" you are instructing the entire layer to redraw. THIS IS NOT THE SAME AS A TYPICAL PERFORMANCE OPTIMIZED PROPERTY ANIMATION. If the layer is the size of the entire screen, then you are effectively recreating a new version of the layer on every frame. This is one of the things that apple does to ensure smooth animations. You want to prevent redraws as much as possible.
I would suggest creating a layer with the hole in the center, and make sure that the layer is large enough to cover the entire screen no matter where the hole is centered. Then the animation should animate the "position" property of the layer. While it may seem wasteful to have this massive layer of which only ~ 30% will ever be used at once, it is much less wasteful then redrawing on every frame. If you would like to maintain your "holeRect" interface you can, but the layer with "holeRect" property should contain a sublayer or sublayers that are animated in the way that I described (in layoutSubviews, not drawInContext:).
In summary, make sure you are animating position, opacity, transform as these are among the most efficient animations on layers, and redraw only when necessary.
In your example you use a rectangular hole. Implementing such animation could be more efficient if you use four rectangular layers (see attached image). To animate the hole rectangle position you'll have to animate the blue layers' widths, and red layers' heights. (if the hole rectangle can change width, the red layers' widths will also have to be animated)
If you need a non-rectangular hole, you could place another layer in the middle of these, which has the hole inside and change only the position of this layer (see the 2nd image). Resizing this non-rectangular hole will result in recreating only the middle layer's contents, so it should be a bit faster than in your original case where this layer was of the size of the screen if I understood correctly.
I am posting an anwser because I am not able to comment on your question (yet) due to reputation.
My question is whether this has to be created programmatically? If it is just about creating an attention area you could use another approach.
Why not just use a black png with a transparent hole in it? Then you do not have performance issues during animation and to a certain extent if you choose your original hole size well, you could even resize it. The image has to be just big enough so that it covers every part of the view indenpendently of the current position of the hole. The part outside the hole could also include transparency resulting in a shadow effect outside the attention area.