Clipping and shadows on Android

Marcin Korniluk
6 min readNov 27, 2018

--

Material Design is filled with fancy shapes and shadows, but not all of these things are implemented and ready to use. There’s a good number of 3rd party libraries, but their quality varies. Let’s go through the options we have when it comes to clipping and shadows.

Clipping

Here ‘clipping’ means ‘restricting the drawing of a view’s content to a specific shape’. Material Design introduced rounded corners for buttons and cards. Floating Action Button was a special, circular type of button. Currently, with Material Design 2, we have more rounded and cut corners, diagonal slices and other interesting shapes.

If the clipped view is a layout then clipping is also applied to its children. This is especially important when talking about CardView and full-bled images.

ViewOutlineProvider

Starting with Android Lollipop there’s a native way of clipping views using view outlines. This method is hardware accelerated, very fast, pretty easy to use, correctly clips the layout’s content and produces antialiased outlines. This should be the default method used in API 21+ and the only method used by system components.

It has two downsides though. The first one is pretty obvious — you can’t use it on KitKat and below. In such a case you have to use one of the other options or just skip drawing shadows altogether. Older phones are usually slower, so it may be a pretty good solution.

The lack of clipping on pre-Lollipop systems results in tons of issues related to CardView, where clipping is a necessity. That’s why CardView forces an ugly padding or refuses to round corners on older platforms.

A card with its corners rounded correctly on API 14

The other one is more problematic. ViewOutlineProvider can’t clip to shapes other than rectangles, rounded rectangles and circles. Even ovals are not supported. Neither are paths, even convex ones. This is even more surprising when you realize that a lot of recent Material Design ideas require a cut-off corner or a cradle inside a view.

The second issue introduced a lot of friction between developers and designers unaware of that limitation. Especially when the clipped cinema/plane ticket shapes became very popular on Dribbble and Behance. Unfortunately there’s no way to support such shapes using ViewOutlineProvider as of today.

View.setBackground() and Drawable

If the view’s shape is simple then instead of clipping it may use a background drawable faking the clipping, for example a rounded rectangle with a bitmap, a gradient or a solid color. This technique can be used to achieve the correct shape of a Button or an ImageView used for showing profile pictures.

A view’s background won’t clip its contents, but it is easy to prepare, works on all platforms and is very fast to draw. Drawables support all concave shapes, so it’s possible to draw stars, cinema tickets and other things impossible to clip using ViewOutlineProvider.

The Design Support Library uses drawables for its components to support rounded corners of Button and CardView on pre-Lollipop platforms. That’s why CardView behaves on older phones as described in the previous paragraph.

Canvas.clipPath()

Each view uses a Canvas instance to draw itself. It’s possible to set a clipping shape to that Canvas and draw the clipped content manually. This method requires a tiny drawing code modification in each component we want to clip. This kind of clipping is fast, works on all platforms and supports any shape.

There are two drawbacks. The first one is that there’s no antialiasing, so the outline is very sharp.

The other one is that Canvas.clipPath() is not hardware accelerated on API 14 to 17. I suppose that with the current market share this is not a game changer. Nonetheless, it’s important to remember that certain operations force the drawing code to switch to the software rendering pipeline, which results in slower drawing and less FPS.

Paint.setXferemode()

This method is the most complicated and computation-heavy, but gives the best results on all platforms: it supports all shapes, is hardware accelerated and the outline is antialiased.

The idea is known to artists as ‘masking’. All we need is a separate layer for masking new content, a mask and a composition code. The algorithm is pretty easy:

  1. Create a new layer for masking operations using Canvas.saveLayer().
  2. Draw the view.
  3. Switch the drawing mode to either clearing unmasked or keeping masked parts.
  4. Draw the mask.
  5. Compose layers using Canvas.restore().
int saveCount = canvas.saveLayer(...);
super.draw(canvas);
paint.setXfermode(CLEAR_MODE);
canvas.drawPath(cornersMask, paint);
paint.setXfermode(null);
canvas.restoreToCount(saveCount);

Shadows

Material Design shadows are dynamic and take the caster’s shape, position and opacity into account. Shadows have a bunch of roles — they help show importance, purpose and relations between components.

Android Lollipop introduced a native mechanism for rendering nice, hardware-accelerated shadows. This part of the framework wasn’t officially backported and is not available for older platforms. The Design Support Library uses custom drawables to draw shadows.

ViewOutlineProvider

Again, the official method uses the view’s outline and works on API 21+. It’s hardware accelerated, fast and looks nice. There’s a small difference in supported shapes — when drawing shadows, the outline can be any convex shape, so it’s possible to use custom paths. This leads to weird situations where you can get nice shadows for diagonally cut views, but you have to clip them using a different method.

ViewOutlineProvider supports colored shadows starting from API 28. That means a lot of waiting until this feature is used more widely.

Concave shapes are not supported, so cinema tickets mentioned earlier can’t cast shadows using this method. While this is a pretty sophisticated case, there’s also a more common one. The BottomAppBar component introduced in Material Design 2 has a cradle for a button, which makes its shape non-convex. Because of that, BottomAppBar doesn’t cast shadows using the official method, but using Paint.setShadowLayer().

Paint.setShadowLayer()

This method uses blurring to draw a shadow below anything drawn using that Paint object. It’s easy to use, gives pretty good results and works with any shape and color. The Design Support Library provides the MaterialDrawableShape class, which uses Paint.setShadowLayer() internally — you can find it under the following link:

There’s one problem with Paint.setShadowLayer() — for drawing things other than text it is hardware accelerated starting from API 28. This downside makes this method pretty much unusable as it may lead to issues with component drawing and animation.

You can find more information about hardware-accelerated drawing operations in the docs. There’s a table with these operations and the minimum supported API level for each of them.

View.setBackground() and Drawable

It’s obviously possible to draw shadows using proper drawables — dynamic, 9-patches or simply images. In the HTML world this solution was used widely for a long time.

When using a background drawable for drawing shadows, we have to remember that the shadow becomes part of the view. It means that the view needs some padding inside to correctly align its contents.

ScriptIntrinsicBlur

This is a RenderScript shader we can use to generate shadows dynamically. It’s the most complex and the slowest of the discussed methods, but it works in all cases, on all platforms and can be used with hardware layers without any issues. Here’s the algorithm:

  1. Draw a view’s black shape to an offscreen bitmap.
  2. Blur it.
  3. Draw the blurred shape below the actual view.
Colored shadows generated using ScriptIntrisincBlur

The generated shadow can be set as the view’s background with some padding, drawn by the view or drawn by the view’s parent. Shadows can be cached and reused to reduce time needed for blurring.

Personally I’m using a method based on hardware blurring with a couple of optimizations. The most fancy trick is to generate a 9-patch instead of the full shape of a view. It greatly reduces blurring time and supports resize animations for free.

--

--