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?
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();
}
}
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;
}