Create beautiful shapes with MaterialShapeDrawable

The Material Design 2.0 comes with couple of new and interesting concepts. One of them is the emphasis on shapes of your components. There is one way to create very nice complex shapes in a very simple way which is called MaterialShapeDrawable. Is it useful? Is it easy to use enough? Let’s find out.

When we want to include graphic elements into our Andoid app we have couple of options. We can of course import bitmap images in formats like webp or png. We can use Android vector drawables and use them in any size we want. Another way to include simple graphic has always been using the ShapeDrawable. Shapes are the simplest way to add some simple backgrounds and maybe also some simple icons. Drawables created using ShapeDrawables are always display density independent, they can be defined using XML resources even as a part of another drawables (e.g. the StateListDrawables) and are available since API version 1.

We can specify a few shape’s properties to modify it’s appearance. These properties include the base shape, fill color (or gradient), stroke size and color and for the rectangular shapes even some rounded corners. With these properties we can create drawables that can be used as button backgrounds, separators, panels or whatever else you can imagine. If you are targeting API version 21 or higher and use the ShapeDrawable as a background of some element with elevation, you get the correctly shaped shadow efect under the View (works for convex shapes only).

These basic shapes are without any doubt great and have been serving us well ever since, but it is more than clear that with arrival of Material Design 2.0 we need something more powerful. The new design specs encourages us to use miscellanious bold shapes to emphasize meaning, state, identity or branding of various components in our apps. Like I said at the beginning, we have multiple ways to achieve such effects, but the simplest way is not sufficient any more. Does that mean we need to start designing our shapes using vectors and lose the possibility to have cost-free shadows of elevated Views? Or should we even move back to the bitmaps and providing all the graphic assets for like six different screen densities? Luckily with the arrival of the new Material Design specs comes also the brand new Material Components library.

This library tries to unify the look and feel of the Material Design UI components across the different Android versions and of course unify these components across the different platforms (there is version of the library for iOS, web and for Flutter). The Material Components library also implements many of the features introduced in the new Material Design specs. For example it already includes the implementation of the new BottomAppBar component with all the desired behavior. Among the other ready-to-use components the library includes also MaterialShapeDrawable. This is in my opinion one of the main tools of the library to tackle the biggest challenges of the new design specs.

Although still being considered experimental in the 1.0.0 release, the MaterialShapeDrawable class comes with pretty useful toolset to implement very cool effects for our app. The MaterialShapeDrawable class offers us to define the shape by specifying how the edges and the corners of the final shape should look like. These basic shape definitions can be in addition controlled using the interpolation float property to allow the corners and edges animation.

To create the custom MaterialShapeDrawable we use the constructor, that takes the ShapePathModel as a parameter. The ShapePathModel stores the information about each edge and corner of the shape (there are always 4 edges and 4 corners, although you can define almost any shape to them) using the classes EdgeTreatment and CornerTreatment. You can set different treatments for each edge/corner or set the same treatment for all of them with one call.

There are few predefined ready-to-use edge and corner treatments which already implement most of the shape effects introduced in the Material Design 2.0 specs. There is already the RoundedCornerTreatment(float radius), the CutCornerTreatment(float size) or the TriangleEdgeTreatment(float size, boolean inside), which all work perfectly as you would expect. I prepared simple example below:

val shapePathModel = ShapePathModel().apply {
setAllCorners(CutCornerTreatment(dip(5).toFloat()))
setAllEdges(TriangleEdgeTreatment(dip(5).toFloat(), true))
}
val backgroundDrawable = MaterialShapeDrawable(shapePathModel).apply {
setTint(ContextCompat.getColor(this@MainActivity, R.color.colorPrimary))
paintStyle = Paint.Style.FILL
}
textView.background = backgroundDrawable

Which displays like this:

You can of course create your own corners and edges treatments since it is very simple. You always define the shape of the edge/corner as a path for the most top and left element (e.g. the top left corner) and the drawable will correctly rotate and connect it to the other edges/corners. I have again prepared a small example to begin with:

class CutoutCornersTreatment(val size: Float) : CornerTreatment() {
    override fun getCornerPath(angle: Float, interpolation: Float, shapePath: ShapePath) {
shapePath.reset(0.0f, size * interpolation)
shapePath.lineTo(size * interpolation, size * interpolation)
shapePath.lineTo(size * interpolation, 0f)
}
}
class CurvedEdgeTreatment(val size: Float) : EdgeTreatment() {
    override fun getEdgePath(length: Float, interpolation: Float, shapePath: ShapePath) {
shapePath.quadToPoint(length / 2f, size * interpolation, length, 0f)
}
}

When used in a background MaterialShapeDrawable, these treatments are diplayed like this:

In the bottomappbar package of the Material Components library there is already defined the BottomAppBarTopEdgeTreatment that allows all the cool effects like the cutout for the floating action button with possibility to animate the top edge based on the translation and the scale of the actual FloatingActionButton. I encourage you to go through the actual code of these classes to really see that the possibilities with the MaterialShapeDrawable are almost endless.

There is one more thing I have mentioned when talking about the basic ShapeDrawable and it is the possibility to drop the correctly shaped shadow. Since you can create very interesting shapes using the MaterialShapeDrawable it would be very disapointing not to have the possibility to cast shadows with them, especially when these shadows can be seen everywhere in the Material Design specs. The MaterialShapeDrawable considers also the shadows. There is possibility to enable the shadow casting using the shadowEnabled property. You can even specify the radius, elevation and the tint of the shadow. Does that sound too good to be true? Unfortunately yes and if you try to use the shadow with the MaterialShapeDrawable you end up with the nice shadow (painted using the setShadowLayer(float radius, float dx, float dy, int shadowColor) of the Paint class, intended for text shadows drawing) clipped to the bounds of the View the drawable is displayed in:

But we need to remember that the MaterialShapeDrawable is still considered experimental and its API is and will be subject to changes. Also all the contents of the Material Components library is open-sourced so we are most welcome to create an issue or even come up with the solution and fix the problem with a pull request. In fact when you are reading this post the API is already a little different in the master branch of the library (e.g. instead of ShapePathModel will be used ShapeAppearanceModel), which shows the hard work being done behind this library. The most interesting feature for the next release of the library seems to be the possibility to specify the default edges/corners treatments in the theme of your app. You can read more here and of course in the sources of the library.

Originally published at www.ackee.cz on October 4, 2018.