Es necesario tener un cuidado especial cuando se controlan eventos táctiles en un ViewGroup
, ya que es común que un ViewGroup
tenga elementos secundarios que sean objetivos para eventos táctiles diferentes a los del ViewGroup
en sí mismo. A fin de asegurarte de que cada vista reciba correctamente los eventos táctiles destinados a ella, anula el método onInterceptTouchEvent()
.
Consulta los siguientes recursos relacionados:
Como interceptar eventos táctiles en un ViewGroup
Cada vez que se detecta un evento táctil en la superficie de un onInterceptTouchEvent()
, incluida la superficie de sus elementos secundarios, se llama al método ViewGroup
. Si onInterceptTouchEvent()
muestra true
, se intercepta el MotionEvent
, lo que significa que no se pasa al elemento secundario, sino al método onTouchEvent()
del elemento superior.
El método onInterceptTouchEvent()
le da a un elemento superior la oportunidad de ver cualquier evento táctil antes de que lo hagan sus elementos secundarios. Si muestras true
desde onInterceptTouchEvent()
, la vista secundaria que antes controlaba eventos táctiles recibirá un objeto ACTION_CANCEL
y los eventos que se produzcan a partir de ese momento, se enviarán al método onTouchEvent()
del elemento superior para su control habitual.
El método onInterceptTouchEvent()
también puede mostrar false
y simplemente espiar a los eventos mientras pasan por la jerarquía de vistas hacia sus objetivos habituales, que controlarán los eventos con su propio objeto onTouchEvent()
.
En el siguiente fragmento, la clase MyViewGroup
extiende ViewGroup
.
MyViewGroup
contiene varias vistas secundarias. Si arrastraras el dedo horizontalmente sobre una vista secundaria, esta ya no debería recibir eventos táctiles, y MyViewGroup
debería controlar los eventos táctiles desplazando su contenido. Sin embargo, si presionas botones en la vista secundaria o desplazas la vista secundaria verticalmente, el elemento superior no debe interceptar esos eventos táctiles, porque el elemento secundario es el objetivo pretendido. En esos casos, onInterceptTouchEvent()
debería mostrar false
y no se llamará al método onTouchEvent()
de MyViewGroup
.
Kotlin
class MyViewGroup @JvmOverloads constructor( context: Context, private val mTouchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop ) : ViewGroup(context) { ... override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { /* * This method JUST determines whether we want to intercept the motion. * If we return true, onTouchEvent will be called and we do the actual * scrolling there. */ return when (ev.actionMasked) { // Always handle the case of the touch gesture being complete. MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { // Release the scroll. mIsScrolling = false false // Do not intercept touch event, let the child handle it } MotionEvent.ACTION_MOVE -> { if (mIsScrolling) { // We're currently scrolling, so yes, intercept the // touch event! true } else { // If the user has dragged her finger horizontally more than // the touch slop, start the scroll // left as an exercise for the reader val xDiff: Int = calculateDistanceX(ev) // Touch slop should be calculated using ViewConfiguration // constants. if (xDiff > mTouchSlop) { // Start scrolling! mIsScrolling = true true } else { false } } } ... else -> { // In general, we don't want to intercept touch events. They should be // handled by the child view. false } } } override fun onTouchEvent(event: MotionEvent): Boolean { // Here we actually handle the touch event (e.g. if the action is ACTION_MOVE, // scroll this container). // This method will only be called if the touch event was intercepted in // onInterceptTouchEvent ... } }
Java
public class MyViewGroup extends ViewGroup { private int mTouchSlop; ... ViewConfiguration vc = ViewConfiguration.get(view.getContext()); mTouchSlop = vc.getScaledTouchSlop(); ... @Override public boolean onInterceptTouchEvent(MotionEvent ev) { /* * This method JUST determines whether we want to intercept the motion. * If we return true, onTouchEvent will be called and we do the actual * scrolling there. */ final int action = MotionEventCompat.getActionMasked(ev); // Always handle the case of the touch gesture being complete. if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { // Release the scroll. mIsScrolling = false; return false; // Do not intercept touch event, let the child handle it } switch (action) { case MotionEvent.ACTION_MOVE: { if (mIsScrolling) { // We're currently scrolling, so yes, intercept the // touch event! return true; } // If the user has dragged her finger horizontally more than // the touch slop, start the scroll // left as an exercise for the reader final int xDiff = calculateDistanceX(ev); // Touch slop should be calculated using ViewConfiguration // constants. if (xDiff > mTouchSlop) { // Start scrolling! mIsScrolling = true; return true; } break; } ... } // In general, we don't want to intercept touch events. They should be // handled by the child view. return false; } @Override public boolean onTouchEvent(MotionEvent ev) { // Here we actually handle the touch event (e.g. if the action is ACTION_MOVE, // scroll this container). // This method will only be called if the touch event was intercepted in // onInterceptTouchEvent ... } }
Ten en cuenta que ViewGroup
también proporciona un método requestDisallowInterceptTouchEvent()
.
La clase ViewGroup
llama a este método cuando un elemento secundario no quiere que el elemento superior y sus elementos superiores intercepten eventos táctiles con onInterceptTouchEvent()
.
Cómo procesar eventos ACTION_OUTSIDE
Si un ViewGroup
recibe un MotionEvent
con un ACTION_OUTSIDE
, no se enviará el evento a sus elementos secundarios de forma predeterminada. Para procesar un MotionEvent
con ACTION_OUTSIDE
, anula dispatchTouchEvent(MotionEvent event)
para enviarlo al objeto View
correspondiente o contrólalo en el objeto Window.Callback
relevante (por ejemplo, Activity
).
Cómo usar las constantes de ViewConfiguration
En el fragmento anterior, se usa la clase ViewConfiguration
actual para inicializar una variable llamada mTouchSlop
. Puedes usar la clase ViewConfiguration
para acceder a distancias, velocidades y tiempos normales que usa el sistema Android.
"Margen táctil" se refiere a la distancia en píxeles en que el toque de un usuario puede oscilar antes de que se interprete el gesto como desplazamiento. Por lo general, se usa el margen táctil para evitar realizar un desplazamiento accidentalmente cuando el usuario está realizando alguna operación táctil, por ejemplo, tocando elementos en pantalla.
Los otros dos métodos ViewConfiguration
que se usan con frecuencia son getScaledMinimumFlingVelocity()
y getScaledMaximumFlingVelocity()
.
Estos métodos muestran las velocidades (medidas en píxeles por segundo) mínima y máxima, respectivamente, para iniciar un lanzamiento. Por ejemplo:
Kotlin
private val vc: ViewConfiguration = ViewConfiguration.get(context) private val mSlop: Int = vc.scaledTouchSlop private val mMinFlingVelocity: Int = vc.scaledMinimumFlingVelocity private val mMaxFlingVelocity: Int = vc.scaledMaximumFlingVelocity ... MotionEvent.ACTION_MOVE -> { ... val deltaX: Float = motionEvent.rawX - mDownX if (Math.abs(deltaX) > mSlop) { // A swipe occurred, do something } return false } ... MotionEvent.ACTION_UP -> { ... if (velocityX in mMinFlingVelocity..mMaxFlingVelocity && velocityY < velocityX) { // The criteria have been satisfied, do something } }
Java
ViewConfiguration vc = ViewConfiguration.get(view.getContext()); private int mSlop = vc.getScaledTouchSlop(); private int mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); private int mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); ... case MotionEvent.ACTION_MOVE: { ... float deltaX = motionEvent.getRawX() - mDownX; if (Math.abs(deltaX) > mSlop) { // A swipe occurred, do something } ... case MotionEvent.ACTION_UP: { ... } if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity && velocityY < velocityX) { // The criteria have been satisfied, do something } }
Cómo extender el área táctil de una vista secundaria
Android proporciona la clase TouchDelegate
para permitir que un elemento superior extienda el área táctil de una vista secundaria más allá de los límites del elemento secundario.
Esto resulta útil cuando el elemento secundario tiene que ser pequeño, pero debe tener una región táctil más grande. Si es necesario, también puedes usar este enfoque para reducir la región táctil del elemento secundario.
En el siguiente ejemplo, un objeto ImageButton
es la "vista delegada" (es decir, el elemento secundario cuya área táctil extenderá el elemento superior).
Aquí puedes ver el archivo de diseño:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/parent_layout" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" > <ImageButton android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@null" android:src="@drawable/icon" /> </RelativeLayout>
El siguiente fragmento hace lo siguiente:
- Obtiene la vista superior y publica una interfaz
Runnable
en el subproceso de IU. Esto garantiza que el elemento superior disponga sus elementos secundarios antes de llamar al métodogetHitRect()
. El métodogetHitRect()
obtiene el rectángulo de visita del elemento secundario (área táctil) en las coordenadas del elemento superior. - Busca la vista secundaria de
ImageButton
y llama agetHitRect()
para obtener los límites del área táctil del elemento secundario. - Extiende los límites del rectángulo de visita de
ImageButton
. - Crea una instancia de la clase
TouchDelegate
, que pasa el rectángulo de visita expandido y la vista secundaria deImageButton
como parámetros. - Establece la clase
TouchDelegate
en la vista superior, de modo que los toques dentro de los límites de delegados táctiles se enrutan al elemento secundario.
En su calidad de delegado táctil para la vista secundaria ImageButton
, la vista superior recibirá todos los eventos táctiles. Si el evento táctil ocurrió dentro del rectángulo de visita del elemento secundario, el elemento superior pasará el evento táctil al elemento secundario para que lo controle.
Kotlin
public class MainActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // Post in the parent's message queue to make sure the parent // lays out its children before you call getHitRect() findViewById<View>(R.id.parent_layout).post { // The bounds for the delegate view (an ImageButton // in this example) val delegateArea = Rect() val myButton = findViewById<ImageButton>(R.id.button).apply { isEnabled = true setOnClickListener { Toast.makeText( this@MainActivity, "Touch occurred within ImageButton touch region.", Toast.LENGTH_SHORT ).show() } // The hit rectangle for the ImageButton getHitRect(delegateArea) } // Extend the touch area of the ImageButton beyond its bounds // on the right and bottom. delegateArea.right += 100 delegateArea.bottom += 100 // Sets the TouchDelegate on the parent view, such that touches // within the touch delegate bounds are routed to the child. (myButton.parent as? View)?.apply { // Instantiate a TouchDelegate. // "delegateArea" is the bounds in local coordinates of // the containing view to be mapped to the delegate view. // "myButton" is the child view that should receive motion // events. touchDelegate = TouchDelegate(delegateArea, myButton) } } } }
Java
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Get the parent view View parentView = findViewById(R.id.parent_layout); parentView.post(new Runnable() { // Post in the parent's message queue to make sure the parent // lays out its children before you call getHitRect() @Override public void run() { // The bounds for the delegate view (an ImageButton // in this example) Rect delegateArea = new Rect(); ImageButton myButton = (ImageButton) findViewById(R.id.button); myButton.setEnabled(true); myButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Toast.makeText(MainActivity.this, "Touch occurred within ImageButton touch region.", Toast.LENGTH_SHORT).show(); } }); // The hit rectangle for the ImageButton myButton.getHitRect(delegateArea); // Extend the touch area of the ImageButton beyond its bounds // on the right and bottom. delegateArea.right += 100; delegateArea.bottom += 100; // Instantiate a TouchDelegate. // "delegateArea" is the bounds in local coordinates of // the containing view to be mapped to the delegate view. // "myButton" is the child view that should receive motion // events. TouchDelegate touchDelegate = new TouchDelegate(delegateArea, myButton); // Sets the TouchDelegate on the parent view, such that touches // within the touch delegate bounds are routed to the child. if (View.class.isInstance(myButton.getParent())) { ((View) myButton.getParent()).setTouchDelegate(touchDelegate); } } }); } }