I was recently confronted with the seemingly trivial task of creating a View with the shape of a cinema ticket. So I thought I would use this opportunity to refresh my knowledge on view shaping in Android as well as shadows and create a cheat sheet for future reference.
In this article, I will attempt to cover the different techniques available to create a view with a custom shape, and for each technique I will assess how well it performs on the following criteria:
— Does it really clip
— Does it create a shadow with the correct shape
— Does it work on any shape
And if those criteria don’t make sense to you, keep reading and it will become clearer as we go through our options.
Official documentation is relatively straightforward. https://developer.android.com/training/material/shadows-clipping
Basically to clip a view to the desired shape, all you need to do is create a XML Shape and set it as background of your View. And it works quite well for basic shapes like circles, triangles and rounded corner rectangles. It will even draw a shadow for you on API 21+ if you set an elevation. But it falls short in many cases because of two major issues:
1 — It does not work with complex shapes. The Shape api simply does not support shapes other than rectangles, oval, lines and rings.
2 — The View does not really get clipped nor does it acquire a new shape in which the drawing should be done. This means that any other View that you put inside the View that is shaped, might appear outside of the shaped area if it is too big. This is because although you can give the appearance of a shape to your view, the View remains a rectangle and the view bonds remain the bonds of the rectangle.
The second issue that I mention above (the Drawable background not clipping the View) is no longer an issue since API 21 which introduced the notion of Outline. The Outline is basically the “actual” shape of your View, and you can request that the View gets clipped according to the Outline. And it is quite simple.
But what about the first issue that we have with the Drawable Shapes defined in XML ? You remember that they only allow for simple shapes like rounded rectangles or circles (it also supports rings, but honestly, who uses rings?). Well, we can use a method called setConvexPath(Path path) on our Outline, and you can use it to shape the outline following the contour of the given Path, but it will throw an IllegalArgumentException if you try to use a non convex path.
According to the Outline documentation, A path is convex if it has a single contour, and only ever curves in a single direction. Another definition would be : “Anything that has only angles lower than 180 degrees.” So a triangle is fine, and you can add as many sides as you want until you get a circle, and you would still be fine. But try to do a star or add an inward curve and your Path is no longer convex. So the Outline Api is a nice touch but we are still far from my ticket shape.
Vector Drawables, PNGs and 9 patches
Here is an idea, recreate your shape as a Vector Drawable and use it as a background. Vector Drawables were introduced in API 21 and if your are targeting versions below that, Android Studio will generate PNGs for you (or you can also use VectorDrawableCompat). That actually works, the view has the proper shape, you can have any shape you want, and you get to have your designer work for you by providing you nice assets. And since you are using a vector or PNG asset, nothing stops you from adding a shadow drawn in your asset.
But you hit the exact same issue as before with your View not being really clipped.
The last resort solution when everything else fails is to clip the View Canvas using a Path. This is the solution I ended up using.
For this you need to create a custom View extending any View or ViewGroup and override the draw method like this.
Basically what you do is that before drawing anyting into the view, you clip the canvas to your desired shape. The path used can be anything. Here is the one I wrote for my ticket:
Writing a Path can be daunting at first but with a little bit of practice it is quite easy and I even find it relaxing.
But this method has a major drawback, which is that you won’t have any shadow drawn for you. You need to draw it yourself. The good news is that it is quite easy now that you have a Path. All you need to do, is put an ImageView underneath the View that you just clipped and create a Bitmap representing the shadow of the View.
As you can see, all the magic is done by the method setShadowLayer of the Paint object. Then all you do really is call drawPath() on the Canvas, using the path and the Paint you just configured.
This method clearly requires more work but you get nice results.
To sum up, here is the actual cheat sheet.
As you can see, the main issue in the clipping method is that it is not easy to implement and requires a lot of boilerplate code. That is why in the process of working on this View, I created a small library that makes this technique a lot easier to use. I called it ViewShaper and it is available here.