Android development
drawable color
programmatically change color
Android UI
mobile app development

Change drawable color programmatically

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In Android development, you often need to change the color of a drawable at runtime rather than defining separate XML resources for every color variation. This arises when supporting dynamic themes, responding to user preferences, or highlighting UI states like selection or error. Android provides several approaches for tinting drawables programmatically, each with different trade-offs around API level support and mutability.

Using setColorFilter

The setColorFilter method applies a color overlay to a drawable. It works on all API levels and is the simplest way to change a drawable's apparent color.

kotlin
1// Kotlin
2val drawable = ContextCompat.getDrawable(context, R.drawable.ic_star)
3drawable?.setColorFilter(
4    ContextCompat.getColor(context, R.color.accent_blue),
5    PorterDuff.Mode.SRC_IN
6)
7imageView.setImageDrawable(drawable)
java
1// Java
2Drawable drawable = ContextCompat.getDrawable(context, R.drawable.ic_star);
3if (drawable != null) {
4    drawable.setColorFilter(
5        ContextCompat.getColor(context, R.color.accent_blue),
6        PorterDuff.Mode.SRC_IN
7    );
8}
9imageView.setImageDrawable(drawable);

The PorterDuff.Mode.SRC_IN mode keeps the shape of the original drawable while replacing its color with the filter color. Other modes like MULTIPLY blend the filter with the original colors, which is useful for gradient drawables.

Using DrawableCompat.setTint

DrawableCompat.setTint from the AndroidX library is the recommended approach because it handles backward compatibility automatically. It delegates to the platform tint API on newer devices and falls back to a color filter on older ones.

kotlin
1// Kotlin
2val drawable = ContextCompat.getDrawable(context, R.drawable.ic_heart)?.let {
3    DrawableCompat.wrap(it).mutate()
4}
5drawable?.let {
6    DrawableCompat.setTint(it, Color.RED)
7}
8imageView.setImageDrawable(drawable)
java
1// Java
2Drawable drawable = ContextCompat.getDrawable(context, R.drawable.ic_heart);
3if (drawable != null) {
4    drawable = DrawableCompat.wrap(drawable).mutate();
5    DrawableCompat.setTint(drawable, Color.RED);
6}
7imageView.setImageDrawable(drawable);

You can also specify a tint mode with DrawableCompat.setTintMode(drawable, PorterDuff.Mode.SRC_IN) if the default mode does not produce the result you want.

Why mutate() Matters

Drawables loaded from resources share a common state by default. If you change the color of one instance, every view using that drawable resource sees the change. Calling mutate() gives the drawable its own independent state so that tinting one instance does not affect others.

kotlin
1// Without mutate: both imageViews turn red
2val drawableA = ContextCompat.getDrawable(context, R.drawable.ic_flag)
3drawableA?.setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)
4imageViewA.setImageDrawable(drawableA)
5
6val drawableB = ContextCompat.getDrawable(context, R.drawable.ic_flag)
7// drawableB is also red because they share state
8
9// With mutate: only imageViewA turns red
10val drawableA = ContextCompat.getDrawable(context, R.drawable.ic_flag)?.mutate()
11drawableA?.setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)
12imageViewA.setImageDrawable(drawableA)
13
14val drawableB = ContextCompat.getDrawable(context, R.drawable.ic_flag)
15// drawableB keeps its original color
16imageViewB.setImageDrawable(drawableB)

Always call mutate() before changing color when multiple views might reference the same drawable resource.

Using ColorStateList for State-Aware Tinting

When a drawable needs to change color based on view state (pressed, focused, disabled), use a ColorStateList instead of a single color. This avoids manually tracking state changes yourself.

kotlin
1// Kotlin
2val states = arrayOf(
3    intArrayOf(android.R.attr.state_pressed),
4    intArrayOf(android.R.attr.state_focused),
5    intArrayOf()  // default state
6)
7val colors = intArrayOf(
8    Color.parseColor("#1565C0"),  // pressed
9    Color.parseColor("#1976D2"),  // focused
10    Color.parseColor("#90CAF9")   // default
11)
12val colorStateList = ColorStateList(states, colors)
13
14val drawable = ContextCompat.getDrawable(context, R.drawable.ic_button_bg)?.mutate()
15drawable?.let {
16    DrawableCompat.setTintList(it, colorStateList)
17}
18button.background = drawable
java
1// Java
2int[][] states = new int[][] {
3    new int[] { android.R.attr.state_pressed },
4    new int[] { android.R.attr.state_focused },
5    new int[] {}  // default
6};
7int[] colors = new int[] {
8    Color.parseColor("#1565C0"),
9    Color.parseColor("#1976D2"),
10    Color.parseColor("#90CAF9")
11};
12ColorStateList colorStateList = new ColorStateList(states, colors);
13
14Drawable drawable = ContextCompat.getDrawable(context, R.drawable.ic_button_bg);
15if (drawable != null) {
16    drawable = drawable.mutate();
17    DrawableCompat.setTintList(drawable, colorStateList);
18}
19button.setBackground(drawable);

The order of state arrays matters. Android checks from top to bottom and uses the first match, so place the most specific states first and the default (empty) state last.

Common Pitfalls

  • Forgetting to call mutate(): Skipping mutate() causes shared drawable state across views, so tinting one icon unintentionally tints every instance of that resource.
  • Using deprecated setColorFilter(int, Mode) on API 29 and above: The two-argument overload is deprecated. Use setColorFilter(new PorterDuffColorFilter(color, mode)) or switch to DrawableCompat.setTint instead.
  • Applying a tint to a null drawable: ContextCompat.getDrawable() can return null if the resource ID is invalid. Always null-check before calling tint methods to avoid a crash.
  • Incorrect PorterDuff mode: Using SRC_OVER instead of SRC_IN overlays the color on top of the original rather than replacing it, producing a muddy or unexpected result on non-white drawables.
  • Placing the default state before specific states in ColorStateList: Android matches the first qualifying state array. If the empty (default) array comes first, the pressed and focused colors are never reached.

Summary

  • Use setColorFilter with PorterDuff.Mode.SRC_IN for a quick, all-API-level color change on a drawable.
  • Prefer DrawableCompat.setTint from AndroidX for backward-compatible tinting with cleaner API usage.
  • Always call mutate() before modifying a drawable's color to avoid shared-state side effects across views.
  • Use ColorStateList with DrawableCompat.setTintList when the drawable color should respond to view states like pressed or focused.
  • Null-check every drawable obtained from resources before applying color changes.

Course illustration
Course illustration

All Rights Reserved.