Smoothly animated hole in CALayer? - ios

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.

Related

CABasicAnimation to path strange behaviour [duplicate]

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.

Draw gradient below a CAShapeLayer graph

I'm drawing a graph using a CGPath applied to a CAShapeLayer. The graph itself is drawn just fine, but I want to add a gradient underneath it afterwards. My problem is that the path is closed with a straight line going from the last point to the first point (see below) – this would make a gradient fill look totally ridiculous.
As far as I can see, the only way to circumvent this issue is to draw two additional lines: one from the last point of the graph to the bottom-right corner, and from there, another one to the bottom-left corner. This would close the path off nicely, but it would add a bottom line to the graph, which I don't want.
If I were using CGContext, I could easily solve this by changing the stroke color to transparent for the last two lines. However, with the code below, I don't see how that would be possible.
CGMutablePathRef graphPath = CGPathCreateMutable();
for (NSUInteger i = 0; i < self.coordinates.count; i++) {
CGPoint coordinate = [self.coordinates[i] CGPointValue];
if (!i) {
CGPathMoveToPoint(graphPath, NULL, coordinate.x, coordinate.y);
} else {
CGPathAddLineToPoint(graphPath, NULL, coordinate.x, coordinate.y);
}
}
CAShapeLayer *graphLayer = [CAShapeLayer new];
graphLayer.path = graphPath;
graphLayer.strokeColor = [UIColor whiteColor].CGColor;
graphLayer.fillColor = [UIColor redColor].CGColor;
[self.layer addSublayer:graphLayer];
I hope you guys can help me out!
Update: You suggest that I could create a CAGradientLayer, and then apply the graph layer as its mask. I don't see how that would work, though, when the graph/path looks the way it does. I have replaced the image above with another graph that hopefully illustrates the problem better (note that I've given the CAShapeLayer a red fill). As I see it, if I were to apply above layer as the mask of a CAGradientLayer, some of the gradient would lie above the graph, some it below. What I want is for all of the gradient to be placed right beneath the graph.
Maybe I'm not understanding the problem correctly, but if you're looking to add a consistent gradient, couldn't you create a gradient layer and then make your graphLayer be that layer's mask?
Figure out whatever the min max bounds of your coordinates, create a CAGradientLayer that size, configure it however you might like and then, apply your graphLayer as it's mask. Then add the new CAGradientLayer to your self.layer.
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
// ... Configure the gradientLayer colors / locations / size / etc...
gradientLayer.mask = graphLayer;
[self.layer addSubLayer:gradientLayer];
This doesn't take into account the stroke, but it shouldn't be difficult to apply that as well if that's important.
I solved the problem by creating two separate paths: One for the graph (as shown in my original post), and one that starts in the lower-right corner, moves in a straight line to the lower-left corner, and from there follows the same path as the graph. By doing so, the path gets closed off nicely, since the graph ends at the same x-coordinate as where the path started.
From there, I applied the second path to a CAShapeLayer, and then used this layer as the mask of a gradient layer.

core gore graphics and alpha values

I am draw some circle on a map with
CGContextFillEllipseInRect
This works great, but the only issue is that I have the alpha value set to .5
If tow or more circle overlap the the overlap area is darker because of the two alpha values being added I suppose. Is there an easy way to maintain the same alpha value regardless if the circle overlap?
Thanks
If you're working with closed shapes and drawing them all in the same drawrect (or at least in the same context) you can accomplish what you want by using UIBezierPath.
// create a path with a circle
UIBezierPath* path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(50, 50) radius:50.0f startAngle:0 endAngle:M_PI * 2.0f clockwise:YES];
// add another circle to the path
[path appendPath:[UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 100) radius:50.0f startAngle:0 endAngle:M_PI * 2.0f clockwise:YES]];
// draw them both, since they are in a single path they will be treated as a single shape
[path fill];
Draw the circles at full opacity, and set the opacity of the view/layer to 0.5.
EDIT:
To clarify, I assume you know how to stop drawing your circles at half opacity, since you managed to set it up that way in the first place. So make the opacity of your circles be 1.0. Then, set your view's opacity to 0.5 by doing the following:
view.alpha = 0.5;
I'm assuming you have a UIView subclass, so this can either be in the place in your code where you create the view or it could be inside the subclass in your init method for it. So if you're doing it from within the UIView subclass, it would be self instead of view:
self.alpha = 0.5;
Also, this will make all of the drawing you do in this view be 0.5 opacity. If you want to have some content that's full opacity and some that's half opacity, the simplest solution I can give you is to have two different views, one for the half opacity content, the other for the full opacity content. There are better ways to accomplish this, but for the sake of communicating easily a solution, this is probably easiest.

Can I add a custom line cap to a UIBezierPath?

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.

Animating CAShapeLayer with moving frame

Using core animation layers, I've been trying to implement the following feature. Within a containing superlayer, there are two anchor layers, and another layer that connects the two. The following image should make the situation clear. On the left, the two orange-coloured anchors are marked 'A' and 'B', and the green line connects them. The enclosing layer frames are shown using dotted lines. On the right, the layer hierarchy is shown as I've currently implemented it, where both the anchors and the connection are sublayer of the enclosing superlayer.
Now, what I'm trying to do is allow the anchors to be moved around, and have the connection stay attached. I'm currently updating its frame and path properties exploiting the connection layer's -setFrame: method , using the code shown below:
- (void)setFrame:(CGRect)frame {
CGSize size = frame.size;
CGPoint startPoint = CGPointZero;
if (size.height < 0.0) startPoint.y -= size.height;
CGPathRef oldPath = self.path;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, startPoint.x, startPoint.y);
CGPathAddLineToPoint(path, NULL, startPoint.x + size.width, startPoint.y + size.height);
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.duration = [CATransaction animationDuration];
animation.timingFunction = [CATransaction animationTimingFunction];
animation.fromValue = (id)oldPath;
animation.toValue = (id)path;
[self addAnimation:animation forKey:#"pathAnimation"];
self.path = path;
CGPathRelease(path);
[super setFrame:frame];
}
Now, this sort of works, but the problem is that the frame (or position + bounds) animation doesn't run in sync with the path animation, causing some jittery effects where the connection's far end momentarily detaches (and some other minor issues to, presumably caused by the same core issue).
I've been toying around with the issue, but only to moderate success. At one point, I set the connection's frame equal to that of the enclosing superlayer, which did have the desired effect (since now the frame doesn't need to be animated any longer). However, I'm worried about the performance of this solution in a context with multiple connections—ie. multiple non-opaque, large-size overlapping layers seems bad?
Would anyone have a better, more elegant solution to this? Thanks!
Since you're only really stretching (scaling) and rotating the connection layer have you considered applying transformations to it instead of manually modifying the frame?
You should be able to calculate the angle of rotation and scaling factor using some basic trigonometry based on the positions of the anchor layers.
Since you are animating the path and not the frame of the layer or path, you will get performance issues after approx. 50 simultaneous animations.
You will achieve high performance only when manipulating the layers implicit animatable properties, therefore its frame and not its content (e.g path), because of gpu acceleration.

Resources