Tuesday, February 27, 2018

How to create a selector drawable and change its states on the fly in code

If you've ever built your own buttons you will likely use a selector Drawable to describe all the different states that the button will go through when it's pressed or disabled etc...
To do that the most common way to do it is by using a selector XML resource file.
Here's an example:

<selector xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res-auto">
  <item android:state_enabled="true"
        android:drawable="@drawable/normal_button"/>
  <item android:state_pressed="true"
        android:drawable="@drawable/pressed_button"/>
  <item app:state_red="true"
        android:drawable="@color/red"/>
  <item android:state_enabled="false"
        android:drawable="@color/white"/>
</selector>

Now, what if you want to change some of these conditionally or simply you want to do this in code?
Here's what you would write to get to the same selector behavior but using code:
    StateListDrawable states = new StateListDrawable();
    states.addState(
      new int[]{android.R.attr.state_enabled},
      getResources().getDrawable(R.drawable.blue_button));
    states.addState(
      new int[]{android.R.attr.state_pressed},
      getResources().getDrawable(R.drawable.grey_button));
    states.addState(
      new int[]{R.attr.state_red},
      getResources().getDrawable(R.color.red));
    states.addState(
      new int[]{-android.R.attr.state_enabled},
      getResources().getDrawable(R.color.white));
    myButton.setBackgroundDrawable(states);

As you can see all you need is to create a StateListDrawable and add all the states that your selector has. You can even add your own custom states (for example here I'm using my own R.attr.state_red)
The trick is to know that if you want to set the drawable for true you use the attribute as it is and if you want the false case you use its negative value. (in this example I set the disabled state using -android.R.attr.state_enabled)
Once all your states are defined you can just use that StateListDrawable as a background for your Button or ImageView or whatever...

Now you can change anything you want on the fly and switch or test behavior as you need.

PS: to declare your own stylable attributes (like my state_red) you create an attrs.xml file in your res/values/ folder and define them like this: (all you need here for this example is the line with state_red but I added more examples for the other possible types)

<resources>
  <declare-styleable name="MyButton">
    <attr name="primaryColor" format="reference|color" />
    <attr name="myButtonStyle" format="string" />
    <attr name="state_red" format="boolean" />
    <attr name="state_test" format="boolean" />

    <attr name="myflags">
      <flag name="one" value="1" />
      <flag name="two" value="2" />
      <flag name="four" value="4" />
      <flag name="eight" value="8" />
    </attr>

    <attr name="myenum">
      <enum name="one" value="1" />
      <enum name="two" value="2" />
      <enum name="three" value="3" />
    </attr>

  </declare-styleable>
</resources>


BONUS: If you want to set custom states and change them at runtime and in code here's what you do:
You will have to define your own class that extends Button or ImageView or whatever it is that your modifying.
In that child class, you will want to override the onCreateDrawableState method so that it will set different states depending on whatever use case you have. Here I'm making the state green or red depending on the value of a field variable called mMyBoolean:

  @Override  protected int[] onCreateDrawableState(int extraSpace) {
    final int[] drawableState = super.onCreateDrawableState(extraSpace + 2);
    if (mMyBoolean) {
      mergeDrawableStates(drawableState, {R.attr.state_green});
    } else {
      mergeDrawableStates(drawableState, {R.attr.state_red});
    }
    return drawableState;
  }
}

Then all you have to do is set that boolean to whatever value it needs to be for your use case and then call refreshDrawableState(); on your Button or ImageView or whatever your View is.