Botón izquierdo en estado resaltado con touchListener y clickListener

10

Tengo un problema con mi botón que permanece en un estado resaltado, después de hacer lo siguiente:

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

    }
}

Con respecto al código anterior, cuando lo uso, espero que el clic del botón se maneje con el tacto, y al devolver "verdadero" el manejo debería detenerse en el touchListener.

Pero este no es el caso. El botón permanece en un estado resaltado, aunque se llame al clic.

Lo que obtengo es:

Test - calling onClick
Test - Performing click

Por otro lado, si estoy usando el siguiente código, se hace clic en el botón, se imprime lo mismo, pero el botón no termina atascado en un estado resaltado:

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

    }
}

Estoy un poco confundido sobre cuál es la cadena de respuesta al evento táctil. Mi conjetura es que es:

1) TouchListener

2) ClickListener

3) ParentViews

¿Alguien puede confirmar esto también?

Oso blanco
fuente
Lo que realmente quieres hacer, ¿es manejarlo al tacto o cambiar de color con solo presionar?
Haider Saleem
Quiero ejecutar algo de lógica en contacto y luego llamar a performClick para que no cambie el color del botón.
Whitebear
@ Whitebear Por favor, compruebe la respuesta a continuación. Quizás pueda agregar más información.
GensaGames
Esto podría ayudarlo a comprender el flujo de eventos táctiles. No está claro lo que quieres que ocurra. ¿Quieres un controlador de clics y hacer el clic? ¿Desea que el botón pase de su color inicial al estado establecido por el filtro de color y luego vuelva a su color inicial?
Cheticamp
Déjame explicarte lo que quiero decir. Tengo un TouchListener y un ClickListener en el botón. El toque precede al clic por prioridad y devuelve verdadero si manejó el evento, lo que significa que nadie más debería manejarlo. Esto es exactamente lo que estoy haciendo a través del toque, manejar el clic y volver verdadero, sin embargo, el botón sigue resaltado aunque se llame al oyente de clic y el flujo se realice correctamente.
Whitebear

Respuestas:

10

Dichas personalizaciones no necesitan modificaciones programáticas. Puedes hacerlo simplemente en xmlarchivos. En primer lugar, elimine el setOnTouchListenermétodo que proporciona en su onCreatetotalidad. A continuación, defina un color selector en el res/colordirectorio como el siguiente. (si el directorio no existe, créelo)

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>

Ahora, configúrelo en el app:backgroundTintatributo del botón :

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

ingrese la descripción de la imagen aquí



EDITADO: (para abordar el problema del evento táctil)

Desde un punto de vista general, el flujo del evento táctil comienza desde Activity, luego fluye hacia el diseño (desde los diseños principales hasta los secundarios) y luego hacia las vistas. (Flujo LTR en la siguiente imagen)

ingrese la descripción de la imagen aquí

Cuando el evento tacto llega a la vista de destino, la vista puede controlar el evento luego decidir pasarlo a los diseños antes / actividad o no (volviendo falsede trueen onTouchel método). (Flujo RTL en la imagen de arriba)

Ahora echemos un vistazo al código fuente de la Vista para obtener una visión más profunda de los flujos de eventos táctiles. Al echar un vistazo a la implementación de la dispatchTouchEvent, veremos que si configuras una OnTouchListenervista y luego regresas trueen su onTouchmétodo, onTouchEventno se llamará a la vista.

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

Ahora, mire el onTouchEventmétodo donde está la acción del evento MotionEvent.ACTION_UP. Vemos que la acción de hacer clic ocurre allí. Por lo tanto, regresar trueen el OnTouchListener's onTouchy, en consecuencia onTouchEvent, no llamar al OnClickListener' hace que no se llame al 's onClick.

Hay otro problema con no llamar al onTouchEvent, que está relacionado con el estado presionado y que mencionó en la pregunta. Como podemos ver en el siguiente bloque de código, hay una instancia de UnsetPressedStateesas llamadas cuando se ejecuta. El resultado de no llamar es que la vista se atasca en el estado presionado y su estado de dibujo no cambia. 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);
    }
}


Con respecto a las descripciones anteriores, puede cambiar el código llamándose a setPressed(false)sí mismo para cambiar el estado dibujable donde está la acción del 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;
    }
});
aminografía
fuente
Esto no es lo que estoy buscando a mi amigo. Estoy buscando entender el cambio de comportamiento en ambas situaciones que describí anteriormente. Si hay algo que pueda explicar, hágamelo saber. No quiero eliminar el controlador táctil ni el controlador de clic. Por favor, mira mi comentario a la respuesta anterior.
Whitebear
@ Whitebear: He actualizado la respuesta. Por favor échale un vistazo, amigo.
aminografía
Esa es una buena respuesta detallada, y la aceptaría. Algunos consejos para cambiar: 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.en mi caso se llama a onClick mUnsetPressedState comprueba si es nulo antes de establecerlo en falso y tampoco se puede ejecutar el ejecutable si estamos preimpresos. Yo no entiendo muy bien cómo se deduce que debe establecerse en false
Whitebear
Se onClickllama porque estás llamando v.performClick();. Compruebe el código anterior en la MotionEvent.ACTION_UPsección nuevamente, setPressed(false)se llama de todos modos, si mUnsetPressedStatees nulo o no, si prepressedes verdadero o no. La diferencia está en la forma de llamar a setPressed(false)que podría ser a través de post/ postDelayedo directamente.
aminografía
2

Estás jugando touchy focuseventos. Comencemos por comprender el comportamiento con el mismo color. De manera predeterminada, se Selectorasigna como fondo a ButtonAndroid. Entonces, simplemente cambiando el color de fondo, make es estático (el color no cambiará). Pero no es un comportamiento nativo.

Selector podría parecerse a 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 puede ver arriba, hay estado focusedy estado pressed. Al configurar onTouchListener, manejará los eventos táctiles, que no tienen nada que ver focus.

Selectordel botón debe reemplazar el focusevento con un evento touchdurante el clic en el botón. Pero en la primera parte de su código, interceptó eventos para el touch(retorno verdadero de la devolución de llamada). El cambio de color no puede continuar y se congela con el mismo color. Y es por eso que la segunda variante (sin intercepción) está funcionando bien y esa es su confusión.

ACTUALIZAR

Todo lo que necesita hacer es cambiar el comportamiento y el color de la Selector. Por ej. utilizando el siguiente fondo para el Button. Y eliminar onTouchListenerde su implementación en absoluto.

<?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
fuente
¿Cómo cambiarías el primer ejemplo para que funcione bien?
Whitebear
No estoy buscando eliminar el oyente táctil de mi implementación. ya que quiero captar clics en una vista específica usando el controlador táctil, y luego manejarlos yo mismo (usando performClick) y devolver true desde el oyente táctil para decirle a los otros controladores que no se realiza más manejo. Los registros se imprimen bien en mi ejemplo, pero el botón aún permanece resaltado.
Whitebear
@ Whitebear No mencionaste eso en tus preguntas. De cualquier manera, puede usar tantos onTouchListeners como desee. Simplemente no necesita consumir eventos, por return true.
GensaGames
@Whitebear O Retire el selector y conjunto de color crudo al botón través backgroundColor.
GensaGames
chicos, todavía no están abordando lo que escribí en la publicación original ...
Whitebear
0

Si asigna un fondo al botón, no cambiará el color al hacer clic.

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

y configúralo como fondo para tu botón

android:background="@color/myColor"
Haider Saleem
fuente