Como adicionar um estado de botão personalizado

132

Por exemplo, o botão padrão possui as seguintes dependências entre seus estados e imagens de plano de fundo:

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

Como posso definir meu próprio estado personalizado (como smth android:state_custom), para poder usá-lo para alterar dinamicamente a aparência visual do botão?

Vit Khudenko
fonte
Eu estava querendo estados extras para uma exibição EditText para determinar quando duas caixas de senha correspondem para mostrar uma pequena marca de seleção.
Nathan Schwermann

Respostas:

275

A solução indicada por @ (Ted Hopp) funciona, mas precisa de uma pequena correção: no seletor, os estados do item precisam de um prefixo "app:"; caso contrário, o inflador não reconhecerá corretamente o espaço para nome e falhará silenciosamente; pelo menos é o que acontece comigo.

Permita-me relatar aqui toda a solução, com mais alguns detalhes:

Primeiro, crie o arquivo "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>

Em seguida, defina sua classe personalizada. Por exemplo, pode ser uma classe "FoodButton", derivada da classe "Button". Você terá que implementar um construtor; implemente este, que parece ser o usado pelo inflador:

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

No topo da classe derivada:

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

Além disso, suas variáveis ​​de estado:

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

E alguns levantadores:

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

Em seguida, substitua a função "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, a peça mais delicada deste quebra-cabeça; o seletor que define o StateListDrawable que você usará como plano de fundo para seu widget. Este é o arquivo "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 o prefixo "app:", enquanto que com os estados padrão do android você usaria o prefixo "android:". O espaço para nome XML é crucial para uma interpretação correta do inflador e depende do tipo de projeto no qual você está adicionando atributos. Se for um aplicativo, substitua com.mydomain.mypackage pelo nome do pacote real do seu aplicativo (nome do aplicativo excluído). Se for uma biblioteca, você deverá usar "http://schemas.android.com/apk/res-auto" (e usar o Tools R17 ou posterior) ou obterá erros de tempo de execução.

Algumas notas:

  • Parece que você não precisa chamar a função "refreshDrawableState", pelo menos a solução funciona bem como está, no meu caso

  • Para usar sua classe personalizada em um arquivo xml de layout, você precisará especificar o nome completo (por exemplo, com.mydomain.mypackage.FoodButton)

  • Você pode misturar estados padrão (por exemplo, android: pressionado, android: ativado, android: selecionado) com estados personalizados, a fim de representar combinações de estado mais complicadas

Giorgio Barchiesi
fonte
3
Atualização: se a classe personalizada deriva do TextView, em vez de Button, a chamada para refreshDrawableState parece ser necessária; caso contrário, a aparência do widget não é atualizada. A chamada deve ser feita nos levantadores. Eu não tentei outras aulas. Testes realizados em um dispositivo froyo.
Giorgio Barchiesi
17
O refreshDrawableStateé definitivamente importante. Não tenho certeza absoluta de quando é realmente necessário. Mas no meu caso, isso foi necessário ao definir o estado programaticamente. Eu acho que é possivelmente chamado da classe View automaticamente no onTouchEvent. É melhor adicioná-lo no método setSelected.
Buergi
1
GiorgioBarchiesi, tenho dois Button personalizados e, quando tento alterar o status dos dois botões no evento onClick de um botão, apenas o botão clicado será alterado, acho que @buergi tem razão em que o método refreshDrawableState seja chamado no onClickEvent. Obrigado novamente por seu maravilhoso tutorial :)
Bolton
2
Mas como você pode usar estados personalizados que não são boolean? Ou os seletores estão operando apenas em booleanos?
28413 Peterdk
2
Como funciona? Quero dizer, como o atributo é atualizado para indicar verdadeiro / falso? Quem o atualiza? A combinação de drawablestate, apenas se a variável local for verdadeira, atualiza o estado ou o valor do atributo? Qual código exatamente estará atualizando R.attr.state_fried?
KAmol #
10

Este tópico mostra como adicionar estados personalizados a botões e similares. (Se você não conseguir ver os novos grupos do Google no seu navegador, há uma cópia do tópico aqui .)

Ted Hopp
fonte
+1 muito obrigado, Ted! No momento, a origem do problema desapareceu, então eu não cheguei à implementação real. No entanto, se meu cliente retornar a isso novamente, tentarei da maneira que você me indicou.
Vit Khudenko
Parece exatamente como o que eu preciso, porém a-lista estado Drawables para meus estados personalizados não estão mudando devo estar faltando alguma coisa ...
Nathan Schwermann
Você está chamando refreshDrawableState ()?
Ted Hopp
Os links estão mortos.
Mitch
@ Mitch - Bem, isso é muito ruim. Vou ver se consigo encontrar alguns links de substituição. Caso contrário, excluirei esta resposta, pois ela é basicamente inútil. Enquanto isso, a resposta aceita tem todas as informações necessárias.
Ted Hopp
6

Não se esqueça de ligar refreshDrawableStatedentro do thread da interface do usuário:

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

Demorou muito tempo para descobrir por que meu botão não está mudando de estado, mesmo que tudo pareça certo.

Nishant Soni
fonte
onde ou quando devo postar este manipulador?
Arthur Melo