Use Bezier Path as Clipping Mask - ios

I am wondering if it is possible to clip a view to a Bezier Path. What I mean is that I want to be able to see the view only in the region within the closed Bezier Path. The reason for this is that I have the outline of an irregular shape, and I want to fill in the shape gradually with a solid color from top to bottom. If I could make it so that a certain view is only visible within the path then I could simply create a UIView of the color I want and then change the y coordinate of its frame as I please, effectively filling in the shape. If anyone has any better ideas for how to implement this that would be greatly appreciated. For the record the filling of the shape will match the y value of the users finger, so it can't be a continuous animation. Thanks.
Update (a very long time later):
I tried your answer, Rob, and it works great except for one thing. My intention was to move the view being masked while the mask remains in the same place on screen. This is so that I can give the impression of the mask being "filled up" by the view. The problem is that with the code I have written based on your answer, when I move the view the mask moves with it. I understand that that is to be expected because all I did was add it as the mask of the view so it stands to reason that it will move if the thing it's tied to moves. I tried adding the mask as a sublayer of the superview so that it stays put, but that had very weird results. Here is my code:
self.test = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
self.test.backgroundColor = [UIColor greenColor];
[self.view addSubview:self.test];
UIBezierPath *myClippingPath = [UIBezierPath bezierPath];
[myClippingPath moveToPoint:CGPointMake(100, 100)];
[myClippingPath addCurveToPoint:CGPointMake(200, 200) controlPoint1:CGPointMake(self.screenWidth, 0) controlPoint2:CGPointMake(self.screenWidth, 50)];
[myClippingPath closePath];
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = myClippingPath.CGPath;
self.test.layer.mask = mask;
CGRect firstFrame = self.test.frame;
firstFrame.origin.x += 100;
[UIView animateWithDuration:3 animations:^{
self.test.frame = firstFrame;
}];
Thanks for the help already.

You can do this easily by setting your view's layer mask to a CAShapeLayer.
UIBezierPath *myClippingPath = ...
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = myClippingPath.CGPath;
myView.layer.mask = mask;
You will need to add the QuartzCore framework to your target if you haven't already.
In Swift ...
let yourCarefullyDrawnPath = UIBezierPath( .. blah blah
let maskForYourPath = CAShapeLayer()
maskForYourPath.path = carefullyRoundedBox.CGPath
layer.mask = maskForYourPath

Just an example of Rob's solution, there's a UIWebView sitting as a subview of a UIView called smoothView. smoothView uses bezierPathWithRoundedRect to make a rounded gray background (notice on right). That works fine.
But if smoothView has only normal clip-subviews, you get this:
If you do what Rob says, you get the rounded corners in smoothView and all subviews ...
Great stuff.

Related

Animate CALayer mask change with fade effect

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.

iOS layer position

I want to draw a layer on a view. for that i write this code.
arcLayer=[CAShapeLayer layer];
arcLayer.frame=self.circleView.frame;
arcLayer.backgroundColor = [UIColor purpleColor].CGColor;
[self.circleView.layer addSublayer:arcLayer];
but this the position of layer is not exact on the view as you can see in the following image. the green one is circleView and the purple one is arc layer.
You have a problem on this line: arcLayer.frame=self.circleView.frame;. Change that line to:
CGRect frame = self.circleView.frame;
frame.origin = CGPointZero;
arcLayer.frame=frame;
This will solve your problem. You just forgot to set origin of your layer to CGPointZero or whatever position.
Good Luck!
More concisely:
arcLayer.frame = self.circleView.bounds;

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.

Clipping a UIView with a bezier path. BezierPath seems not to be applied completely

I have to add a border with a wooden texture to a UIView. In order to do this I thought the following solution:
I have the original UIView (say uiViewA)
I create another UIView (say uiViewB) with the same size of the first one
I create a bezier path with a 8px width
Once created the path I apply this to uiViewB
I add uiViewB to uiViewA's subviews
The code to apply the bezier path is the following:
UIView* uiViewB = [[UIView alloc] initWithFrame:uiViewA.bounds];
UIImage* wood = [UIImage imageNamed:#"texture_wood"];
[uiViewB setBackgroundColor:[UIColor colorWithPatternImage:wood]];
// creation of the bezier path
UIBezierPath* borderPath = ... ;
[borderPath setLineWidth:8.0];
[borderPath moveToPoint:CGPointMake(0.0, 0.0)];
borderPath addLineToPoint:...
borderPath addArcWithCenter:...
borderPath addLineToPoint:...
borderPath addArcWithCenter:...
borderPath addLineToPoint:...
CAShapeLayer* borderMaskLayer = [CAShapeLayer layer];
[borderMaskLayer setFrame:uiViewA.bounds];
borderMaskLayer.path = [borderPath CGPath];
uiViewB.layer.mask = borderMaskLayer;
[uiViewB.layer setMasksToBounds:YES];
I would like to obtain the following result:
But the result I obtain is the following:
Do you have any idea of why bezier path seems not to be applied in the right way?
From the code you have there it looks like you are using the default fill and stroke of the shape layer that is used as a mask, thus the path is filled and you get the result that you are seeing.
What you want to do is set a clear fill color and set some random opaque stroke color. Then you will have to set an appropriate line width, line cap, line round, etc. to configure how the stroke looks.

Why is making two corners round slower than make all corners round?

Edit: Language updated to improve readability.
I made an image view with 2 rounded corners like this:
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.photoImageView.bounds byRoundingCorners:UIRectCornerBottomLeft|UIRectCornerBottomRight cornerRadii:CGSizeMake(10, 10)];
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = maskPath.CGPath;
self.photoImageView.layer.mask = maskLayer;
But it is slower than making all the corners round using this code.
self.photoImageView.layer.cornerRadius = 10;
Would anyone know what why and how I can improve my '2 corner' code please?
Your code is adding another stage to the drawing. Normally, the background is drawn (with the given cornerRadius) directly to the target, but with a mask specified, it is drawn to a temporary surface, then copied to the target using the mask.
There isn't any built-in functionality for only rounding some background corners in the standard CALayer object.
I do wonder how slow "slower" really is; is this premature optimisation?

Resources