Messing with the drawable state

In this post you will learn about some of the details concerning Views and states. And you will learn how you can use your own custom states and manage them in a simple way. For the most part we discuss  StateListDrawable. However everything you will lean here works with ColorStateLists as well. Sample code for this post is available on github.

Using drawables

Sometimes it is very tempting to just set the background of a View manually, depending on the state of your Activity or Fragment. Unfortunately this requires additional code and state transitions won’t work anymore.

In this post I will show you two flavors of another way of doing this. Both of these flavors use the Android framework and will work in any app and with any Drawable.

Our case

We have a TextView somewhere in an app, that displays the current trend of something important for the user. In this case “Solar Radiation” and “Energy Output”. The trend has three possible values: moving up, moving down or stable. And finally, there should just be a single method to set the state and there should be a simple fade between the state changes.

The Android system already has a Drawable that implements all of this behavior, and you probably already know about this Drawable. The Drawable I am talking about is StateListDrawable, more commonly known as a selector. Selectors allow you to respond to state changes, and you can declare them simply using xml.

Creating the drawable

So let’s create that drawable! First we will define the possible states for the trend: state_up, state_equal, and state_down in our attrs.xml file like this:

<attr name="state_up" format="boolean"/>
<attr name="state_equal" format="boolean"/>
<attr name="state_down" format="boolean"/>

Now that we have the different states, we we will create a drawable that uses these different states:

<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
   <item android:drawable="@drawable/ic_state_up" app:state_up="true" />
   <item android:drawable="@drawable/ic_state_equal" app:state_equal="true" />
   <item android:drawable="@drawable/ic_state_down" app:state_down="true" />
   <item android:drawable="@drawable/ic_state_down" />
</selector>

That’s all for step one, we now have the selector we will use to transition between our custom states. As I said, there are two flavors we’ll explore. The first flavor is (not completely coincidental) exactly how Android implements view-state management in the framework. With view-state management I am talking about things such as selected state, checked state, activated state and focussed state. For us this involves creating a subclass of the view we want to use, and integrating the view-state into this class.

The second flavor makes use of a special DrawableWrapper that will manage the state. This is a bit more generic as we no longer need to subclass the view and tightly couple the state to it. Instead we can couple the state into a drawable subclass which we can use within all views.

Flavor 1: View subclass

As noted before, this implementation mirrors the way the framework implements state management. Once you know understand the steps involved here, it becomes real easy to do this yourself. The steps we need to follow are as follows:

  1. Create a subclass of the view we need, for example TrendView.
  2. Add an int-def that contains the possible states we can set.
  3. Add a method to the view to set our custom states.
  4. Implement refreshDrawableState to calculate the new state.

IntDef

I usually choose to implement the state as an IntDef with a setter for the state. This has the lowest memory footprint as discussed on developer.android.com. To quote what is written there:

For example, enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

First we add the IntDef and the different states to our class, and a setter setTrend to set the state. This method just sets our internal state, and in order to prevent additional work we also verify that the new state is actually different from the old state. After assigning the variable, we call refreshDrawableState. Calling refreshDrawableState triggers the invalidation process for the drawable state and will result in a call to onCreateDrawableState.

@IntDef({
         STATE_UP,
         STATE_DOWN,
         STATE_EQUAL
   })
@Retention(RetentionPolicy.CLASS)
public @interface Trend {}

@Trend
int mTrend;

public void setTrend(@Trend int trend) {
   if (mTrend != trend) {
      mTrend = trend;
      refreshDrawableState();
   }
}

Next we add a state-set for each of the possible states. These state-sets are merged together when the state is being calculated, and each state-set may consist of multiple identifiers. In our case each of them just contains a single constant for its state. These are the same constants we defined in attrs.xml earlier. To clarify, a single state-set looks like this:

private static final int[] UP_STATE_SET = { 
   R.attr.state_up 
}; 

The final part of the puzzle is onCreateDrawableState. In this method we just switch on our state, and merge our state with the thus far calculated drawable state. First we call super.onCreateDrawableState with extraState + 1 (because we want the parent class to reserve one additional slot for our state) and then we merge the states together. And finally, we return the result of the merge.

The full class now looks something like this (without the constructors):

public class TrendView extends AppCompatTextView {

   public static final int STATE_UP = 0;
   public static final int STATE_DOWN = 1;
   public static final int STATE_EQUAL = 2;

   @IntDef({
         STATE_UP,
         STATE_DOWN,
         STATE_EQUAL
   })
   @Retention(RetentionPolicy.CLASS)
   public @interface Trend {}

   private static final int[] UP_STATE_SET = {
      R.attr.state_up
   };

   private static final int[] DOWN_STATE_SET = {
      R.attr.state_down
   };

   private static final int[] EQUAL_STATE_SET = {
      R.attr.state_equal
   };

   @Trend
   int mTrend;

   public void setTrend(@Trend int trend) {
      if (mTrend != trend) {
         mTrend = trend;
         refreshDrawableState();
      }
   }

   @Override
   protected int[] onCreateDrawableState(int extraSpace) {
      // Only add 1 because we only have one state active at
      // any time
      final int[] drawableState =
         super.onCreateDrawableState(extraSpace + 1);
      switch (mTrend) {
         case STATE_UP:
         mergeDrawableStates(
            drawableState, UP_STATE_SET);
         break;
      case STATE_DOWN:
         mergeDrawableStates(
               drawableState, DOWN_STATE_SET);
         break;
      case STATE_EQUAL:
         mergeDrawableStates(
               drawableState, EQUAL_STATE_SET);
         break;
      }
      return drawableState;
   }
}

Now we can simply call setTrend on our custom View and the drawable will automatically be updated as well. In fact any Drawable that has the same states will work just fine.

Flavor 2: Using a wrapper drawable

As you might have suspected, the state being part of the view is not ideal. We can’t reuse the states or the drawables, in other classes without subclassing those views. It would be easier if we could decouple the state from the View. Let’s take a look at a different approach!

Why we need a wrapper

This part of the post is called using a wrapper drawable. Let’s first try to understand why we need a wrapper. What is wrong with calling setState directly?

As we have seen in the previous approach, the drawable state is controlled by the view. The state calculated in onCreateDrawableState is applied to each of the drawables by calling the setState method.  This means that in case the view’s state changes, it will synchronize its state to the drawable and call setState. So in case we manually call setState, we risk the view overriding our state with its own state.

Our solution

That is why we are going to create a StateDrawableWrapper. This wrapper will have two tasks, its first task is to prevent the view from propagating its state to the wrapped drawable, and its second task is applying our custom state to the wrapped drawable.

To accomplish the first task, we will override the setState method to prevent the View from updating the state. For now we will just return false from this method. For the second task we will add a new method to the wrapper to set our own state, called setCustomState. This method will simply call setState on the wrapped drawable. This wrapper might look like this:

public class StateDrawableWrapper extends DrawableWrapper {

	int[] mStateSet;

	public StateDrawableWrapper(Drawable drawable) {
 		super(drawable);
	}

	@Override
	public boolean setState(int[] stateSet) {
		// do nothing
		return false;
	}

	public void setCustomState(int[] stateSet) {
		if (!Arrays.equals(mStateSet, stateSet)) {
			mStateSet = stateSet;
			getWrappedDrawable().setState(stateSet);
			invalidateSelf();
		}
	}
	public int[] getCustomState() {
		return mStateSet;
	}
}

Above drawable wrapper is all there is to this. It prevents the view from updating the state of the wrapped drawable, and it can set our custom state on the View. And, it even works fine with transitions.

The one thing that is still missing here is the ability to also handle the states the view sets. For example selection and pressed states. In my next post I will discuss a way to implement this in a different way that also works correctly with the view’s state.

Thank you for reading! Cheers and see you next time.

Leave a Reply

Your email address will not be published. Required fields are marked *