Manipulating images and Drawables with Android’s ColorFilter

Tinting, custom effects and reusability for visual Android app resources

Nick Rout
Over Engineering
Published in
6 min readJul 31, 2018

--

Image and Drawable resources are an integral part of any Android app. Since day (i.e. API level) 1, the Android framework has provided a means of manipulating the colors of these assets on a per-pixel level, as they are drawn to screen, with the ColorFilter class.

The documentation for the base abstract class stipulates that “a color filter can be used with a Paint to modify the color of each pixel drawn with that paint”. This is a powerful tool that can be used for:

  • Applying effects and adjustments to images
  • Tinting of icons and logos

Ultimately this encourages the good practice of reusing resources for scenarios that require different color variations, resulting in reduced app size.

Note: ColorFilter is an abstract class that should never be used directly, and the constructor was deprecated in API level 26. Only the subclasses should be used, which we will discuss further down.

Where can this be used? 🤔

Before we explore the existing subclasses and their capabilities, we need to discuss where we can actually use ColorFilter.

Canvas

As hinted at in the documentation description, we can use ColorFilter when drawing to Canvas in a custom View. Specifically, a ColorFilter is set on a Paint which then affects everything that is drawn with it:

// Set ColorFilter (pass in null to clear)
paint.colorFilter = colorFilter
// Get ColorFilter
val colorFilter = paint.colorFilter

Drawable

A ColorFilter can be applied to a Drawable, which will affect how it is rendered in Views and layouts:

// Set ColorFilter (pass in null to clear)
drawable.colorFilter = colorFilter
// Get ColorFilter
val colorFilter = drawable.colorFilter
// Clear ColorFilter
drawable.clearColorFilter()

In addition to this, there exists a Drawable convenience function to apply a PorterDuffColorFilter (more on that later):

// Set PorterDuffColorFilter with color and mode
val
color = Color.BLUE
val mode = PorterDuff.Mode.DST_OVER
drawable.setColorFilter(color, mode)

Lastly, it is important to note that a tint can be applied to a Drawable. This is separate and will be overridden if a ColorFilter is set via either one of the above functions.

ImageView

A ColorFilter can be applied to an ImageView, which will affect how its current image will be rendered:

// Set ColorFilter (pass in null to clear)
imageView.colorFilter = colorFilter
// Get ColorFilter
val colorFilter = imageView.colorFilter
// Clear ColorFilter
imageView.clearColorFilter()

As with Drawable, convenience functions exist for applying a PorterDuffColorFilter:

// Set PorterDuffColorFilter with color and mode
val
color = Color.BLUE
val mode = PorterDuff.Mode.DST_OVER
imageView.setColorFilter(color, mode)
// Set PorterDuffColorFilter with color (mode defaults to SRC_ATOP)
imageView.setColorFilter(color)

Introducing our sample image 🖼️

We are now going to dive into the three subclasses of ColorFilter (PorterDuffColorFilter, LightingColorFilter and ColorMatrixColorFilter). In order to demonstrate this, we will make use of a visual aid in the form of a sample image:

Source: pixabay.com

It has photographic detail while also having “shape” as a result of the transparent areas. This will allow all of the subclasses to be demonstrated.

PorterDuffColorFilter 1️⃣

We have already touched on this briefly. This is a color filter that accepts a single color that is applied to all source pixels along with a Porter-Duff composite mode. There are many modes that are suitable to different scenarios. Typically, this is used to apply a “blanket” color (eg. To tint an icon).

val porterDuffColorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)

LightingColorFilter 2️⃣

This color filter can be used to simulate lighting effects on an image. The constructor accepts two parameters, the first to multiply the source color (colorMultiply) and the second to add to the source color (colorAdd).

val lightingColorFilter = LightingColorFilter(colorMultiply, colorAdd)

While the source color alpha channel is not affected, the R, G and B channels are computed like so:

// Each channel is pinned to the range [0, 255]
val newRed = color.red * colorMultiply.red + colorAdd.red
val newGreen = color.green * colorMultiply.green + colorAdd.green
val newBlue = color.blue * colorMultiply.blue + colorAdd.blue

Note: The above is using Android KTX Color extension functions for accessing the red, blue and green channels (alpha is also available).

ColorMatrixColorFilter 3️⃣

This is arguably the most flexible (but also the most complex) color filter.

It is quite similar to LightingColorFilter, in that we can tweak each pixel’s color channels using values that are multiplicative and additive. However, a ColorMatrixColorFilter is constructed with a ColorMatrix, which is essentially a wrapper class for a 4x5 matrix. This gives us 20 values, used in a certain way, that allow us to transform colors using all of the existing channels, including alpha.

// Optionally provide a 4x5 matrix (size 20 Float array) to the ColorMatrix constructor, otherwise assumes identity matrix
val
colorMatrix = ColorMatrix()
val
colorMatrixColorFilter = ColorMatrixColorFilter(colorMatrix)

Before we dive into using matrices, let’s first take a look at some of the convenience functions offered by ColorMatrix:

// Set saturation
colorMatrix.setSaturation(0.5f)
// Set RGB to YUV colorspace, or vice versa
colorMatrix.setRGB2YUV()
colorMatrix.setYUV2RGB()
// Set the rotation on a color axis
// Red axis = 0, green axis = 1, blue axis = 2
colorMatrix.setRotate(axis = 0, degrees = 30f)

Fear not if some of these color concepts do not make sense! The point here is that ColorMatrix offers a lot of flexibility, and these convenience functions are simply matrix operations under the hood.

We know that ColorMatrix wraps a 4x5 matrix. Given this matrix, how do we arrive at our resultant color channels? They are computed like so:

// 4x5 matrix (i.e. Float array of size 20)
val
matrix = floatArrayOf(
a, b, c, d, e,
f, g, h, i, j,
k, l, m, n, o,
p, q, r, s, t
)
// Using matrix coefficients to compute final colors
val newRed = color.red * a + color.green * b + color.blue * c + color.alpha * d + e
val newGreen = color.red * f + color.green * g + color.blue * h + color.alpha * i + j
val newBlue = color.red * k + color.green * l + color.blue * m + color.alpha * n + o
val newAlpha = color.red * p + color.green * q + color.blue * r + color.alpha * s + t

The 4 rows in fact represent the resultant R, G, B and A channels. For each channel, the 5 columns allow you to combine the existing R, G, B, and A channels and a wildcard value in a plethora of ways.

This provides us with a lot of possibilities. We can adjust the brightness/contrast of images, ignore some channels, invert an image or even create basic filters and effects.

Limitations and other options 🛠️

While ColorFilter is powerful and flexible, image manipulation is a vast field and it simply cannot cover everything.

For example, the current pixel value that is being passed through the filter would be handy to know, but is not available. Additionally, you are limited to the three subclasses that ColorFilter currently provides. It appears as if you cannot create a custom subclass, due to native code restrictions.

In instances like this, what other options do we have?

The graphics framework has other useful classes such as Shader and MaskFilter. We could turn to RenderScript, which offers Bitmap utilities that still keep us mostly within traditional Android graphics territory. There is also OpenGL, which is perhaps the most extreme power vs. complexity tradeoff, but opens up all of the possibilities of custom GLSL shaders.

Overall, ColorFilter is still a fantastic tool for working with app resources.

I hope this post has provided some insight into ColorFilter and the flexibility it provides when working with images and Drawables. If you have any questions, thoughts or suggestions then I’d love to hear from you!

Find me on Twitter @ricknout

--

--

Nick Rout
Over Engineering

Principal Android Engineer at GoDaddy | Ex-Google | Google Developer Expert for Android