How to add a custom button state
Asked Answered
O

3

150

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?

Oxytocic answered 2/12, 2010 at 14:30 Comment(1)
I was wanting extra states for an EditText view to determine when two password boxes match to show a little checkmark.Disney
I
297

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

Isaacs answered 27/4, 2011 at 14:47 Comment(17)
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.Isaacs
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.Solve
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 @Solve is right that the refreshDrawableState method gets called in the onClickEvent. Thanks again for your wonderful tutorial:)Cultivated
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.Microsporophyll
After some testing, this does conflict with duplicateParentState="true", so I've had to build a workaround. It's great knowledge nonetheless.Microsporophyll
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.Supermundane
But how can you use custom states that are not boolean? Or are selectors only operating on booleans?Depreciable
declare-styleable is not really required, although it will still generate correct attr resources. So, you can just define your attr as is.Redhanded
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 applicationIdPlio
You need to initialize attribute in constructor as well: https://mcmap.net/q/160358/-android-custom-state-button-does-not-workPredecessor
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?Rigi
It seems that now we may use xmlns:app="http://schemas.android.com/apk/res-auto" everywhere, not only in libraries. https://mcmap.net/q/160359/-difference-in-naming-xml-between-res-auto-and-com-package-name-androidAhmednagar
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.Wrightson
Actually @Ahmednagar 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"Decorate
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.Lightman
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.Geophyte
If you want to spread the state down to its children, then you have to use setDuplicateParentStateEnabled(true) in your setup. Useful in case your view is a custom ViewGroup. Also you might need to override refreshDrawableState() method to call refreshDrawableState() on every child.Feebleminded
P
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.)

Promiscuous answered 21/12, 2010 at 2:49 Comment(7)
+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.Oxytocic
Looks exactly like what I need, however the state-list drawables for my custom states aren't changing I must be missing something...Disney
Are you calling refreshDrawableState()?Promiscuous
The links are dead.Geophyte
@Geophyte -- 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.Promiscuous
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.Geophyte
@Geophyte - 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.Promiscuous
L
8

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.

Loisloise answered 25/7, 2014 at 17:44 Comment(1)
where or when should I post this handler?Hennery

© 2022 - 2024 — McMap. All rights reserved.