Android Drawable with Custom States

Brian Terczynski
4 min readNov 28, 2020

--

Photo by Rami Al-zayat on Unsplash

Let’s say you are writing an Android app. You need to display a list of items, and they can all be in different states. For instance, you need to display a list of “tasks”, and those tasks can have a state of “in progress”, “done” or“failed”. And you want to display an icon next to the task indicating its state. For example:

One way to do this is to simply change the Drawable in the <ImageView> that shows the icon (e. g. imageView.setImageDrawable() ). That’s simple enough, but it does mean that the calling code needs to know what Drawable to pass in.

But there is a more elegant way. You can make use of a StateListDrawable with custom states. A StateListDrawable allows you to show a different Drawable based on a particular View’s state. A common example of this is a check box. A check box can have different states based on whether it is checked, being pressed, disabled, etc. And a StateListDrawable allows you to specify different ways to draw that check box based on one of those states. For example, if you are defining a custom check box, you can define how it is drawn in a StateListDrawable XML like so:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_checked="true"
android:state_enabled="false"
android:drawable="@drawable/checkbox_checked_disabled"/>
<item
android:state_checked="true"
android:drawable="@drawable/checkbox_checked"/>
<item
android:state_enabled="false"
android:drawable="@drawable/checkbox_disabled"/>
<item
android:drawable="@drawable/checkbox_unchecked"/>
</selector>

Note that the first match (reading from top to bottom) that meets the minimum criteria for a particular<item> will serve as the match, and thus short-circuit the evaluation. So ordering is very imporant when specifying your <selector> .

Android defines a default set of states that are defined here. Thus you can create a StateListDrawable that draws a different Drawable for any combination of those states’s values.

But you can go even further. You can define your own custom states. You can even combine your custom states with standard Android states. For my case, I have three states: “in progress”, “done” and “failed”. I want to show a different Drawable when each of those states istrue . So here is what I can do.

First, I need to define those custom states as attributes so that I can use them in my <selector> XML. I do that by creating a file called attrs.xml in my res/values directory. (This section of the Android Developer documentation talks a bit about creating custom attributes.) We then need to define our state attributes, which can be done with <attr> tags like so:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="state_in_progress" format="boolean" />
<attr name="state_done" format="boolean" />
<attr name="state_failed" format="boolean" />

</resources>

Now we need to define our StateListDrawable. We define it in XML in the res/drawable directory using the <selector> tag like in the check box example above. However, this time we make use of our custom attributes:

<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:drawable="@drawable/task_done"
app:state_done="true" />
<item
android:drawable="@drawable/task_failed"
app:state_failed="true" />
<item android:drawable="@drawable/task_pending" />
</selector>

To reference our custom states, we need to include the app namespace in our XML. Then we reference our custom attributes by prefacing them with app: , such as app:state_done .

Note also in the above that we did not reference app:state_in_progress . The reason is we want this to be the default state for our Drawable, so we list it as the last <item> and with no state specification. That way, if we don’t explicitly specify this state in our code, it will still show that Drawable as the default. (Because of that,state_in_progress is probably superfluous, but for illustration we’ll leave it in for now.)

Now let’s say that our StateListDrawable is in a file called res/drawable/task_indicator.xml. We can include it in an <ImageView> like so:

<ImageView
android:id="@+id/stateIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/task_indicator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

Then in our Kotlin code, we simply set the state with a call to setImageState():

binding.stateIndicator.setImageState(
intArrayOf(R.attr.state_done),
true // merge
)

This adds the “done” state to the ImageView, which in turn selects the appropriate Drawable in task_indicator.xml (in this case, @drawable/task_done). If the second parameter is true, then the specified state array will be merged with existing states; otherwise, it will replace all of the states. Thus with the above code, the <ImageView> shows the appropriate icon:

One subtle little trick: if you need to remove (cancel out) a state, you can specify its negative in the array you pass in. For example, to remove the “done” state:

binding.stateIndicator.setImageState(
intArrayOf(-R.attr.state_done),
true // merge
)

By using a StateListDrawable and defining custom states, we can easily show different images within the same View based on states that we define. We can then change which image is shown with just a simple line of code. By doing it this way, the code is more semantic and the caller does not need to handle the logic of selecting the appropriate image. Rather, the image selection is all defined within the StateListDrawable itself.

--

--

Brian Terczynski

Documenting my learnings on my journey as a software engineer.