137

For instance, the default button has the following dependencies between its states and background images:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_window_focused="false" android:state_enabled="true"
        android:drawable="@drawable/btn_default_normal" />
    <item android:state_window_focused="false" android:state_enabled="false"
        android:drawable="@drawable/btn_default_normal_disable" />
    <item android:state_pressed="true" 
        android:drawable="@drawable/btn_default_pressed" />
    <item android:state_focused="true" android:state_enabled="true"
        android:drawable="@drawable/btn_default_selected" />
    <item android:state_enabled="true"
        android:drawable="@drawable/btn_default_normal" />
    <item android:state_focused="true"
        android:drawable="@drawable/btn_default_normal_disable_focused" />
    <item
        android:drawable="@drawable/btn_default_normal_disable" />
</selector>

How can I define my own custom state (smth like android:state_custom), so then I could use it to dynamically change my button visual appearance?

GabrielOshiro
  • 6,951
  • 4
  • 40
  • 53
Vit Khudenko
  • 27,639
  • 10
  • 56
  • 86

3 Answers3

285

The solution indicated by @(Ted Hopp) works, but needs a little correction: in the selector, the item states need an "app:" prefix, otherwise the inflater won't recognise the namespace correctly, and will fail silently; at least this is what happens to me.

Allow me to report here the whole solution, with some more details:

First, create file "res/values/attrs.xml":

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="food">
        <attr name="state_fried" format="boolean" />
        <attr name="state_baked" format="boolean" />
    </declare-styleable>
</resources>

Then define your custom class. For instance, it may be a class "FoodButton", derived from class "Button". You will have to implement a constructor; implement this one, which seems to be the one used by the inflater:

public FoodButton(Context context, AttributeSet attrs) {
    super(context, attrs);
}

On top of the derived class:

private static final int[] STATE_FRIED = {R.attr.state_fried};
private static final int[] STATE_BAKED = {R.attr.state_baked};

Also, your state variables:

private boolean mIsFried = false;
private boolean mIsBaked = false;

And a couple of setters:

public void setFried(boolean isFried) {mIsFried = isFried;}
public void setBaked(boolean isBaked) {mIsBaked = isBaked;}

Then override function "onCreateDrawableState":

@Override
protected int[] onCreateDrawableState(int extraSpace) {
    final int[] drawableState = super.onCreateDrawableState(extraSpace + 2);
    if (mIsFried) {
        mergeDrawableStates(drawableState, STATE_FRIED);
    }
    if (mIsBaked) {
        mergeDrawableStates(drawableState, STATE_BAKED);
    }
    return drawableState;
}

Finally, the most delicate piece of this puzzle; the selector defining the StateListDrawable that you will use as a background for your widget. This is file "res/drawable/food_button.xml":

<?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.mydomain.mypackage">
<item
    app:state_baked="true"
    app:state_fried="false"
    android:drawable="@drawable/item_baked" />
<item
    app:state_baked="false"
    app:state_fried="true"
    android:drawable="@drawable/item_fried" />
<item
    app:state_baked="true"
    app:state_fried="true"
    android:drawable="@drawable/item_overcooked" />
<item
    app:state_baked="false"
    app:state_fried="false"
    android:drawable="@drawable/item_raw" />
</selector>

Notice the "app:" prefix, whereas with standard android states you would have used prefix "android:". The XML namespace is crucial for a correct interpretation by the inflater and depends on the type of project in which you are adding attributes. If it is an application, replace com.mydomain.mypackage with the actual package name of your application (application name excluded). If it is a library you must use "http://schemas.android.com/apk/res-auto" (and be using Tools R17 or later) or you will get runtime errors.

A couple of notes:

  • It seems you don't need to call the "refreshDrawableState" function, at least the solution works well as is, in my case

  • In order to use your custom class in a layout xml file, you will have to specify the fully qualified name (e.g. com.mydomain.mypackage.FoodButton)

  • You can as weel mix-up standard states (e.g. android:pressed, android:enabled, android:selected) with custom states, in order to represent more complicated state combinations

Neil Miller
  • 1,034
  • 8
  • 10
Giorgio Barchiesi
  • 5,707
  • 3
  • 29
  • 33
  • 4
    Update: if the custom class derives from TextView, rather than Button, the call to refreshDrawableState appears to be necessary, otherwise the widget appearance is not updated. The call shall be placed in the setters. I have not tried other classes. Tests performed on a froyo device. – Giorgio Barchiesi Apr 28 '11 at 17:09
  • 17
    The `refreshDrawableState` is definitely important. I'm not absolutely sure when it's really needed. But in my case it was needed when setting the state programmatically. I guess it is possibly called from the View class automatically in the onTouchEvent. I'd better add it in the setSelected method. – buergi Aug 10 '11 at 00:55
  • 1
    GiorgioBarchiesi, I have two custom Button, and when I try to change the status of both the buttons from the onClick event of one button, only the clicked button will get changed, I think @buergi is right that the refreshDrawableState method gets called in the onClickEvent. Thanks again for your wonderful tutorial:) – Bolton Oct 18 '11 at 09:58
  • Can this be used with `duplicateParentState="true"`? I have a row of `ImageViews` that aren't individually focusable and use the parent state, but I'd like to add some visual cue to one of them at a time. – Karakuri Jun 06 '12 at 11:10
  • After some testing, this does conflict with duplicateParentState="true", so I've had to build a workaround. It's great knowledge nonetheless. – Karakuri Jun 06 '12 at 16:25
  • I've used this solution and it works great! But I have 1 thing to update, when you call `setBaked()` or `setFried()` it does not call automatically the `onCreateDrawableState()` so you have to call `refreshDrawableState()`.If you change native state of button right after the new state change, then you don't have to refresh since `onCreateDrawableState()` will be called by the native change state. – Yaniv Jan 06 '13 at 09:37
  • 2
    But how can you use custom states that are not `boolean`? Or are selectors only operating on booleans? – Peterdk May 28 '13 at 19:48
  • `declare-styleable` is not really required, although it will still generate correct `attr` resources. So, you can just define your `attr` as is. – Dmitry Zaytsev Mar 07 '14 at 12:23
  • 1
    If for someone this selector doesn't work it's most likely that you put wrong `xmlns:` in selector. It always should be `xmlns:myapp="http://schemas.android.com/apk/res/com.example.asd"` with **res** . Also get package name not from the _manifest_ file but rather that from you app.build _applicationId_ – murt Aug 18 '16 at 14:02
  • You need to initialize attribute in constructor as well: http://stackoverflow.com/a/39382662/1639556 – Leos Literak Sep 08 '16 at 05:06
  • 2
    How does it work? I mean, how the attribute gets updated to state true/false? Who updates it? Does merging drawablestate, only if local variable is true, updates the state or value of attribute? Which code exactly will be updating R.attr.state_fried? – kAmol Apr 04 '17 at 10:46
  • 2
    It seems that now we may use `xmlns:app="http://schemas.android.com/apk/res-auto"` everywhere, not only in libraries. https://stackoverflow.com/a/30620868/6182136 – Artyom Mar 10 '18 at 17:02
  • I did all of this but for some reason my buttons don't do anything when I press them, I set setOnClickListener(...) (without crashing, as I'm getting the actual buttons instead of a null pointer) but nothing happens, it never enters the code from the click listener. – Francisco Peters Apr 09 '18 at 19:21
  • 2
    Actually @Artyom is right. We should use `xmlns:app="http://schemas.android.com/apk/res-auto"` instead of `"http://schemas.android.com/apk/res/com.mydomain.mypackage"` – Jan Radzikowski Sep 17 '19 at 10:32
  • It works, but for some reason on preview in `Android Studio` it thinks that my custom attribute is always `true`. Well, there are no problem on devices. – Ircover Dec 11 '19 at 09:03
  • Custom attributes that are not boolean are still broken for working with selector drawables. The code is from version 1 of Android, so as of this comment, they have always been broken. – Mitch Jun 04 '20 at 00:59
10

This thread shows how to add custom states to buttons and the like. (If you can't see the new Google groups in your browser, there's a copy of the thread here.)

Ted Hopp
  • 222,293
  • 47
  • 371
  • 489
  • +1 thanks a lot, Ted! Right now origin of the trouble has gone so I did not get to the actual implementation. However should my customer return to this again I will try the way you pointed me to. – Vit Khudenko Dec 21 '10 at 08:23
  • Looks exactly like what I need, however the state-list drawables for my custom states aren't changing I must be missing something... – Nathan Schwermann Dec 24 '10 at 06:10
  • Are you calling refreshDrawableState()? – Ted Hopp Jan 13 '11 at 04:59
  • The links are dead. – Mitch Jun 02 '20 at 20:56
  • 1
    @Mitch -- Well, that's too bad. I'll see if I can find some replacement links. If not, I'll delete this answer, as it's basically useless as is. Meanwhile, the accepted answer has all the info needed. – Ted Hopp Jun 03 '20 at 00:54
  • The accepted answer is wrong. The bug has been around since version 1, so I suspect Google will not be fixing it. Simply put, the answer will not work for anything but boolean. The answer doesn't qualify this, so the accepted answer is wrong. I did mark it in the comments for what that's worth. – Mitch Jun 04 '20 at 01:07
  • @Mitch - I was under the impression that state lists (color state list; drawable state list) only work with boolean attributes by design. For each state in the selector, attributes can be tested to be explicitly false, explicitly true, or irrelevant when matching to the actual state. I don't see how a non-boolean attribute would fit be used in this scheme. – Ted Hopp Jun 04 '20 at 03:04
7

Please do not forget to call refreshDrawableState within UI thread:

mHandler.post(new Runnable() {
    @Override
    public void run() {
        refreshDrawableState();
    }
});

It took lot of my time to figure out why my button is not changing its state even though everything looks right.

GabrielOshiro
  • 6,951
  • 4
  • 40
  • 53
Nishant Soni
  • 628
  • 1
  • 9
  • 19