Jak dodać stan przycisku niestandardowego

133

Na przykład przycisk domyślny ma następujące zależności między swoimi stanami a obrazami tła:

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

Jak mogę zdefiniować własny stan niestandardowy (coś podobnego android:state_custom), aby móc go użyć do dynamicznej zmiany wyglądu przycisku?

Vit Khudenko
źródło
Chciałem mieć dodatkowe stany dla widoku EditText, aby określić, kiedy pasują dwa pola haseł, aby wyświetlić mały znacznik wyboru.
Nathan Schwermann

Odpowiedzi:

277

Rozwiązanie wskazane przez @ (Ted Hopp) działa, ale wymaga niewielkiej korekty: w selektorze stany pozycji wymagają prefiksu „app:”, w przeciwnym razie inflater nie rozpozna poprawnie przestrzeni nazw i po cichu zawiedzie; przynajmniej tak się ze mną dzieje.

Pozwólcie, że opiszę tutaj całe rozwiązanie, z kilkoma szczegółami:

Najpierw utwórz plik „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>

Następnie zdefiniuj swoją klasę niestandardową. Na przykład może to być klasa „FoodButton” pochodząca z klasy „Button”. Będziesz musiał zaimplementować konstruktora; wdrożyć ten, który wydaje się być tym używanym przez inflatera:

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

Oprócz klasy pochodnej:

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

Ponadto zmienne stanu:

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

I kilka seterów:

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

Następnie zastąp funkcję „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;
}

Wreszcie najdelikatniejszy element tej układanki; selektor definiujący StateListDrawable, którego będziesz używać jako tła dla widgetu. To jest plik „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>

Zwróć uwagę na prefiks „app:”, podczas gdy w standardowych stanach Androida użyłbyś prefiksu „android:”. Przestrzeń nazw XML jest kluczowa dla poprawnej interpretacji przez inflater i zależy od typu projektu, w którym dodajesz atrybuty. Jeśli jest to aplikacja, zamień com.mydomain.mypackage na rzeczywistą nazwę pakietu aplikacji (bez nazwy aplikacji). Jeśli jest to biblioteka, musisz użyć „http://schemas.android.com/apk/res-auto” (i używać narzędzi w wersji 17 lub nowszej), w przeciwnym razie wystąpią błędy w czasie wykonywania.

Kilka uwag:

  • Wygląda na to, że nie musisz wywoływać funkcji "refreshDrawableState", przynajmniej rozwiązanie działa dobrze tak jak w moim przypadku

  • Aby użyć własnej klasy w pliku xml układu, musisz określić w pełni kwalifikowaną nazwę (np. Com.mydomain.mypackage.FoodButton)

  • Możesz dowolnie mieszać stany standardowe (np. Android: wciśnięty, android: włączony, android: wybrany) ze stanami niestandardowymi, aby przedstawić bardziej skomplikowane kombinacje stanów

Giorgio Barchiesi
źródło
3
Aktualizacja: jeśli klasa niestandardowa pochodzi od TextView, a nie od Button, wywołanie refreshDrawableState wydaje się konieczne, w przeciwnym razie wygląd widżetu nie zostanie zaktualizowany. Wezwanie powinno być skierowane do rozgrywających. Nie próbowałem innych klas. Testy wykonane na urządzeniu froyo.
Giorgio Barchiesi
17
To refreshDrawableStatejest zdecydowanie ważne. Nie jestem absolutnie pewien, kiedy jest to naprawdę potrzebne. Ale w moim przypadku było to potrzebne przy programowym ustawianiu stanu. Myślę, że jest to prawdopodobnie wywoływane automatycznie z klasy View w zdarzeniu onTouchEvent. Lepiej dodam to w metodzie setSelected.
buergi
1
GiorgioBarchiesi, mam dwa niestandardowe przyciski, a kiedy spróbuję zmienić stan obu przycisków ze zdarzenia onClick jednego przycisku, tylko kliknięty przycisk zostanie zmieniony, myślę, że @buergi ma rację, że metoda refreshDrawableState zostaje wywołana w onClickEvent. Jeszcze raz dziękuję za wspaniały tutorial :)
Bolton,
2
Ale jak można używać stanów niestandardowych, które nie są boolean? A może selektory działają tylko na logicznych?
Peterdk
2
Jak to działa? Mam na myśli, w jaki sposób atrybut jest aktualizowany, aby wskazywał na prawdę / fałsz? Kto to aktualizuje? Czy scalenie drawablestate, tylko jeśli zmienna lokalna jest prawdziwa, aktualizuje stan lub wartość atrybutu? Który kod dokładnie będzie aktualizował R.attr.state_fried?
kAmol
10

W tym wątku pokazano, jak dodawać niestandardowe stany do przycisków i tym podobnych. (Jeśli nie widać nowych grup Google w swojej przeglądarce, nie kopię wątku tutaj .)

Ted Hopp
źródło
+1 wielkie dzięki, Ted! W tej chwili źródło problemu zniknęło, więc nie doszedłem do właściwej implementacji. Jednak jeśli mój klient wróci do tego ponownie, spróbuję tak, jak mi wskazałeś.
Vit Khudenko,
Wygląda dokładnie tak, jak potrzebuję, jednak lista stanów do rysowania dla moich niestandardowych stanów się nie zmienia. Muszę czegoś brakować ...
Nathan Schwermann,
Czy wywołujesz refreshDrawableState ()?
Ted Hopp,
Linki są martwe.
Mitch
1
@Mitch - Cóż, szkoda. Zobaczę, czy znajdę jakieś linki zastępcze. Jeśli nie, usunę tę odpowiedź, ponieważ jest w zasadzie bezużyteczna. W międzyczasie zaakceptowana odpowiedź zawiera wszystkie potrzebne informacje.
Ted Hopp
7

Nie zapomnij zadzwonić refreshDrawableStatew wątku UI:

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

Zajęło mi dużo czasu, aby dowiedzieć się, dlaczego mój przycisk nie zmienia swojego stanu, mimo że wszystko wygląda dobrze.

Nishant Soni
źródło
gdzie i kiedy powinienem wysłać tego handlera?
Arthur Melo,