Przycisk pozostawiony w podświetlonym stanie z touchListener i clickListener

10

Mam problem z pozostawieniem podświetlonego przycisku po wykonaniu następujących czynności:

public class MainActivity extends AppCompatActivity {

    @SuppressLint("ClickableViewAccessibility")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        AppCompatButton button = (AppCompatButton) findViewById(R.id.mybutton);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("Test", "calling onClick");
            }
        });
        button.setOnTouchListener(new View.OnTouchListener() {

            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN: {
                        v.getBackground().setColorFilter(0xe0f47521,PorterDuff.Mode.SRC_ATOP);
                        v.invalidate();
                        break;
                    }
                    case MotionEvent.ACTION_UP: {
                        v.getBackground().clearColorFilter();
                        v.invalidate();
                        v.performClick();
                        Log.d("Test", "Performing click");
                        return true;
                    }
                }
                return false;
            }
        });

    }
}

Jeśli chodzi o powyższy kod, podczas jego używania oczekuję, że kliknięcie przycisku będzie obsługiwane przez dotyk, a po zwróceniu „true” obsługa powinna zatrzymać się na touchListener.

Ale tak nie jest. Przycisk pozostaje podświetlony, nawet jeśli wywoływane jest kliknięcie.

Dostaję to:

Test - calling onClick
Test - Performing click

z drugiej strony, jeśli używam następującego kodu, przycisk jest klikany, drukowane są te same, ale przycisk nie zatrzymuje się w wyróżnionym stanie:

public class MainActivity extends AppCompatActivity {

    @SuppressLint("ClickableViewAccessibility")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        AppCompatButton button = (AppCompatButton) findViewById(R.id.mybutton);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("Test", "calling onClick");
            }
        });
        button.setOnTouchListener(new View.OnTouchListener() {

            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN: {
                        v.getBackground().setColorFilter(0xe0f47521,PorterDuff.Mode.SRC_ATOP);
                        v.invalidate();
                        break;
                    }
                    case MotionEvent.ACTION_UP: {
                        v.getBackground().clearColorFilter();
                        v.invalidate();
                        // v.performClick();
                        Log.d("Test", "Performing click");
                        return false;
                    }
                }
                return false;
            }
        });

    }
}

Jestem trochę zdezorientowany co do łańcucha odpowiedzi na zdarzenie dotykowe. Domyślam się, że to:

1) TouchListener

2) ClickListener

3) ParentViews

Czy ktoś może to również potwierdzić?

Biały niedźwiedź
źródło
To, co naprawdę chcesz zrobić, to obsługa dotykowa lub zmiana koloru po naciśnięciu?
Haider Saleem
Chcę uruchomić logikę w kontakcie, a następnie wywołać performClick, aby nie zmieniała koloru przycisku.
Whitebear,
@Whitebear Sprawdź odpowiedź poniżej. Może mogę dodać więcej informacji.
GensaGames,
Może to pomóc w zrozumieniu przepływu zdarzeń dotykowych. Nie jest jasne, co chcesz wystąpić. Czy chcesz obsługiwać kliknięcia i wykonać kliknięcie wykonania? Czy chcesz, aby przycisk przeszedł z początkowego koloru do stanu ustawionego przez filtr kolorów, a następnie z powrotem do początkowego koloru?
Cheticamp
Pozwól mi wyjaśnić, co mam na myśli. Mam przycisk TouchListener i clickListener na przycisku. Dotyk poprzedza kliknięcie według priorytetu i zwraca true, jeśli obsłużył zdarzenie, co oznacza, że ​​nikt inny nie powinien go obsłużyć. To właśnie robię za pomocą dotyku, obsługując kliknięcie i zwracając wartość true, ale przycisk nadal pozostaje podświetlony, nawet jeśli wywoływany jest odbiornik onclick, a przepływ jest wykonywany poprawnie.
Whitebear

Odpowiedzi:

10

Takie dostosowania nie wymagają modyfikacji programowych. Możesz to zrobić po prostu w xmlplikach. Przede wszystkim usuń setOnTouchListenermetodę podaną w onCreatecałości. Następnie zdefiniuj kolor selektora w res/colorkatalogu, jak poniżej. (jeśli katalog nie istnieje, utwórz go)

res / color / button_tint_color.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="#e0f47521" android:state_pressed="true" />
    <item android:color="?attr/colorButtonNormal" android:state_pressed="false" />
</selector>

Teraz ustaw go na app:backgroundTintatrybut przycisku :

<androidx.appcompat.widget.AppCompatButton
    android:id="@+id/mybutton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button"
    app:backgroundTint="@color/button_tint_color" />


Wynik wizualny:

wprowadź opis zdjęcia tutaj



EDYCJA: (w celu rozwiązania problemu ze zdarzeniem dotykowym)

Z ogólnego punktu widzenia przepływ zdarzenia dotyku rozpoczyna się od Activity, a następnie spływa do układu (od nadrzędnego do podrzędnego), a następnie do widoków. (Przepływ LTR na poniższym obrazku)

wprowadź opis zdjęcia tutaj

Gdy zdarzenie dotykowy osiągnięciu docelowego obrazu, widok w stanie obsłużyć zdarzenie zdecydować, aby przekazać je do stanu układy / działania, czy nie (powrót falseod truew onTouchsposób). (Przepływ RTL na powyższym zdjęciu)

Teraz spójrzmy na kod źródłowy widoku , aby uzyskać głębszy wgląd w przepływy zdarzeń dotykowych. Patrząc na implementację dispatchTouchEvent, zobaczymy, że jeśli ustawisz OnTouchListenerna widok, a następnie powrócisz truew jego onTouchmetodzie, onTouchEventwidok nie zostanie wywołany.

public boolean dispatchTouchEvent(MotionEvent event) {
    // removed lines for conciseness...
    boolean result = false;    
    // removed lines for conciseness...
    if (onFilterTouchEventForSecurity(event)) {
        // removed lines for conciseness...
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) { // <== right here!
            result = true;
        }
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    // removed lines for conciseness...
    return result;
}

Teraz spójrz na onTouchEventmetodę, w której znajduje się akcja zdarzenia MotionEvent.ACTION_UP. Widzimy, że dzieje się tam akcja perform-click. Tak więc powrót truedo OnTouchListener„a” onTouchi w konsekwencji nie wywołanie „ onTouchEventpowoduje” wywołanie OnClickListener„” onClick.

Jest jeszcze jeden problem z nie wywołaniem onTouchEvent, który jest związany ze stanem wciśniętym i wspomnianym w pytaniu. Jak widać w poniższym bloku kodu, istnieje instancja takich UnsetPressedStatewywołań, gdy jest uruchomiona. W wyniku braku wywoływania widok utknie w stanie wciśniętym, a jego stan do rysowania nie zmieni się. setPressed(false)setPressed(false)

public boolean onTouchEvent(MotionEvent event) {
    // removed lines for conciseness...
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                // removed lines for conciseness...
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    // removed lines for conciseness...
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // removed lines for conciseness...
                        if (!focusTaken) {
                            // Use a Runnable and post this rather than calling
                            // performClick directly. This lets other visual state
                            // of the view update before click actions start.
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClickInternal();
                            }
                        }
                    }
                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }
                    if (prepressed) {
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                        // If the post failed, unpress right now
                        mUnsetPressedState.run();
                    }
                    // removed lines for conciseness...
                }
                // removed lines for conciseness...
                break;
            // removed lines for conciseness...
        }
        return true;
    }
    return false;
}

UnsetPressedState :

private final class UnsetPressedState implements Runnable {
    @Override
    public void run() {
        setPressed(false);
    }
}


Jeśli chodzi o powyższe opisy, możesz zmienić kod, dzwoniąc setPressed(false)do siebie, aby zmienić stan rysowania, w którym akcja zdarzenia to MotionEvent.ACTION_UP:

button.setOnTouchListener(new View.OnTouchListener() {

    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                v.getBackground().setColorFilter(0xe0f47521,PorterDuff.Mode.SRC_ATOP);
                v.invalidate();
                break;
            }
            case MotionEvent.ACTION_UP: {
                v.getBackground().clearColorFilter();
                // v.invalidate();
                v.setPressed(false);
                v.performClick();
                Log.d("Test", "Performing click");
                return true;
            }
        }
        return false;
    }
});
aminografia
źródło
Nie tego szukam mojego przyjaciela. Chcę zrozumieć zmianę w zachowaniu w obu sytuacjach opisanych powyżej. Jeśli mogę coś rozwinąć, daj mi znać. Nie chcę usuwać modułu dotykowego ani modułu obsługi kliknięć. Proszę sprawdzić mój komentarz do powyższej odpowiedzi.
Whitebear,
@Whitebear: Zaktualizowałem odpowiedź. Proszę to sprawdzić, stary.
aminografia
To dobra szczegółowa odpowiedź i przyjąłbym ją. Kilka wskazówek do zmiany: Now, look at the onTouchEvent method where the event action is MotionEvent.ACTION_UP. We see that perform-click action happens there. So, returning true in the OnTouchListener's onTouch and consequently not calling the onTouchEvent, causes not calling the OnClickListener's onClick.W moim przypadku wywoływany jest onClick. mUnsetPressedState sprawdza, czy ma wartość null przed ustawieniem wartości false, a także uruchamialność nie jest pewna, czy zostanie uruchomiona, jeśli jesteśmy przygotowani. Nie do końca rozumiem, jak można odjąć, że należy ustawić wartość false
Whitebear,
onClickNazywa ponieważ dzwonisz v.performClick();. Sprawdź ponownie powyższy kod w MotionEvent.ACTION_UPsekcji, w setPressed(false)każdym razie jest wywoływany, niezależnie od tego, czy mUnsetPressedStatema wartość zerową, czy prepressedjest prawdą, czy nie. Różnica polega na sposobie wywoływania, setPressed(false)który może odbywać się bezpośrednio post/ postDelayedbezpośrednio.
aminografia
2

Bałaganisz touchi focuswydarzenia. Zacznijmy od zrozumienia zachowania w tym samym kolorze. Domyślnie jest Selectorprzypisany jako tło dla ButtonAndroida. Po prostu zmieniając kolor tła, make jest statyczny (kolor się nie zmieni). Ale to nie jest rodzime zachowanie.

Selector może wyglądać tak

<?xml version="1.0" encoding="utf-8"?> 
  <selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:state_focused="true"
        android:state_pressed="true"
        android:drawable="@drawable/bgalt" />

    <item
        android:state_focused="false"
        android:state_pressed="true"
        android:drawable="@drawable/bgalt" />

    <item android:drawable="@drawable/bgnorm" />
</selector>

Jak widać powyżej, istnieje stan focusedi stan pressed. Ustawiając onTouchListener, będziesz obsługiwać zdarzenia dotykowe, które nie mają z tym nic wspólnego focus.

Selectorprzycisku powinien zastąpić focuszdarzenie touchpodczas zdarzenia kliknięcia na przycisku. Ale w pierwszej części kodu przechwyciłeś zdarzenia dla touch(zwracając wartość true z wywołania zwrotnego). Zmiana koloru nie może być kontynuowana i marznie z tym samym kolorem. I dlatego drugi wariant (bez przechwytywania) działa dobrze i to jest twoje zamieszanie.

AKTUALIZACJA

Wszystko, co musisz zrobić, to zmienić zachowanie i kolor Selector. Np. za pomocą następnego tła dla Button. IonTouchListener w ogóle usuń ze swojej implementacji.

<?xml version="1.0" encoding="utf-8"?> 
  <selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:state_pressed="true"
        android:drawable="@color/color_pressed" />

    <item android:drawable="@color/color_normal" />
</selector>
GensaGames
źródło
Jak w takim razie zmieniłby się pierwszy przykład, aby działał dobrze?
Whitebear,
Nie chcę usunąć detektora dotyku z mojej implementacji. ponieważ chcę przechwytywać kliknięcia określonego widoku za pomocą funkcji obsługi dotykowej, a następnie obsługiwać je osobiście (za pomocą performClick) i zwracać wartość true za pomocą funkcji nasłuchiwania dotykowego, aby poinformować inne programy obsługi, że nie jest wykonywana żadna dalsza obsługa. Dzienniki są dobrze wydrukowane w moim przykładzie, ale przycisk nadal jest podświetlony.
Whitebear,
@Whitebear Nie wspomniałeś o tym w swoich pytaniach. W dowolny sposób możesz użyć dowolnej liczby onTouchListeners. Po prostu nie musisz spożywać wydarzeń return true.
GensaGames,
@Whitebear LUB Usuń selektor i ustaw surowy kolor przycisku za pomocą backgroundColor.
GensaGames,
chłopaki, nadal nie zajmujecie się tym, co napisałem w oryginalnym poście ..
Whitebear,
0

jeśli przypiszesz tło do przycisku, nie zmieni on koloru po kliknięciu.

 <color name="myColor">#000000</color>

i ustaw go jako tło dla swojego przycisku

android:background="@color/myColor"
Haider Saleem
źródło
0

możesz po prostu użyć żetonów materiału zamiast widoku przycisków. patrz: https://material.io/develop/android/components/chip tam obsługują te zdarzenia hililghted i można je dostosować za pomocą motywów.

Prabudda Fernando
źródło