Android ViewGroup触摸事件拦截详解

前言

在自定义ViewGroup中,有时候需要实现触摸事件拦截,比如ListView下拉刷新就是典型的触摸事件拦截的例子。触摸事件拦截就是在触摸事件被parent view拦截,而不会分发给其child,即使触摸发生在该child身上。被拦截的事件会转到parent view的onTouchEvent方法中进行处理。但是这个交互过程还是挺复杂的,有多种情况,今天我们就来分析一下吧。这篇分析文章已经放了一段时间了,如果有任何问题请高人指出。

触摸事件的分发

简单来说触摸事件的分发会经过这么几个顺序,dispatchTouchEvent --> onInterceptTouchEvent --> onTouchEvent,事件拦截就在onInterceptTouchEvent方法中进行,在该方法中返回true即代表拦截触摸事件。触摸事件的分发是一个典型的隧道事件,即从上到下的过程。从视图树角度上来说,就是触摸事件会从父视图挨个传递到子视图。比如一个LinearLayout中又一个TextView,当触摸这个TextView时触摸事件会先打到LinearLayout,然后再到达TextView。如果LinearLayout将触摸事件拦截了,那么TextView就会收到一个CANCEL事件,其他触摸就收不到了。但是触摸事件的处理过程是一个冒泡事件,还是以上面的TextView为例,正常情况下,事件从上到下分发到TextView上,TextView则会对该事件进行处理,如果TextView处理了该事件,即TextView的dispatchTouchEvent返回了true, 那么该事件就被消费了。但是如果TextView的dispatchTouchEvent返回的是false, 则代表这个事件没有被处理,此时该事件就会从下到上(即从child 到 view group的过程)找parent view进行处理。如果parent view也没有处理,那么最终会交给Activity (如果是Activity窗口) 的onTouchEvent来处理。下面就是ViewGroup的事件分发过程,更详细的资料请参考Android Touch事件分发过程

触摸事件的拦截

ViewGroup对于事件的拦截是一个复杂的流程,如果你想对触摸事件进行拦截,那么你需要覆写onInterceptTouchEvent方法,并且返回true。然后后续的事件就会被转移到该ViewGroup的onTouchEvent方法进行处理,而在后续的事件处理过程中onInterceptTouchEvent中也不会收到后续事件,因此你也需要覆写onTouchEvent方法。我们首先看看onInterceptTouchEvent方法的官方说明 :

public boolean onInterceptTouchEvent (MotionEvent ev)

Implement this method to intercept all touch screen motion events. This allows you to watch events as they are dispatched to your children, and take ownership of the current gesture at any point.

Using this function takes some care, as it has a fairly complicated interaction with View.onTouchEvent(MotionEvent), and using it requires implementing that method as well as this one in the correct way. Events will be received in the following order:

You will receive the down event here.
    The down event will be handled either by a child of this view group, or given to your own onTouchEvent() method to handle; this means you should implement onTouchEvent() to return true, so you will continue to see the rest of the gesture (instead of looking for a parent view to handle it). Also, by returning true from onTouchEvent(), you will not receive any following events in onInterceptTouchEvent() and all touch processing must happen in onTouchEvent() like normal.

For as long as you return false from this function, each following event (up to and including the final up) will be delivered first here and then to the target‘s onTouchEvent().

If you return true from here, you will not receive any following events: the target view will receive the same event but with the action ACTION_CANCEL, and all further events will be delivered to your onTouchEvent() method and no longer appear here.
翻译如下 :

实现这个方法来拦截所有触摸事件。这会使得您可以监控到所有分发到你的子视图的事件,然后您可以随时控制当前的手势。
使用这个方法您需要花些精力,因为它与View.onTouchEvent(MotionEvent)的交互非常复杂,并且要想使用这个功能还需要把当前ViewGroup的onTouchEvent方法和子控件的onTouchEvent方法正确地结合在一起使用。事件获取顺序如下:

你将从这里开始接收ACTION_DOWN触摸事件。
ACTION_DOWN触摸事件可以由该ViewGroup自己处理,也可以由它的子控件的onTouchEvent进行处理;这就意味着你需要实现onTouchEvent(MotionEvent)方法并且返回true,这样你才可以接收到后续的事件(以免会继续寻找父控件进行处理)。如果你在onTouchEvent(MotionEvent)返回了true,那么在onInterceptTouchEvent()方法中您将不会再收到后续的事件,所有这些后续的事件(例如您在ACTION_DOWN中返回了true,那么ACTION_MOVE, ACTION_UP这些成为后续事件)将会被本类的onTouchEvent(MotionEvent)方法中被处理。

************
只要您在onInterceptTouchEvent方法中返回false,每个后续的事件(从当前事件到最后ACTION_UP事件)将会先分发到onInterceptTouchEvent中,然后再交给目标子控件的onTouchEvent处理 (前提是子控件的onTouchEvent返回是true )。

如果在onInterceptTouchEvent返回true,onInterceptTouchEvent方法中将不会收到后续的任何事件,目标子控件中除了ACTION_CANCEL外也不会接收所有这些后续事件,所有的后续事件将会被交付到你自己的onTouchEvent()方法中。
************

触摸事件拦截示例

TouchLayout类 ( ViewGroup )

import android.content.Context;
import android.support.v4.view.MotionEventCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.FrameLayout;
import android.widget.Scroller;

public class TouchLayout extends FrameLayout {

    private String TAG = TouchLayout.class.getSimpleName();

    public TouchLayout(Context context) {
        super(context);

    }

    public TouchLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TouchLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
//        setClickable(true);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean result = super.dispatchTouchEvent(ev) ;

        return result;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //
        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) {
            // Do not intercept touch event, let the child handle it
            return false;
        }

        TouchUtils.showEventInfo(TAG + "#   onInterceptTouchEvent", action);

        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        TouchUtils.showEventInfo(TAG + "# *** onTouchEvent", ev.getAction());
        Log.d(TAG, "### is Clickable = " + isClickable());
         return super.onTouchEvent(ev);
//        return true;
    }

}

TouchTv ( View 类型)

public class TouchTv extends TextView {

    private String TAG = TouchTv.class.getSimpleName();

    public TouchTv(Context context) {
        this(context, null);
    }

    public TouchTv(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TouchTv(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
//        setClickable(true);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        TouchUtils.showEventInfo(TAG + "#dispatchTouchEvent", ev.getAction());
        boolean result = super.dispatchTouchEvent(ev);
        Log.d(TAG, "### dispatchTouchEvent result = " + result);
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        TouchUtils.showEventInfo(TAG + "#onTouchEvent", ev.getAction());
        boolean result = super.onTouchEvent(ev);
        Log.d(TAG, "### onTouchEvent result = " + result);
        return result;
    }
}

Activity :

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.touch_event_intercept);

        View myView = findViewById(R.id.my_button);

        ValueAnimator colorAnim = ObjectAnimator.ofInt(myView,
                "backgroundColor", /* Red */
                0xFFFF8080, /* Blue */0xFF8080FF);
        colorAnim.setDuration(3000);
        colorAnim.setEvaluator(new ArgbEvaluator());
        colorAnim.setRepeatCount(ValueAnimator.INFINITE);
        colorAnim.setRepeatMode(ValueAnimator.REVERSE);
        colorAnim.start();

        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(myView, "scaleX",
                0.5f);
        objectAnimator.setDuration(3000);
        objectAnimator.setRepeatMode(ObjectAnimator.REVERSE);
        objectAnimator.start();

        Log.d("", "### Activiti中getWindow()获取的类型是 : " + this.getWindow());

        // state list
        StateListDrawable stateListDrawable = new StateListDrawable();
        stateListDrawable.addState(new int[] {
                android.R.attr.state_enabled
        }, getResources().getDrawable(R.drawable.ic_launcher));
        stateListDrawable.addState(new int[] {
                android.R.attr.state_pressed
        }, getResources().getDrawable(R.drawable.ic_launcher));

    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // Log.d("", "### activity dispatchTouchEvent");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        TouchUtils.showEventInfo("activity onTouchEvent", event.getAction());
        return super.onTouchEvent(event);
    }

}

touch_event_intercept.xml :

<myview.TouchLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    tools:context="com.example.touch_event.MainActivity"
    tools:ignore="MergeRootFrame" >

    <myview.TouchTv
        android:id="@+id/my_button"
        android:layout_width="match_parent"
        android:layout_height="160dp"
        android:layout_gravity="center"
        android:layout_margin="20dp"
        android:background="#00aa00"
        android:gravity="center"
        android:text="@string/hello_world"
        android:textSize="30sp" />

</myview.TouchLayout>

情景分析

以下的情景的触摸事件都是在TouchTv的范围内。

情景1

条件 :

在TouchLayout的onInterceptTouchEvent中返回true进行事件拦截, 在TouchLayout的onTouchEvent中返回 true消费事件;

说明 : 

触摸事件被拦截,后续的事件不会进入到onInterceptTouchEvent ( If you return true from here, you will not receive any following events ),而直接进入TouchLayout的onTouchEvent方法进行处理。onTouchEvent返回true,表明事件被消费了,不会再冒泡给上面的Parent进行处理;

输出  : 

// 事件拦截
10-01 20:22:52.892: D/TouchLayout#   onInterceptTouchEvent(407): ### action -->  ACTION_DOWN
// 处理
10-01 20:22:52.892: D/TouchLayout# *** onTouchEvent(407): ### action -->  ACTION_DOWN
// DOWN的后续事件不经过onInterceptTouchEvent,直接交给TouchLayout的onTouchEvent处理
10-01 20:22:52.917: D/TouchLayout# *** onTouchEvent(407): ### action -->  ACTION_MOVE
10-01 20:22:52.937: D/TouchLayout# *** onTouchEvent(407): ### action -->  ACTION_MOVE
10-01 20:22:52.957: D/TouchLayout# *** onTouchEvent(407): ### action -->  ACTION_MOVE
10-01 20:22:52.977: D/TouchLayout# *** onTouchEvent(407): ### action -->  ACTION_MOVE
10-01 20:22:52.977: D/TouchLayout# *** onTouchEvent(407): ### action -->  ACTION_MOVE
10-01 20:22:52.997: D/TouchLayout# *** onTouchEvent(407): ### action -->  ACTION_MOVE
10-01 20:22:53.017: D/TouchLayout# *** onTouchEvent(407): ### action -->  ACTION_MOVE
10-01 20:22:53.017: D/TouchLayout# *** onTouchEvent(407): ### action -->  ACTION_UP

情景2

条件 : 

在TouchLayout的onInterceptTouchEvent中在ACTION_MOVE事件时返回true进行事件拦截, TouchTv的onTouchEvent中返回false,在TouchLayout的onTouchEvent中返回 true消费事件;

说明 : 

事件被拦截之前,会被分发给TouchTv的onTouchEvent进行处理;触摸事件被拦截之后,后续的事件不会进入到onInterceptTouchEvent,也不会交给TouchTv的onTouchEvent进行处理,而是直接进入TouchLayout的onTouchEvent方法进行处理。onTouchEvent返回true,表明事件被消费了,不会再冒泡给上面的Parent进行处理;

输出  : 

// DOWN中没有对事件进行拦截,因此可以被TouchTv进行处理
10-01 20:32:05.017: D/TouchLayout#   onInterceptTouchEvent(573): ### action -->  ACTION_DOWN
// TouchTv事件分发
10-01 20:32:05.017: D/TouchTv#dispatchTouchEvent(573): ### action -->  ACTION_DOWN
// TouchTv对事件进行处理,TouchTv的onTouchEvent返回false,导致事件交给TouchLayout的onTouchEvent处理
10-01 20:32:05.017: D/TouchTv#onTouchEvent(573): ### action -->  ACTION_DOWN
// TouchLayout的onTouchEvent处理DOWN事件
10-01 20:32:05.017: D/TouchLayout# *** onTouchEvent(573): ### action -->  ACTION_DOWN
// TouchLayout的onTouchEvent处理后续事件
10-01 20:32:05.062: D/TouchLayout# *** onTouchEvent(573): ### action -->  ACTION_MOVE
10-01 20:32:05.082: D/TouchLayout# *** onTouchEvent(573): ### action -->  ACTION_MOVE
10-01 20:32:05.267: D/TouchLayout# *** onTouchEvent(573): ### action -->  ACTION_MOVE
10-01 20:32:05.287: D/TouchLayout# *** onTouchEvent(573): ### action -->  ACTION_MOVE
10-01 20:32:05.307: D/TouchLayout# *** onTouchEvent(573): ### action -->  ACTION_MOVE
10-01 20:32:05.307: D/TouchLayout# *** onTouchEvent(573): ### action -->  ACTION_MOVE
10-01 20:32:05.312: D/TouchLayout# *** onTouchEvent(573): ### action -->  ACTION_UP

情景3

条件 : 

在TouchLayout的onInterceptTouchEvent中返回true进行事件拦截, 但在TouchLayout的onTouchEvent中返回FALSE, 导致事件没有被消费;

说明 : 

触摸事件被拦截,后续的事件不会进入到onInterceptTouchEvent,而直接进入TouchLayout的onTouchEvent方法进行处理。onTouchEvent返回false,表明事件没有被消费,需要交给parent处理,如果最终该事件没有被处理,那么事件交给Activity的onTouchEvent处理。

输出  : 

// 事件拦截onInterceptTouchEvent
10-01 20:16:03.617: D/TouchLayout#   onInterceptTouchEvent(32675): ### action -->  ACTION_DOWN
// 事件处理onTouchEvent
10-01 20:16:03.617: D/TouchLayout# *** onTouchEvent(32675): ### action -->  ACTION_DOWN
// TouchLayout的dispatchTouchEvent最终返回了false,
10-01 20:16:03.617: D/TouchLayout(32675): ### dispatchTouchEvent, return false
// 事件没有被处理,最终交给了Activity的onTouchEvent处理
10-01 20:16:03.617: D/activity onTouchEvent(32675): ### action -->  ACTION_DOWN
10-01 20:16:03.697: D/activity onTouchEvent(32675): ### action -->  ACTION_MOVE
10-01 20:16:03.712: D/activity onTouchEvent(32675): ### action -->  ACTION_MOVE
10-01 20:16:03.732: D/activity onTouchEvent(32675): ### action -->  ACTION_MOVE
10-01 20:16:03.882: D/activity onTouchEvent(32675): ### action -->  ACTION_MOVE
10-01 20:16:03.897: D/activity onTouchEvent(32675): ### action -->  ACTION_MOVE
10-01 20:16:03.917: D/activity onTouchEvent(32675): ### action -->  ACTION_UP

情景4

条件 : 

在TouchLayout的onInterceptTouchEvent中返回FALSE,不对触摸进行事件拦截, TouchLayout的onTouchEvent中返回true,但在TouchTv的onTouchEvent中返回FALSE, 导致事件没有被消费;

说明 : 

触摸事件不进行拦截,因此事件最终进入到TouchTv的onTouchEvent,但其返回false,表明事件没有被消费,需要交给parent处理,如果最终该事件没有被处理,那么事件交给Activity的onTouchEvent处理。

输出  : 

// TouchLayout不对事件进行拦截
10-01 20:43:04.682: D/TouchLayout#   onInterceptTouchEvent(814): ### action -->  ACTION_DOWN
// 事件被TouchTv分发
10-01 20:43:04.682: D/TouchTv#dispatchTouchEvent(814): ### action -->  ACTION_DOWN
// 事件被TouchTv处理
10-01 20:43:04.682: D/TouchTv#onTouchEvent(814): ### action -->  ACTION_DOWN
// 事件被TouchTv的处理结果为false,因此该事件需要找parent来处理
10-01 20:43:04.682: D/TouchTv(814): ### dispatchTouchEvent result = false
// 事件被交给TouchTv的parent的onTouchEvent处理,即TouchLayout的onTouchEvent,该方法返回true
// 因此后续事件继续交给TouchLayout处理
10-01 20:43:04.682: D/TouchLayout# *** onTouchEvent(814): ### action -->  ACTION_DOWN
10-01 20:43:04.727: D/TouchLayout# *** onTouchEvent(814): ### action -->  ACTION_MOVE
10-01 20:43:04.747: D/TouchLayout# *** onTouchEvent(814): ### action -->  ACTION_MOVE
10-01 20:43:04.872: D/TouchLayout# *** onTouchEvent(814): ### action -->  ACTION_MOVE
10-01 20:43:04.892: D/TouchLayout# *** onTouchEvent(814): ### action -->  ACTION_MOVE
10-01 20:43:04.892: D/TouchLayout# *** onTouchEvent(814): ### action -->  ACTION_UP

情景5

条件 : 

在TouchLayout的onInterceptTouchEvent中返回FALSE,不对触摸进行事件拦截, 但在TouchTv的onTouchEvent中返回true, 导致事件被消费;

说明 : 

触摸事件不进行拦截,因此事件最终进入到TouchTv的onTouchEvent,但其返回true,表明事件没有被消费,那么后续事件将会先到TouchLayout的onInterceptTouchEvent,然后再分发给TouchTv。原文描述为 : For as long as you return false from this function, each following event (up to and including the final up) will be delivered first here and then to the target‘s onTouchEvent().

输出  : 

// TouchLayout不拦截事件,因此事件分发给TouchTv进行处理,而TouchTv的处理结果为true,因此后续的事件将会先从
// TouchLayout的onInterceptTouchEvent再到TouchTv的onTouchEvent
10-01 20:48:49.612: D/TouchLayout#   onInterceptTouchEvent(1030): ### action -->  ACTION_DOWN
// TouchTv处理事件
10-01 20:48:49.612: D/TouchTv#dispatchTouchEvent(1030): ### action -->  ACTION_DOWN
10-01 20:48:49.612: D/TouchTv#onTouchEvent(1030): ### action -->  ACTION_DOWN
10-01 20:48:49.612: D/TouchTv(1030): ### dispatchTouchEvent result = true
// 后续事件从TouchLayout的onInterceptTouchEvent再到TouchTv的onTouchEvent
10-01 20:48:49.697: D/TouchLayout#   onInterceptTouchEvent(1030): ### action -->  ACTION_MOVE
10-01 20:48:49.697: D/TouchTv#dispatchTouchEvent(1030): ### action -->  ACTION_MOVE
10-01 20:48:49.697: D/TouchTv#onTouchEvent(1030): ### action -->  ACTION_MOVE
10-01 20:48:49.697: D/TouchTv(1030): ### dispatchTouchEvent result = true
10-01 20:48:49.717: D/TouchLayout#   onInterceptTouchEvent(1030): ### action -->  ACTION_MOVE
10-01 20:48:49.717: D/TouchTv#dispatchTouchEvent(1030): ### action -->  ACTION_MOVE
10-01 20:48:49.717: D/TouchTv#onTouchEvent(1030): ### action -->  ACTION_MOVE
10-01 20:48:49.717: D/TouchTv(1030): ### dispatchTouchEvent result = true
// UP事件直接在TouchTv中进行分发
10-01 20:48:49.782: D/TouchTv#dispatchTouchEvent(1030): ### action -->  ACTION_UP
10-01 20:48:49.782: D/TouchTv#onTouchEvent(1030): ### action -->  ACTION_UP
10-01 20:48:49.782: D/TouchTv(1030): ### dispatchTouchEvent result = true

情景6

条件 : 

在TouchLayout的onInterceptTouchEvent中的DOWN事件中返回FALSE,但在MOVE事件中返回true, 且TouchTv的onTouchEvent返回true。

说明 : 

触摸事件对DOWN事件不进行拦截,因此TouchTv可以正常的处理。但是在MOVE时对事件进行了拦截,那么TouchTv就无法接收到MOVE以及后面的事件了,它会收到一个CANCEL事件,后续的事件将会被TouchLayout的onTouchEvent进行处理。( If you return true from here, you will not receive any following events: the target view will receive the same event but with the action ACTION_CANCEL, and all further events will be delivered to your onTouchEvent() method and no longer appear here. )

输出  : 

// TouchLayout不对DOWN进行拦截
10-01 20:56:37.642: D/TouchLayout#   onInterceptTouchEvent(1205): ### action -->  ACTION_DOWN
// TouchTv分发与处理DOWN事件
10-01 20:56:37.642: D/TouchTv#dispatchTouchEvent(1205): ### action -->  ACTION_DOWN
10-01 20:56:37.642: D/TouchTv#onTouchEvent(1205): ### action -->  ACTION_DOWN
10-01 20:56:37.642: D/TouchTv(1205): ### dispatchTouchEvent result = true
// TouchLayout对MOVE事件进行拦截
10-01 20:56:37.712: D/TouchLayout#   onInterceptTouchEvent(1205): ### action -->  ACTION_MOVE
// TouchTv收到一个CANCEL事件,然后不会不到MOVE以及后续的事件
10-01 20:56:37.712: D/TouchTv#dispatchTouchEvent(1205): ### action -->  ACTION_CANCEL
10-01 20:56:37.712: D/TouchTv#onTouchEvent(1205): ### action -->  ACTION_CANCEL
10-01 20:56:37.712: D/TouchTv(1205): ### dispatchTouchEvent result = true
// MOVE以及后续事件被TouchLayout处理
10-01 20:56:37.727: D/TouchLayout# *** onTouchEvent(1205): ### action -->  ACTION_MOVE
10-01 20:56:37.747: D/TouchLayout# *** onTouchEvent(1205): ### action -->  ACTION_MOVE
10-01 20:56:37.762: D/TouchLayout# *** onTouchEvent(1205): ### action -->  ACTION_MOVE
10-01 20:56:37.777: D/TouchLayout# *** onTouchEvent(1205): ### action -->  ACTION_MOVE
10-01 20:56:37.797: D/TouchLayout# *** onTouchEvent(1205): ### action -->  ACTION_MOVE
10-01 20:56:37.997: D/TouchLayout# *** onTouchEvent(1205): ### action -->  ACTION_MOVE
10-01 20:56:38.012: D/TouchLayout# *** onTouchEvent(1205): ### action -->  ACTION_MOVE
10-01 20:56:38.017: D/TouchLayout# *** onTouchEvent(1205): ### action -->  ACTION_UP

总结

以上的几种情况就是我们经常遇到的了,总结起来有几个重要的点 :

1、如果Parent ViewGroup的onInterceptTouchEvent返回false, 并且触摸的目标view对于触摸事件的处理结果返回的是true,那么后续事件会先经过parent 的onInterceptTouchEvent, 然后再交给目标view进行处理;

2、如果Parent ViewGroup的onInterceptTouchEvent返回true,即对事件进行拦截,那么事件将不会再经过onInterceptTouchEvent,而是直接进入到onTouchEvent进行处理;如果onTouchEvent返回true,则表示该事件被处理了;如果返回FALSE,则代表事件没有被处理,那么事件会被上交给它的parent来处理,如果没有parent来处理,那么最终会交给Activity来处理;

3、如果用户在触摸的某个事件才拦截,那么目标view会收到一个CANCEL事件,然后后续的事件不会再交给目标view,而被转交给Parent的onTouchEvent方法进行处理。比如情景6当中,在TouchLayout的DOWN时不对事件进行拦截,这时事件会被TouchTv正常处理。但是在MOVE时事件被拦截了,此时TouchTv收到了一个CANCEL事件,MOVE以及后续的事件就交给了TouchLayout进行处理。这个情景就是我们做下拉刷新时要用的场景了。

我们结合ViewGroup的事件分发过程来验证吧。代码如下 :

/**
 * {@inheritDoc}
 */
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (!onFilterTouchEventForSecurity(ev)) {
        return false;
    }  

    final int action = ev.getAction();
    final float xf = ev.getX();
    final float yf = ev.getY();
    final float scrolledXFloat = xf + mScrollX;
    final float scrolledYFloat = yf + mScrollY;
    final Rect frame = mTempRect;
    // 是否禁用拦截,如果为true表示不能拦截事件;反之,则为可以拦截事件
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    // ACTION_DOWN事件,即按下事件
    if (action == MotionEvent.ACTION_DOWN) {
        if (mMotionTarget != null) {
            mMotionTarget = null;
        }
        // If we‘re disallowing intercept or if we‘re allowing and we didn‘t
        // intercept。如果不允许事件拦截或者不拦截该事件,那么执行下面的操作
        if (disallowIntercept || !onInterceptTouchEvent(ev))         // 1、是否禁用拦截、是否拦截事件的判断
            // reset this event‘s action (just to protect ourselves)
            ev.setAction(MotionEvent.ACTION_DOWN);
            // We know we want to dispatch the event down, find a child
            // who can handle it, start with the front-most child.
            final int scrolledXInt = (int) scrolledXFloat;
            final int scrolledYInt = (int) scrolledYFloat;
            final View[] children = mChildren;
            final int count = mChildrenCount;  

            for (int i = count - 1; i >= 0; i--)        // 2、迭代所有子view,查找触摸事件在哪个子view的坐标范围内
                final View child = children[i];
                // 该child是可见的
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {
                    // 3、获取child的坐标范围
                    child.getHitRect(frame);
                    // 4、判断发生该事件坐标是否在该child坐标范围内
                    if (frame.contains(scrolledXInt, scrolledYInt))
                        // offset the event to the view‘s coordinate system
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                        // 5、child处理该事件,如果返回true,那么mMotionTarget为该child。正常情况下,
                        // dispatchTouchEvent(ev)的返回值即onTouchEcent的返回值。因此onTouchEcent如果返回为true,
                        // 那么mMotionTarget为触摸事件所在位置的child。
                        if (child.dispatchTouchEvent(ev))
                            // 6、 mMotionTarget为该child
                            mMotionTarget = child;
                            return true;
                        }  

                    }
                }
            }
        }
    }// end if  

    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
            (action == MotionEvent.ACTION_CANCEL);  

    if (isUpOrCancel) {
        // Note, we‘ve already copied the previous state to our local
        // variable, so this takes effect on the next event
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }  

  	// 触摸事件的目标view, 即触摸所在的view
    final View target = mMotionTarget;
    // 7、如果mMotionTarget为空,那么执行super.super.dispatchTouchEvent(ev),
    // 即View.dispatchTouchEvent(ev),就是该View Group自己处理该touch事件,只是又走了一遍View的分发过程而已.
    // 拦截事件或者在不拦截事件且target view的onTouchEvent返回false的情况都会执行到这一步.
    if (target == null) {
        // We don‘t have a target, this means we‘re handling the
        // event as a regular view.
        ev.setLocation(xf, yf);
        if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            ev.setAction(MotionEvent.ACTION_CANCEL);
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        }
        return super.dispatchTouchEvent(ev);
    }  

    // 8、如果没有禁用事件拦截,并且onInterceptTouchEvent(ev)返回为true,即进行事件拦截.  ( 似乎总走不到这一步 ??? )
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        ev.setAction(MotionEvent.ACTION_CANCEL);
        ev.setLocation(xc, yc);
        //
        if (!target.dispatchTouchEvent(ev)) {
            // target didn‘t handle ACTION_CANCEL. not much we can do
            // but they should have.
        }
        // clear the target
        mMotionTarget = null;
        // Don‘t dispatch this event to our own view, because we already
        // saw it when intercepting; we just want to give the following
        // event to the normal onTouchEvent().
        return true;
    }  

    if (isUpOrCancel) {
        mMotionTarget = null;
    }  

    // finally offset the event to the target‘s coordinate system and
    // dispatch the event.
    final float xc = scrolledXFloat - (float) target.mLeft;
    final float yc = scrolledYFloat - (float) target.mTop;
    ev.setLocation(xc, yc);  

    if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
        ev.setAction(MotionEvent.ACTION_CANCEL);
        target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        mMotionTarget = null;
    }
    // 9、事件不拦截,且target view在ACTION_DOWN时返回true,那么后续事件由target来处理事件
    return target.dispatchTouchEvent(ev);
}  

如果不对事件进来拦截,且TouchTv对事件的处理返回true,那么在DOWN事件时,mMotionTarget就是TouchTv,后续的事件就会通过注释9来处理,即直接交给TouvhTv来处理。如果在DOWN时就拦截事件,那么mMotionTarget为空,则会执行注释7出的代码,一直调用super.dispatchTouchEvent处理事件,即调用本类的事件处理,最终会调用onTouchEvent方法。如果在DOWN时不拦截,MOVE时拦截,那么会引发注释8的代码,target view收到一个cancel事件,且mMotionTarget被置空,后续事件在注释7出的代理进行处理,即在自己的onTouchEvent中进行处理。

时间: 2024-12-14 18:47:23

Android ViewGroup触摸事件拦截详解的相关文章

Android ViewGroup 触摸事件传递机制

引言 上一篇博客我们学习了Android View 触摸事件传递机制,不了解的同学可以查看Android View 触摸事件传递机制.今天继续学习Android触摸事件传递机制,这篇博客将和大家一起探讨ViewGroup的触摸事件传递机制. 示例 示例代码如下: public class MainActivity extends ActionBarActivity { private String TAG = "MainActivity"; private MyViewGroup pa

Android系统输入事件分发详解

什么是输入事件? 我们知道,运行android系统的设备本质上是一台计算机,使用者在和计算机进行交互的时候可以抽象成简单的对计算机的输入和输出(IO).那么对于运行在计算机上的操作系统来说,操作系统在与使用者进行交互的时候起始也是可以抽象成对外界的输入进行处理,然后在输出返还给使用者.本文只讨论的是android系统中的"输入事件"(因为本文讨论的都是输入事件,所以以下简称"输入事件"为"事件")相关的内容. 根据以上描述,我们就可以回答上面的问

iOS中的四中触摸事件的详解 - 平移- 捏合 - 滑动(TouchesBegan,touchesMoved,touchesEnded,touchesCancelled)

RootViewController #import "RootViewController.h" #import "TouchView.h" #import "PanView.h" #import "PinchView.h" @interface RootViewController () @end @implementation RootViewController - (void)viewDidLoad { [super

Android Touch事件分发详解

Android Touch事件分发详解 先说一些基本的知识,方便后面分析源码时能更好理解. - 所有Touch事件都被封装成MotionEvent对象,包括Touch的位置.历史记录.第几个手指等. 事件类型分为ACTION_DOWN,ACTION_UP,ACTION_MOVE,ACTION_POINTER_DOWN,ACTION_POINTER_UP,ACTION_CANCEL, 每个 一个完整的事件以ACTION_DOWN开始ACTION_UP结束,并且ACTION_CANCEL只能由代码引

一个demo让你彻底理解Android中触摸事件的分发

注:本文涉及的demo的地址:https://github.com/absfree/TouchDispatch 1. 触摸动作及事件序列 (1)触摸事件的动作 触摸动作一共有三种:ACTION_DOWN.ACTION_MOVE.ACTION_UP.当用户手指接触屏幕时,便产生一个动作为ACTION_DOWN的触摸事件,此时若用户的手指立即离开屏幕,会产生一个动作为ACTION_UP的触摸事件:若用户手指接触屏幕后继续滑动,当滑动距离超过了系统中预定义的距离常数,则产生一个动作为ACTION_MO

Android Design Support Library使用详解

Android Design Support Library使用详解 Google在2015的IO大会上,给我们带来了更加详细的Material Design设计规范,同时,也给我们带来了全新的Android Design Support Library,在这个support库里面,Google给我们提供了更加规范的MD设计风格的控件.最重要的是,Android Design Support Library的兼容性更广,直接可以向下兼容到Android 2.2.这不得不说是一个良心之作. 使用S

Android图解浅析事件拦截机制

当Android系统捕获到用户的各种输入事件后,如何准确的传递给真正的需要这个事件的控件?Android提供了一整套完善的事件传递.处理机制,来帮助开发者完成准确的事件分配与处理,这里我就不分析源码了,简单点,图形化分发过程,便于理解. 当我们点击一个按钮时,通常会产生两个或者三个事件---按下.滑动(可能无).抬起.Android为触摸事件封装了一个类----MotionEvent,其中假如我们重写一个view的onTouchEvent事件中的参数就是一个MotionEvent.由于Andro

Android笔记:触摸事件的分析与总结----TouchEvent处理机制

   其他相关博文:    Android笔记:触摸事件的分析与总结----MotionEvent对象    Android笔记:触摸事件的分析与总结----TouchEvent处理机制 Android中的事件类型分为按键事件和屏幕触摸事件.TouchEvent是屏幕触摸事件的基础事件,要深入了解屏幕触摸事件的处理机制,就必须掌握TouchEvent在整个触摸事件中的转移和处理过程.此处将对TouchEvent处理机制的学习做个小小的总结和备记. 当屏幕中包含一个ViewGroup,而这个Vie

Android高效率编码-第三方SDK详解系列(三)——JPush推送牵扯出来的江湖恩怨,XMPP实现推送,自定义客户端推送

Android高效率编码-第三方SDK详解系列(三)--JPush推送牵扯出来的江湖恩怨,XMPP实现推送,自定义客户端推送 很久没有更新第三方SDK这个系列了,所以更新一下这几天工作中使用到的推送,写这个系列真的很要命,你要去把他们的API文档大致的翻阅一遍,而且各种功能都实现一遍,解决各种bug各种坑,不得不说,极光推送真坑,大家使用还是要慎重,我们看一下极光推送的官网 https://www.jpush.cn/common/ 推送比较使用,很多软件有需要,所以在这个点拿出来多讲讲,我们本节