Botão esquerdo no estado destacado com touchListener e clickListener

10

Estou tendo um problema com meu botão permanecendo em um estado destacado, depois de fazer o seguinte:

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;
            }
        });

    }
}

Com relação ao código acima, ao usá-lo, espero que o clique do botão seja manipulado pelo toque e, retornando "true", o tratamento deve parar no touchListener.

Mas esse não é o caso. O botão permanece em um estado destacado, mesmo que o clique esteja sendo chamado.

O que eu recebo é:

Test - calling onClick
Test - Performing click

por outro lado, se eu estiver usando o código a seguir, o botão é clicado, as mesmas impressões, mas o botão não fica preso no estado destacado:

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;
            }
        });

    }
}

Estou um pouco confuso quanto ao que é a cadeia de resposta ao evento de toque. Meu palpite é que é:

1) TouchListener

2) ClickListener

3) ParentViews

Alguém pode confirmar isso também?

Urso polar
fonte
O que você realmente deseja fazer é lidar com o toque ou mudar de cor com apenas pressionar?
Haider Saleem
Quero executar alguma lógica no toque e, em seguida, chamar performClick para que não mude a cor do botão.
Whitebear
@Whitebear Verifique a resposta abaixo. Talvez eu possa adicionar mais informações.
GensaGames 12/11/19
Isso pode ajudá-lo a entender o fluxo de eventos de toque. Não está claro o que você deseja que ocorra. Deseja um manipulador de cliques e para executar o clique? Deseja que o botão passe da cor inicial para o estado definido pelo filtro de cores e depois volte para a cor inicial?
Cheticamp
Deixe-me explicar o que quero dizer. Eu tenho um touchListener e um clickListener no botão. O toque precede o clique por prioridade e retorna true se ele manipulou o evento, o que significa que ninguém mais deve lidar com isso. É exatamente isso que estou fazendo com o toque, manipulando o clique e retornando verdadeiro, mas o botão ainda permanece destacado, mesmo que o ouvinte do onclick seja chamado e o fluxo seja feito corretamente.
Whitebear

Respostas:

10

Essas personalizações não precisam de modificações de programação. Você pode fazer isso simplesmente em xmlarquivos. Antes de tudo, exclua completamente o setOnTouchListenermétodo que você fornece onCreate. Em seguida, defina uma cor do seletor no res/colordiretório da seguinte maneira. (se o diretório não existir, crie-o)

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>

Agora, defina-o no app:backgroundTintatributo do botão :

<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" />


Resultado Visual:

insira a descrição da imagem aqui



EDITADO: (para resolver o problema do evento de toque)

Do ponto de vista geral, o fluxo do evento de toque começa no Activity, depois flui para o layout (dos layouts pai e filho) e depois para as visualizações. (Fluxo LTR na figura a seguir)

insira a descrição da imagem aqui

Quando o evento de toque atinge o ponto de vista alvo, a visão pode manipular o evento então decidir para passá-lo para a esquemas anteriores / actividade ou não (voltando falsede trueno onTouchmétodo). (Fluxo RTL na figura acima)

Agora, vamos dar uma olhada no código-fonte do View para obter uma visão mais profunda dos fluxos de eventos de toque. Examinando a implementação do dispatchTouchEvent, veríamos que, se você definir um OnTouchListenerpara a visualização e depois retornar trueem seu onTouchmétodo, onTouchEventa visualização não será chamada.

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;
}

Agora, observe o onTouchEventmétodo em que está a ação do evento MotionEvent.ACTION_UP. Vemos que a ação de executar clique acontece lá. Portanto, retornar trueno OnTouchListener's onTouche, consequentemente, não chamar o onTouchEvent, causa não chamar o OnClickListener' s onClick.

Há outro problema em não chamar o onTouchEvent, que está relacionado ao estado pressionado e você mencionou na pergunta. Como podemos ver no bloco de código abaixo, há uma instância UnsetPressedStatedessas chamadas quando executadas. O resultado da não chamada é que a visualização fica presa no estado pressionado e seu estado de extração não muda. 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);
    }
}


Com relação às descrições acima, você pode alterar o código chamando a setPressed(false)si mesmo para alterar o estado desenhável em que a ação do evento é 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
fonte
Não é isso que estou procurando, meu amigo. Estou procurando entender a mudança de comportamento nas duas situações que descrevi acima. Se houver algo que eu possa elaborar, entre em contato. Não quero excluir o manipulador de toque nem o manipulador de cliques. Por favor, verifique o meu comentário para a resposta acima.
Whitebear
@ Whitebear: Eu atualizei a resposta. Por favor, dê uma olhada, cara.
Aminografia 14/11/19
Essa é uma boa resposta detalhada e eu aceitaria. Algumas dicas para mudar: 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.No meu caso, o onClick é chamado. O mUnsetPressedState verifica se é nulo antes de definir como false e também o executável não tem certeza se será pré-pressionado. Eu não entendo muito bem como você deduzir que ele deve ser definido como falso
Whitebear
O onClické chamado porque você está chamando v.performClick();. Por favor, verifique o código acima na MotionEvent.ACTION_UPseção novamente, setPressed(false)é chamado assim mesmo, seja mUnsetPressedStatenulo ou não, prepressedverdadeiro ou não. A diferença está na maneira de ligar setPressed(false)que pode ser através post/ postDelayedou diretamente.
Aminografia #
2

Você está brincando touche focuseventos. Vamos começar com a compreensão do comportamento da mesma cor. Por padrão, é Selectoratribuído como plano de fundo ao ButtonAndroid. Então, simplesmente alterando a cor de fundo, make é estático (a cor não muda). Mas não é um comportamento nativo.

Selector pode parecer com este.

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

Como você pode ver acima, há estado focusede estado pressed. Ao definir, onTouchListenervocê manipulará os eventos de toque, que não têm nada a ver focus.

Selectordo botão deve substituir o focusevento touchdurante o evento de clique no botão. Mas na primeira parte do seu código, você interceptou eventos para o touch(retornando true do retorno de chamada). A mudança de cor não pode prosseguir e está congelando com a mesma cor. E é por isso que a segunda variante (sem interceptação) está funcionando bem e essa é a sua confusão.

ATUALIZAR

Tudo que você precisa fazer é mudar o comportamento e a cor do Selector. Por ex. usando o próximo plano de fundo para o Button. E remova onTouchListenerda sua implementação.

<?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
fonte
Como você mudaria o primeiro exemplo para funcionar bem?
Whitebear
Não estou olhando para remover o ouvinte de toque da minha implementação. como eu quero capturar cliques em uma visualização específica usando o manipulador de toque e manipulá-los pessoalmente (usando performClick) e retornar true a partir do ouvinte de toque para informar aos outros manipuladores que nenhum tratamento adicional é feito. Os logs são impressos corretamente no meu exemplo, mas o botão ainda permanece destacado.
Whitebear
@Whitebear Você não mencionou isso em suas perguntas. De qualquer forma, você pode usar quantos onTouchListeners quiser. Você só não precisa consumir evento por return true.
GensaGames 14/11/19
@Whitebear OU Remova o seletor e defina a cor bruta para o botão via backgroundColor.
GensaGames 14/11/19
caras, você ainda não está tratando o que eu escrevi no post original ..
Whitebear
0

se você atribuir um plano de fundo ao botão, ele não mudará a cor ao clicar.

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

e defina-o como background para o seu botão

android:background="@color/myColor"
Haider Saleem
fonte
0

você pode apenas usar chips de material para, em vez da exibição de botão. consulte: https://material.io/develop/android/components/chip, onde eles lidam com esses eventos avançados e você pode personalizar com a aplicação dos temas.

Prabudda Fernando
fonte