The Shape of Things to Come

Creating and animating rounded shapes with AndroidX, Part I

Chet Haase
Published in
13 min readApr 19, 2023

--

The new graphics-shapes library allows easy creation and editing of complex, rounded polygonal shapes

A new library with the flashy name :graphics:graphics-shapes: launched recently in AndroidX. I’m happy (especially after many months of working on it with my colleague Sergio Sancho from the Android Wear team) that this project and API is finally out there. But I thought it might be helpful to describe what it is and how to actually use it.

There are two major parts to the library, which internally we call Shapes and Morph. For the benefit of short-attentions-span Medium readership, I will therefore break the description of the library down into two articles along similar lines. This first one will cover the Shapes portion of the library, which allows easy creation and rendering of rounded polygonal shapes. The second article shows how to animate (a.k.a, “Morph”) those shapes.

Android offers a very flexible drawing API. In a custom View, you can override onDraw() and use the Canvas parameter to draw anything from lines to circles to rectangles, to complex Path objects. And if you want something rounded, you can create and draw any shape you want… as long you want a RoundRect.

Of course, you can always (if you’re up for the effort) create a very complex shape (complete with arbitrary rounding) with the Path API. But out of the box, we give you only Canvas.drawRoundRect(). Moreover, Android offers very little flexibility in terms of how those rects are rounded. That is, each of the corners of the rectangles are rounded with a circular curve… period. So if you want something more custom (either in the shape of the rounded corners or the number of vertices), you are on your own.

Until now.

We thought it would be useful to provide simple creation of all kinds of rounded shapes. I mean, rectangles are cool and all. And so are those circular corners, right? They’re so… circular! But sometimes you want just a little more. Or even a lot more.

I don’t know why you would create a shape like this. But isn’t it nice that you can?

We also wanted these shapes to be available not just for apps running on future platform versions, but also across much older releases, for the enjoyment of all developers and users. So we created an API to do just that, by using Path objects internally. Path has been available since 1.0 and thus offers compatibility back as far as AndroidX itself goes.

The API for creating and drawing these shapes is simple (we saved all the complicated bits for the internal code which creates them). There are just a couple of different pieces to understand: creating a polygonal shape and specifying optional rounding parameters for the shape’s corners. I’ll cover these below.

Aside: Polygons

Q: What is a Polygon?

A: There are so many sides to that question…

It’s worth talking a little bit first about what we mean by “polygon.” In particular, it’s worth explaining what we mean when we use of the term, to show how we get to the much more complex (and interesting) shapes enabled by this library.

Wikipedia defines Polygon thusly:

In geometry, a polygon (/ˈpɒlɪɡɒn/) is a plane figure made up of line segments connected to form a closed polygonal chain.

which I find… not terribly helpful. I think mathematicians enjoy math so much that even when they’re writing words, it still sounds like equations. Let’s simplify this definition for the non mathematicians in the audience.

In its most basic form, a polygon is a 2D shape with edges and vertices (or “corners”). I usually think of polygons as having vertices that are ordered around some center, with all edges having the same length. Polygons can be much more complex than that, however, including shapes that can be self-intersecting.

Our library’s polygons are, however, a bit more staid and boring, with vertices that are positioned equidistant from some center, marching around in order. All sides are of equal length and there are no funky self-intersections. (This constraint ends up being important in being able to handle automatic morphing between our shapes with reasonable results). You can think of the base RoundedPolygon object (which we will see in more detail below) as being a shape that has a center around which all of its vertices are positioned at some given radius away from that center.

The library’s polygons can be thought of as a set of equidistant, ordered vertices which lie ‘radius’ distance from some center.

Our polygons can be a bit more complex as well. For one thing, the library has the concept of a “star polygon.” Star polygons are similar to polygons, except they have both an inner and outer radius, with vertices lying on one or the other, taking turns as the outline proceeds around the center.

Finally, our polygons have the concept of “rounding.” Rounding is not strictly a polygonal concept, since mathematical polygons are defined to have straight edges and sharp corners. So we call our shapes “Rounded Polygons,” as a blend of the general concepts of polygons with the additional nuance of optionally rounded corners. Rounded polygons have a similar geometry as the shapes above, except that each vertex has optional parameters which describe how to round its corner. For example, here is a 5-sided star polygon, like the one above, but with rounding specified for corners formed by the vertices on its outer radius.

Another star polygon, except this time with rounded corners on the outer radius

These, then, are the types of shapes that this library will produce: polygonal (ish) non-self-intersecting shapes where the vertices are ordered and equidistant from a radius (or two), with optionally rounded corners.

Now let’s look at how to use the library’s API to create those shapes.

Polygon, Stars, and More

Note: This article is current as of the alpha02 release. There will probably be minor API changes during the alpha phase; I will update the article when the API changes, and will update this release note accordingly.

The main class used to create a shape is RoundedPolygon. There are many different shapes you can create with this API, but all of them boil down to polygonal variations.

The way that you create a simple, unrounded*RoundedPolygon is by telling the API how many vertices you want and optionally providing a radius and center. Of course, any shape will have a radius and center, but by default the library creates canonical shapes with a radius of 1 around a center at (0, 0). Note that you can transform (scale, translate, rotate) the canonical shape to get it to the size and location you want by calling RoundedPolygon.transform(Matrix).

* At this point, you might be wondering why we have an API named “Rounded” which allows you to create an unrounded thing. The original version of the API handled that semantic difference, with a Polygon superclass and a RoundedPolygon subclass. But in the end, it was all a bit academic to split this functionality based on the meaning of the word “polygon,” so we went with a single class instead which handles all possibilities.
API naming is hard, imperfect, and a perpetual source of regret.

The simplest use of the API involves passing in the number of vertices and letting the library do its thing. You can then call the transform() function to resize and position the object and finally draw it into your custom view with an extension method provided by the library.

Here’s an example which creates a five-sided figure with a radius of 200 and draws it with a given Canvas and Paint object (created elsewhere):

val pentagon = RoundedPolygon(5, 200f)
canvas.drawPolygon(pentagon, paint)

Star polygons (discussed earlier) are nearly as simple; the only extra thing needed is a second radius, which is provided via the innerRadius parameter in the Star() function. This inner radius is a value ranging from 0 to the value of radius (which is the “outer” radius for the shape).

For example, to create a five-sided star polygon with a radius of 100 and an inner radius halfway between the outer radius and the center, you would do this:

val pentagonalStar = Star(5, 100f, 50f)
A Star shape created with 5 vertices and an inner radius half the value of the main radius

Rounding Error

So all of this is nice. We’ve provided a simple API to create and draw regular and star polygons. But these are not the hard parts in this problem space; it’s not too difficult to create straight-edged, sharp-corner shapes like these with the existing APIs. The interesting (and tricky) part is how to round those corners.

Figuring this out meant (in my case) re-learning a bunch of high-school level geometry and trigonometry (hey, it had been… a long time since I had those classes). Thing like trig identities, the Law of Cosines, and handy geometry facts like the angles of a triangle adding up to 180 degrees all came into play. Maybe I’ll write up that stuff sometime (or you can just look at the code and see where it ended up).

But the key part (for users of the library) is: how do you use the API to get nice, rounded shapes? Fortunately, the API (like so many APIs) is much simpler than the implementation. Creating a polygon, or star polygon, with rounded corners is nothing more than creating those shapes with the APIs described above, with additional information about how the corners should be rounded.

To accomplish this task, use the classCornerRounding which is responsible for determining how the corners should be rounded. It takes two parameters: radius and smoothing.

Rounding Radius

radius is the radius of the circle used to round a vertex. This is similar to the radius parameters supplied to the existing drawRoundRect method of Canvas, except it works in concert with the optional smoothing parameter (see below). For example, we can create this rounded triangle:

where the rounding radius r for each of the corners can be pictured geometrically as follows:

The rounding radius r determines the circular rounding size of rounded corners

Note that a rounding radius produces a purely circular curve on the corner, between the two straight edges that meet at the vertex.

Smooth Moves

“Smoothing,” unlike the corner rounding radius, is a new concept in our APIs. You can think of smoothing as a factor which determines how long it takes to get from the circular rounding portion of the corner to the edge. A smoothing factor of 0 (unsmoothed, the default value for CornerRounding) results in purely circular corner rounding (if a nonzero radius is specified, as above). A nonzero smoothing factor (up to the max of 1.0) results in the corner being rounded by three separate curves. The center curve is the same circular arc produced by the rounding radius, explained above. But instead of that curve coming all the way to the polygon edges, there are now two “flanking” curves, which transition from the inner circular curve to the outer edges in smooth (non-circular) arcs of their own.

The magnitude of the smoothing curve determines both the length of the inner circular curve (more smoothing == smaller circular curve) and the length of the flanking curves (more smoothing == larger flanking curves). The flanking curves affect not only how much of the rounding happens on a circular path, but also the distance of the overall rounding curve. A larger smoothing factor pushes the intersection point of the rounding curve further along the edge toward the next vertex. A value of 1 (the max) results in no inner curve at all (the circular portion has length zero) and the maximum length of the flanking curves (which can extend as far as the next vertex, depending on the rounding parameters of that vertex).

To illustrate the impact of smoothing, it is helpful to look at a diagram showing the underlying curves. Note that all polygons are represented internally by a list of Bézier cubic curves, which are each defined by pairs of anchor and control points. These cubic curves are then used internally to create the Path objects responsible for drawing the shapes into the custom view.

Note: Describing cubic curves is beyond the scope of this already long article, but fortunately there is plenty of information out there about Bézier curves,** cubic curves, Paths, and more. I invite you to do background reading there and elsewhere if you aren’t familiar with any these concepts. But here’s a very simple description in case it helps: a cubic Bézier curve can be defined by two anchor points (one at each end of the curve) and two control points which determine the slope of the curve at those anchor points.

** I have to give a shout out to the that Bezier curve primer site linked here; it’s a vast treasure trove of information about all things Bézier, with proofs, equations, sample code, diagrams, live embedded demos, and thorough explanations. I return to it often to understand more in this complex and interesting space.

Let’s look at some pictures to see what’s going on with the underlying curves. In the diagram below, the corner of the shape (the white object on the left) is represented on the right by the green line (the outline of the shape) and the white dashed line (a circle with the given rounding radius). The cubic curve is represented by pink circles that are anchor points, yellow circles that are control points for the curve, and yellow lines between the anchor and control points. If you’ve used drawing programs such as Adobe Illustrator, or even Keynote, you may have seen similar handle visuals when drawing curves.

A smoothing factor of 0 (unsmoothed) produces a single cubic curve which follows a circle around the corner with the specified rounding radius, as in the earlier example.

When we supply a non-zero smoothing factor, the rounded corner is created with three cubic curves: the circular curve (which we saw above in the unsmoothed case) plus two flanking curves which transition between the inner circular curve and the edges. Note that the flanking curves start further back along the edge than in the unsmoothed case. The resulting shape is shown by the white object on the right. The effects of smoothing can be quite subtle, but they allow much more flexibility for designers in producing smoothed shapes that go beyond the traditional circular-round shapes.

A nonzero smoothing factor produces three cubic curves to round the vertex: the inner circular curve (as before) plus two flanking curves that transition between the inner curve and the polygon edges.

I should note that although there are many separate segments which make up each rounded corner (two edges, two flanking curves, and one inner circular curve), the result is very, er, smooth because each curve is calculated to match the slope at the point where it joins the next segment. Thus, for example, the rounded corner smoothly transitions from the inner circular curve to the non-circular smoothing curve, and then again to the straight edge.

One More Thing

Besides the constructors covered above, which all take the number of vertices, there is also a more general constructor which takes a list of vertices:

    constructor(
vertices: List<PointF>,
rounding: CornerRounding = CornerRounding.Unrounded,
perVertexRounding: List<CornerRounding>? = null,
center: PointF? = null
)

This constructor makes it possible for you to create shapes that… do not work well with the rest of our rounded-polygon assumptions. So don’t be surprised if you throw randomly complex lists of vertices at it and the results are not as pleasing as the more constrained polygons created by the other constructors.

This constructor exists to allow creation of more interesting polygonal shapes whose vertices are not all equidistant from some center point. For example, we can use the vertex-list constructor to create a triangle shape where the bottom edge bows in.

This shape is mostly… but not completely a regular triangle. We need to use the constructor which takes a list of vertices to capture that bottom edge shape.

This triangle-ish shape is created with this code.

        val triangleInnerRadiusRatio = .1f
val trianglePoints = listOf(
radialToCartesian(1f, 270f.toRadians()),
radialToCartesian(1f, 30f.toRadians()),
radialToCartesian(triangleInnerRadiusRatio, 90f.toRadians()),
radialToCartesian(1f, 150f.toRadians()),
)
RoundedPolygon(trianglePoints, CornerRounding(.22f))

(Don’t worry about the radialToCartesian function above — or check it out in the sample project listed below if you are curious. It’s just a function that simplifies placing vertices around a center point at specific angles).

And So On

I talked specifically about a single CornerRounding parameter above, but the API allows you to specify multiple rounding parameters, including one for the inner and outer radii to get an effect like this on star polygons.

Inner vertices can use different rounding than outer vertices. Here the outer vertices are unrounded.

You can also, if you want to take it that far, define per-vertex rounding parameters, to get a very custom shape indeed. For any of these situations, the API allows you to easily create and draw all kinds of rounded (or unrounded) polygonal shapes. You could always do this on Android, of course. After all, we are just using the existingPath API underneath to handle the drawing. But there are a lot of details (and so much math!) to sort out along the way. This new library makes the job, we hope, far easier.

A sample of shapes that can be created with this library . This screenshot was taken from the Github sample described at the end of this article.

Next Steps

One of the things that drove the internal structure using cubic curves was the need to not just create and draw these shapes, but to also animate smoothly and automatically between them. Check out the next article, Shape Morphing in Android, to see how to do that with this library.

See Also

APIs!

The library is available in alpha form on AndroidX:

Sample code!

The shape editing animation in the header was created with a sample app which demonstrates shape creation, editing, and morphing. It is hosted on GitHub:

The sample has both Compose and View apps, showing how to use the library to create and morph shapes in both kinds of UI toolkits. The Compose version has an additional editor view that helps visualize the various shape parameters.

--

--