Fast Android asset theming with ColorFilter

One of the neat aspects of the material theme in the upcoming Android L release is being able to theme all of your app (icons and all) with just a handful of attributes:

<style name="AppTheme" parent="android:Theme.Material">
  <item name="android:colorPrimary">@color/primary</item>
  <item name="android:colorPrimaryDark">@color/primary_dark</item>
  <item name="android:colorAccent">@color/accent</item>
</style>

This theming method can save you tons of time. You can change all your app's colors at once, or just tweak the colors of a single drawable, without having to go back to your designers for new assets.

The good news is this time-saving method can be used today! We can get a hint of how this might work by looking at ImageView's android:tint attribute. You can set a tint color in XML and voilĂ ! Your asset is now colored differently.

There's two problems with ImageView's tint, though, which is why you should avoid it:

  • ImageView's tint mixes the tint color with the original asset. What you want is for the tint color to take over entirely; instead it applies the tint on top of the existing color. So, for example, if the source asset is black, and you want it to be #77FFFFFF (a translucent shade of white), you'll actually end up getting that shade of white with a black background beneath it.

  • android:tint is limited to ImageView. You want to be able to tint any Drawable in any View.

Both of these problems are easily solved by investigating how ImageView implements its tint: with a ColorFilter. In particular, it uses Porter/Duff compositing to apply a color on top of an image.

To solve the first problem, we just need to use a different Porter/Duff mode. ImageView uses PorterDuff.Mode.SRC_ATOP, which places our tint color on top of the image (but still draws the original image below). To theme, what we actually want to use is PorterDuff.Mode.SRC_IN, which only draws the tint color.

The second problem is also easily avoided because all Drawables have a setColorFilter() method! How handy.

Here's a short sample applying a themed color to the background of a View:

Resources res = getResources();
Drawable background = res.getDrawable(R.drawable.background);
int primaryColor = res.getColor(R.color.primary);
background.setColorFilter(primaryColor, PorterDuff.Mode.SRC_IN);
View view = findViewById(R.id.my_view);
view.setBackgroundDrawable(background);

You can save time by wrapping the Drawable-with-theme-color creation in a reusable method:

public Drawable getTintedDrawable(Resources res,
  @DrawableRes int drawableResId, @ColorRes int colorResId) {
    Drawable drawable = res.getDrawable(drawableResId);
    int color = res.getColor(colorResId);
    drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
    return drawable;
}

(As a side note, check out those cool @DrawableRes and @ColorRes annotations, which will help you avoid mistakes when calling methods with incorrect parameters.)

While not quite as convenient as Android L's theming, this method is backwards compatible, so you can start using it now. Start saving time today!

Addendum

Added July 24, 2015

A word of warning: You can run afoul of Drawable's shared state when using ColorFilter.

As an optimization, Drawables are shared across your app. For example, if you're using the same action bar icon in multiple Toolbars, it's actually (essentially) a single Drawable instance being used. As such, if you use ColorFilter, you might find all your Drawables change even when you only wanted to change one of them!

There's an in-depth explanation of this optimization in this blog post. tl;dr - if you're using ColorFilter and you don't intend to change the Drawable everywhere in your app, make sure to call Drawable.mutate() beforehand.

comments powered by Disqus