I want to be able to draw irregular polygons with a mixure of acute and obtuse angles, with some of the corners rounded, and some not.
Say I have a polygon like this:
And I want to draw it with all of it's corners rounded:
Or every other corner rounded:
How do I do that?
David Rönnqvist wrote an excellent, very informative article describing the math behind drawing rounded corners, but it is quite complex, and will cause your brain to explode if you're not comfortable with triginometry and geometry.
In that same thread Anjali posted an answer that shows how to draw a triangle with rounded corners much more simply than David's math-intensive approach, using the CGMutablePath
method addArc(tangent1End:tangent2End:radius:transform:)
However, it doesn't tell me how to handle polygons with variable numbers of vertexes, or how to mix rounded and sharp corners. How do I do that?
The key to doing this without driving yourself insane is the method addArc(tangent1End:tangent2End:radius:transform:)
. That adds an arc to an existing CGMutablePath
. The method starts from the path's current point. You specify a point, tangent1End
, which is the vertex you want to draw a rounded corner for, and another point, tangent2End
, which is the next vertex in your pologon.
To draw a polygon with a variable number of vertexes, we use an array of points.
In order to get all the corners rounded, you have to set the path's starting point to a point somewhere on one of your polygon's straight segments, and then return to that point at the end. It's easy to calculate the midpoint of 2 points:
let midpoint = CGPoint(x: (point1.x + point2.x)/2, y: (point1.y + point2.y)/2 )
So we'll move the path's current point to the midpoint between the first and last points in our array of vertexes as the starting point:
let midpoint = CGPoint(x: (first.point.x + last.point.x) / 2, y: (first.point.y + last.point.y) / 2 )
path.move(to: midpoint)
Then, for each vertex in the polygon, we will either just draw a line to that point (for a sharp corner) or use the wonderful, easy-to-use addArc(tangent1End:tangent2End:radius:transform:)
to draw a line segment, ending with an arc, around that vertex.
I'll create a subclass of UIView, RoundedCornerPolygonView.
class RoundedCornerPolygonView: UIView {
}
It will set itself up to have it's content layer be a CAShapeLayer. To do that you just add a class var layerClass
to your custom UIView
subclass:
// This class var causes the view's base layer to be a CAShapeLayer.
class override var layerClass: AnyClass {
return CAShapeLayer.self
}
In order to keep track of an array of points, and which points should be rounded and which should be smooth, we'll define a struct PolygonPoint
:
struct PolygonPoint {
let point: CGPoint
let isRounded: Bool
}
We'll give our class an array of PolygonPoint
s:
public var points = [PolygonPoint]()
And add a didSet method to update our shape layer's path if it changes:
public var points = [PolygonPoint]() {
didSet {
guard points.count >= 3 else {
print("Polygons must have at least 3 sides.")
return
}
buildPolygon()
}
}
Here's the code to build our polygon path from the array of PolygonPoint
s above:
/// Rebuild our polygon's path and install it into our shape layer.
private func buildPolygon() {
guard points.count >= 3 else { return }
drawPoints() // Draw each vertex into another layer if requested.
let first = points.first!
let last = points.last!
let path = CGMutablePath()
// Start at the midpoint between the first and last vertex in our polygon
// (Since that will always be in the middle of a straight line segment.)
let midpoint = CGPoint(x: (first.point.x + last.point.x) / 2, y: (first.point.y + last.point.y) / 2 )
path.move(to: midpoint)
//Loop through the points in our polygon.
for (index, point) in points.enumerated() {
// If this vertex is not rounded, just draw a line to it.
if !point.isRounded {
path.addLine(to: point.point)
} else {
//Draw an arc from the previous vertex (the current point), around this vertex, and pointing to the next vertex.
let nextIndex = (index+1) % points.count
let nextPoint = points[nextIndex]
path.addArc(tangent1End: point.point, tangent2End: nextPoint.point, radius: cornerRadius)
}
}
// Close the path by drawing a line from the last vertex/corner to the midpoint between the last and first point
path.addLine(to: midpoint)
// install the path into our (shape) layer
let layer = self.layer as! CAShapeLayer
layer.path = path
}
I've created a sample project on Github that implements the RoundedCornerPolygonView
class defined above, and lets you select which corners should be rounded or smooth at runtime.
The project is called "RoundedCornerPolygon". (link)
It also has code in it's view controller class that starts from an array of vertexes (CGPoint
s). It populates a vertical stack view with a switch for each vertex in the array of vertexes, plus a label for that vertex. It then builds an array of PolygonPoint
s and installs it into the RoundedCornerPolygonView
.
If the user toggles any of the switches it rebuilds the array of PolygonPoint
s and passes them to the RoundedCornerPolygonView
, which redraws itself.
The screen looks like this: