Search code examples
iosobjective-ccore-animationuibezierpath

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.


Solution

  • 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.