Android: Views with custom states

Learn how to add custom view states in Android. This guide covers creating and managing custom drawable states using XML, improving UI consistency with custom state attributes, and enhancing your Android app's user experience.

31 Aug 2020

Android provides a lot of different states by default – Enabled, Checked, Pressed. It’s quite useful to have different drawables, colors & backgrounds for each state so that the user gets proper UI feedback.

I have been developing Android apps for over 6 years now and I just found out that we can create our own custom states and have different drawable states for each of these. Instead of manually changing the text color / drawable, we can use these custom states and have a uniform effect throughout the app.

Use custom states to have a uniform UI throughout the app.

I would like to share a basic example of implementing custom drawable states in Android. As an example, I am going to create custom states that display different levels of alerts - From no alert to urgent/critical.

Custom states – Attributes

First things first - define your custom states in Attrs.xml file. I have defined 5 attributes - one for each state.

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <declare-styleable name="WarningLevel">
    <attr name="warning__level_none" format="boolean" />
    <attr name="warning__level_low" format="boolean" />
    <attr name="warning__level_medium" format="boolean" />
    <attr name="warning__level_high" format="boolean" />
    <attr name="warning__level_urgent" format="boolean" />
  </declare-styleable>
</resources>

I tried it with Enum attributes. But it was not possible to work with it. So I decided to stick with 5 different booleans attributes.

After attributes, I have defined state-list colors. Each warning level has its own color. This is defined in res/colors.

<?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:color="@color/warn_none"
    app:warning__level_none="true" />
  <item
    android:color="@color/warn_low"
    app:warning__level_low="true" />
  <item
    android:color="@color/warn_medium"
    app:warning__level_medium="true" />
  <item
    android:color="@color/warn_high"
    app:warning__level_high="true" />
  <item
    android:color="@color/warn_urgent"
    app:warning__level_urgent="true" />
  <item android:color="@color/warn_none" />
</selector>

TextView to support custom state

I have created a new class which extends TextView. This custom view will be used for seamless state switching.

To make it easier, I have defined an enum in Kotlin which will be used in the TextView.

enum class WarningState(
  @AttrRes val attrId: Int,
  @StringRes val resId: Int
) {
  NONE(
    R.attr.warning__level_none,
    R.string.warning_none
  ),
  LOW(
    R.attr.warning__level_low,
    R.string.warning_low
  ),
  MEDIUM(
    R.attr.warning__level_medium,
    R.string.warning_medium
  ),
  HIGH(
    R.attr.warning__level_high,
    R.string.warning_high
  ),
  URGENT(
    R.attr.warning__level_urgent,
    R.string.warning_urgent
  )
}

Now, a simple extended TextView class. The customization is very little and yet works wonderfully.

  1. Override onCreateDrawableState() and return our merged drawable state.
  2. Call refreshDrawableState() whenever the state is changed. This method makes sure that drawable states are recomputed.
class WarningStateTextView @JvmOverloads constructor(
    context: Context, 
    attrs: AttributeSet? = null, 
    defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
    
    private val warningState = intArrayOf(R.attr.warning__level_none)
    var warningLevel: WarningState? = null
        set(value) {
            if (field == value) {
                return
            }
            field = value
            refreshDrawableState()
        }
    
    override fun onCreateDrawableState(extraSpace: Int): IntArray {
        val state = super.onCreateDrawableState(extraSpace + 1)
        warningLevel?.let {
            warningState[0] = it.attrId
            View.mergeDrawableStates(state, warningState)
        }
        return state
    }
}

Related Articles