Sunday, January 25, 2015

Implementing and Using Custom Drawable States

    One of the things that Android comes with out of the box is pressed states, which allows colors and drawables to change what's displayed on a view based on whether or not a user is pressing down on that view. In this post I'm going to take that a step further and go over a simple example for how to implement custom drawable states that allow you to tailor your user experience based on data. All code for this post can be found on GitHub.

    To keep things simple, the first class we're going to define is an enum containing the expected states that we want to work with. In this case we're going to use a traffic signal set of "Go", "Slow Down" and "Stop".

public enum CustomState {
    GO,
    SLOW_DOWN,
    STOP
}

    Next we're going to want to create an 'attrs.xml' file to store the attributes that we'll use for our states

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="state_go" format="boolean" />
    <attr name="state_slow_down" format="boolean" />
    <attr name="state_stop" format="boolean" />
</resources>

    Once our attributes are defined, we're going to move to where the magic happens: our custom View (CustomDrawableTextView.java, in this example). The first thing we're going to do is add a set of int arrays that represent each state that we want to support as member variables. These int arrays are going to be used later to merge in with our drawable states. We're also going to want to create a variable to keep track of which state is currently active from our custom enum.

    protected static int[] STATE_GO = { R.attr.state_go };
    protected static int[] STATE_SLOW_DOWN = { R.attr.state_slow_down };
    protected static int[] STATE_STOP = { R.attr.state_stop };

    private CustomState mState;

    In order to drive this view, I've added an update method that is manually called on a timer from MainActivity, though this View could just as easily be built to use an event bus or other technique to drive the data. The key thing to notice is that the new desired state is passed to and saved by the View, and refreshDrawableState() is called.

    public void update( CustomState state ) {

        if( state != null )
            mState = state;

        if( CustomState.GO.equals( mState ) ) {
            setText( "GO" );
        } else if( CustomState.SLOW_DOWN.equals( mState ) ) {
            setText( "SLOW DOWN" );
        } else if( CustomState.STOP.equals( mState) ) {
            setText( "STOP" );
        }

        refreshDrawableState();
    }

    The final thing we need to do with our custom View is override onCreateDrawableState to add in our custom state attributes. We do this by saving the result of the super method with an incremented parameter passed to it, and then calling mergeDrawableStates with the result and our int array attribute to add our custom attribute to the active drawable states for our View.

    @Override
    protected int[] onCreateDrawableState(int extraSpace) {
        if( mState == null )
            return super.onCreateDrawableState(extraSpace);

        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);

        if( CustomState.GO.equals( mState ) ) {
            mergeDrawableStates( drawableState, STATE_GO );
            return drawableState;
        } else if( CustomState.SLOW_DOWN.equals( mState ) ) {
            mergeDrawableStates( drawableState, STATE_SLOW_DOWN );
            return drawableState;
        } else if( CustomState.STOP.equals( mState) ) {
            mergeDrawableStates( drawableState, STATE_STOP );
            return drawableState;
        } else {
            return super.onCreateDrawableState(extraSpace);
        }
    }

    Now that that's all set, let's define a new drawable selector file to take advantage of our custom drawable states

<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:customapp="http://schemas.android.com/apk/res-auto">
    <item customapp:state_go="true" android:drawable="@android:color/holo_green_light" />
    <item customapp:state_slow_down="true" android:drawable="@color/yellow" />
    <item customapp:state_stop="true" android:drawable="@android:color/holo_red_light" />
    <item android:drawable="@android:color/white" />
</selector>

    Notice the custom attributes of state_go, state_slow_down and state_stop that match our attrs.xml file and our custom int arrays from our view. Android will automatically handle displaying the proper color for this state drawable now that we've laid out the groundwork for it. Finally we'll assign this drawable as a background for our custom View, and we'll see the color change as our states change.

    <com.ptrprograms.customdrawablestates.CustomDrawableTextView
        android:id="@+id/custom_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:textSize="40sp"
        android:gravity="center"
        android:text="@string/hello_world"
        android:background="@drawable/custom_state_background"/>