Search code examples
androidxmlandroid-layoutandroid-custom-view

Android: Is it possible to use string/enum in drawable selector?


Questions

Q1: Has anyone managed to get custom string/enum attribute working in xml selectors? I got a boolean attribute working by following [1], but not a string attribute.

EDIT: Thanks for answers. Currently android supports only boolean selectors. See accepted answer for the reason.

I'm planning to implement a little complex custom button, whose appearance depends on two variables. Other will be a boolean attribute (true or false) and another category-like attribute (has many different possible values). My plan is to use boolean and string (or maybe enum?) attributes. I was hoping I could define the UI in xml selector using boolean and string attribute.

Q2: Why in [1] the onCreateDrawableState(), boolean attributes are merged only if they are true?

This is what I tested, boolean attribute works, string doesn't

NOTE: This is just a test app to figure out if string/enum attribute is possible in xml selector. I know that I could set button's textcolor without a custom attribute.

In my demo application, I use a boolean attribute to set button background to dark/bright and string attribute to set text color, one of {"red", "green", "blue"}. Attributes are defined in /res/values/attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyCustomButton">
        <attr name="make_dark_background" format="boolean" />
        <attr name="str_attr" format="string" />
    </declare-styleable>
</resources>

Here are the selectors I want to achieve:

@drawable/custom_button_background (which works)

<?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/com.example.customstringattribute">

    <item app:make_dark_background="true" android:drawable="@color/dark" />
    <item android:drawable="@color/bright" />

</selector>

@color/custom_button_text_color (which does not work)

<selector 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res/com.example.customstringattribute">

    <item app:str_attr="red" android:color="@color/red" />
    <item app:str_attr="green" android:color="@color/green" />
    <item app:str_attr="blue" android:color="@color/blue" />

    <item android:color="@color/grey" />

</selector>

Here is how custom button background is connected to boolean selector, and text color is connected to string selector.

<com.example.customstringattribute.MyCustomButton
    ...
    android:background="@drawable/custom_button_background"
    android:textColor="@color/custom_button_text_color"
    ...
/>

Here is how attributes are loaded in the init() method:

private void init(AttributeSet attrs) {

    TypedArray a = getContext().obtainStyledAttributes(attrs,
            R.styleable.MyCustomButton);

        final int N = a.getIndexCount();
        for (int i = 0; i < N; ++i)
        {
            int attr = a.getIndex(i);
            switch (attr)
            {
                case R.styleable.MyCustomButton_str_attr:
                    mStrAttr = a.getString(attr);
                    break;
                case R.styleable.MyCustomButton_make_dark_background:
                    mMakeDarkBg  = a.getBoolean(attr, false);
                    break;
            }
        }
        a.recycle();
}

I have the int[] arrays for the attributes

private static final int[] MAKE_DARK_BG_SET = { R.attr.make_dark_background };
private static final int[] STR_ATTR_ID = { R.attr.str_attr };

And those int[] arrays are merged to drawable state

@Override
protected int[] onCreateDrawableState(int extraSpace) {
    Log.i(TAG, "onCreateDrawableState()");
    final int[] drawableState = super.onCreateDrawableState(extraSpace + 2);
    if(mMakeDarkBg){
        mergeDrawableStates(drawableState, MAKE_DARK_BG_SET);
    }
    mergeDrawableStates(drawableState, STR_ATTR_ID);
    return drawableState;
}

I also have refreshDrawableState() in my attribute setter methods:

public void setMakeDarkBg(boolean makeDarkBg) {
    if(mMakeDarkBg != makeDarkBg){
        mMakeDarkBg = makeDarkBg;
        refreshDrawableState();
    }
}

public void setStrAttr(String str) {
    if(mStrAttr != str){
        mStrAttr = str;
        refreshDrawableState();
    }
}

[1] : How to add a custom button state


Solution

  • Q1:

    When you open the source-code of StateListDrawable.java, you can see this piece of code in the inflate method that reads the drawable xml selector: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/graphics/java/android/graphics/drawable/StateListDrawable.java

            ...
    
            for (i = 0; i < numAttrs; i++) {
                final int stateResId = attrs.getAttributeNameResource(i);
                if (stateResId == 0) break;
                if (stateResId == com.android.internal.R.attr.drawable) {
                    drawableRes = attrs.getAttributeResourceValue(i, 0);
                } else {
                    states[j++] = attrs.getAttributeBooleanValue(i, false)
                            ? stateResId
                            : -stateResId;
                }
            }
            ...
    

    attrs are the attributes of each <item> element in the <selector>.

    In this for-loop it gets the android:drawable, the various android:state_xxxx and custom app:xxxx attributes. All but the android:drawable attributes seem to be interpreted as booleans only: attrs.getAttributeBooleanValue(....) is called.

    I think this is the answer, based on the source code:

    You can only add custom boolean attributes to your xml, not any other type (including enums).

    Q2:

    I'm not sure why the state is merged only if it is specifically set to true. I would suspect the code should have looked like this instead:

    private static final int[] MAKE_DARK_BG_SET     = {  R.attr.make_dark_background };
    private static final int[] NOT_MAKE_DARK_BG_SET = { -R.attr.make_dark_background };
    ....
    ....
    @Override
    protected int[] onCreateDrawableState(int extraSpace) {
        Log.i(TAG, "onCreateDrawableState()");
        final int[] drawableState = super.onCreateDrawableState(extraSpace + 2);
        mergeDrawableStates(drawableState, mMakeDarkBg? MAKE_DARK_BG_SET : NOT_MAKE_DARK_BG_SET);
        //mergeDrawableStates(drawableState, STR_ATTR_ID);
        return drawableState;
    }