Cómo agregar un estado de botón personalizado

132

Por ejemplo, el botón predeterminado tiene las siguientes dependencias entre sus estados e imágenes de fondo:

<?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>

¿Cómo puedo definir mi propio estado personalizado (como algo android:state_custom), para poder usarlo para cambiar dinámicamente la apariencia visual de mi botón?

Vit Khudenko
fuente
Quería estados adicionales para una vista EditText para determinar cuándo coinciden dos cuadros de contraseña para mostrar una pequeña marca de verificación.
Nathan Schwermann el

Respuestas:

275

La solución indicada por @ (Ted Hopp) funciona, pero necesita una pequeña corrección: en el selector, los estados del elemento necesitan un prefijo "app:", de lo contrario el inflador no reconocerá el espacio de nombres correctamente y fallará en silencio; Al menos esto es lo que me pasa.

Permítame informar aquí la solución completa, con algunos detalles más:

Primero, cree el archivo "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>

Luego defina su clase personalizada. Por ejemplo, puede ser una clase "FoodButton", derivada de la clase "Button". Tendrás que implementar un constructor; implementar este, que parece ser el utilizado por el inflador:

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

En la parte superior de la clase derivada:

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

Además, sus variables de estado:

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

Y un par de setters:

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

Luego anule la función "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;
}

Finalmente, la pieza más delicada de este rompecabezas; el selector que define el StateListDrawable que usará como fondo para su widget. Este es el archivo "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>

Observe el prefijo "app:", mientras que con los estados estándar de Android, habría utilizado el prefijo "android:". El espacio de nombres XML es crucial para que el inflador interprete correctamente y depende del tipo de proyecto en el que esté agregando atributos. Si es una aplicación, reemplace com.mydomain.mypackage con el nombre real del paquete de su aplicación (excluido el nombre de la aplicación). Si se trata de una biblioteca, debe usar "http://schemas.android.com/apk/res-auto" (y usar las Herramientas R17 o posterior) o obtendrá errores de tiempo de ejecución.

Un par de notas:

  • Parece que no necesita llamar a la función "refreshDrawableState", al menos la solución funciona bien como es, en mi caso

  • Para utilizar su clase personalizada en un archivo xml de diseño, deberá especificar el nombre completo (por ejemplo, com.mydomain.mypackage.FoodButton)

  • Puede combinar estados estándar (p. Ej., Android: presionado, android: habilitado, android: seleccionado) con estados personalizados, para representar combinaciones de estados más complicadas

Giorgio Barchiesi
fuente
3
Actualización: si la clase personalizada deriva de TextView, en lugar de Button, la llamada a refreshDrawableState parece ser necesaria; de lo contrario, la apariencia del widget no se actualiza. La llamada se colocará en los emisores. No he probado otras clases. Pruebas realizadas en un dispositivo froyo.
Giorgio Barchiesi
17
El refreshDrawableStatedefinitivamente es importante. No estoy absolutamente seguro de cuándo es realmente necesario. Pero en mi caso era necesario al establecer el estado mediante programación. Supongo que posiblemente se llama desde la clase View automáticamente en onTouchEvent. Será mejor que lo agregue en el método setSelected.
buergi
1
GiorgioBarchiesi, tengo dos botones personalizados, y cuando trato de cambiar el estado de ambos botones del evento onClick de un botón, solo se cambiará el botón presionado, creo que @buergi tiene razón en que se llama al método refreshDrawableState en el onClickEvent. Gracias de nuevo por tu maravilloso tutorial :)
Bolton
2
Pero, ¿cómo puede usar estados personalizados que no lo son boolean? ¿O los selectores solo funcionan en booleanos?
Peterdk
2
¿Como funciona? Quiero decir, ¿cómo se actualiza el atributo para decir verdadero / falso? ¿Quién lo actualiza? ¿La fusión de drawablestate, solo si la variable local es verdadera, actualiza el estado o el valor del atributo? ¿Qué código actualizará exactamente R.attr.state_fried?
kAmol
10

Este hilo muestra cómo agregar estados personalizados a botones y similares. (Si no puede ver los nuevos grupos de Google en el navegador, no hay una copia de la rosca aquí .)

Ted Hopp
fuente
+1 muchas gracias, Ted! En este momento, el origen del problema se ha ido, así que no llegué a la implementación real. Sin embargo, si mi cliente vuelve a esto nuevamente, intentaré la forma en que me indicó.
Vit Khudenko
Es exactamente igual a lo que necesito, sin embargo el dibujables-lista del estado de mis estados personalizados no están cambiando Debo estar perdiendo algo ...
Nathan Schwermann
¿Estás llamando a refreshDrawableState ()?
Ted Hopp
Los enlaces están muertos.
Mitch
@ Mitch - Bueno, eso está muy mal. Veré si puedo encontrar algunos enlaces de reemplazo. Si no, eliminaré esta respuesta, ya que es básicamente inútil como es. Mientras tanto, la respuesta aceptada tiene toda la información necesaria.
Ted Hopp
6

No olvides llamar refreshDrawableStatedentro del hilo de la interfaz de usuario:

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

Me tomó mucho tiempo descubrir por qué mi botón no cambia su estado a pesar de que todo parece correcto.

Nishant Soni
fuente
¿Dónde o cuándo debo publicar este controlador?
Arthur Melo