阅读 2215

Android进阶知识:事件分发与滑动冲突

1、前言

Android学习一段时间,需求做多了必然会遇到滑动冲突问题,比如在一个ScrollView中要嵌套一个地图View,这时候触摸移动地图或者放大缩小地图就会变得不太准确甚至没有反应,这就是遇到了滑动冲突,ScrollView中上下滑动与地图的触摸手势发生冲突。想要解决滑动冲突就不得不提到Android的事件分发机制,只有吃透了事件分发,才能对滑动冲突的解决得心应手。

2、事件分发机制相关方法

Android事件分发机制主要相关方法有以下三个:

  • 事件分发:public boolean dispatchTouchEvent(MotionEvent ev)
  • 事件拦截:public boolean onInterceptTouchEvent(MotionEvent ev)
  • 事件响应:public boolean onTouchEvent(MotionEvent ev)

以下是这三个方法在Activity、ViewGroup和View中的存在情况:

相关方法 Activity ViewGroup View
dispatchTouchEvent yes yes yes
onInterceptTouchEvent no yes no
onTouchEvent yes yes yes

这三个方法都返回一个布尔类型,根据返回的不同对事件进行不同的分发拦截和响应。一般有三种返回truefalsesuper引用父类对应方法。

dispatchTouchEvent 返回true:表示改事件在本层不再进行分发且已经在事件分发自身中被消费了。
dispatchTouchEvent 返回 false:表示事件在本层不再继续进行分发,并交由上层控件的onTouchEvent方法进行消费。

onInterceptTouchEvent 返回true:表示将事件进行拦截,并将拦截到的事件交由本层控件 的onTouchEvent 进行处理。
onInterceptTouchEvent 返回false:表示不对事件进行拦截,事件得以成功分发到子View。并由子ViewdispatchTouchEvent进行处理。

onTouchEvent 返回 true:表示onTouchEvent处理完事件后消费了此次事件。此时事件终结,将不会进行后续的传递。
onTouchEvent 返回 false:事件在onTouchEvent中处理后继续向上层View传递,且有上层ViewonTouchEvent进行处理。

除此之外还有一个方法也是经常用到的:

  • public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)

它的作用是子View用来通知父View不要拦截事件。下面先写一个简单的Demo来看一下事件分发和传递:

简单的日志的Demo:

这里的代码只是自定义了两个ViewGroup和一个View,在其对应事件分发传递方法中打印日志,来查看调用顺序情况,所有相关分发传递方法返回皆是super父类方法。
例如: MyViewGroupA.java:

public class MyViewGroupA extends RelativeLayout {
    public MyViewGroupA(Context context) {
        super(context);
    }
    public MyViewGroupA(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public MyViewGroupA(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"dispatchTouchEvent:ACTION_UP");
                break;
        }        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_UP");
                break;
        }        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_UP");
                break;
        }        return super.onTouchEvent(event);
    }
}

复制代码

其他的代码都是类似的,这里再贴一下Acitivity里的布局:

<?xml version="1.0" encoding="utf-8"?>
<com.example.sy.eventdemo.MyViewGroupA xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/viewGroupA"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimary"
    tools:context=".MainActivity">

    <com.example.sy.eventdemo.MyViewGroupB
        android:id="@+id/viewGroupB"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_centerInParent="true"
        android:background="@android:color/white">

        <com.example.sy.eventdemo.MyView
            android:id="@+id/myView"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:layout_centerInParent="true"
            android:background="@android:color/holo_orange_light" />
    </com.example.sy.eventdemo.MyViewGroupB>
</com.example.sy.eventdemo.MyViewGroupA>
复制代码

Demo中的Activity布局层级关系:

除去外层Activity和Window的层级,从MyViewGroup开始是自己定义的打印日志View。接下来运行Demo查看日志:

 D/MainActivity: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
 D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
 D/MyView: dispatchTouchEvent:ACTION_DOWN
 D/MyView: onTouchEvent:ACTION_DOWN
 D/MyViewGroupB: onTouchEvent:ACTION_DOWN
 D/MyViewGroupA: onTouchEvent:ACTION_DOWN
 D/MainActivity: onTouchEvent:ACTION_DOWN
 D/MainActivity: dispatchTouchEvent:ACTION_MOVE
 D/MainActivity: onTouchEvent:ACTION_MOVE
 D/MainActivity: dispatchTouchEvent:ACTION_UP
 D/MainActivity: onTouchEvent:ACTION_UP
复制代码

结合日志可以大概看出(先只看ACTION_DOWN事件):
事件的分发顺序:Activity-->MyViewGroupA-->MyViewGroupB-->MyView自顶向下分发
事件的响应顺序:MyView-->MyViewGroupB-->MyViewGroupA-->Activity自底向上响应消费

同时这里通过日志也发现一个问题:

  • 问题一为什么这里只有ACTION_DOWN事件有完整的从Activity到ViewGroup再到View的分发拦截和响应的运行日志,为什么ACTION_MOVEACTION_UP事件没有?

接着再测试一下之前提的requestDisallowInterceptTouchEvent方法的使用。现在布局文件中将MyView添加一个属性android:clickable="true"。此时在运行点击打印日志是这样的:

 /-------------------ACTION_DOWN事件------------------
 D/MainActivity: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
 D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
 D/MyView: dispatchTouchEvent:ACTION_DOWN
 D/MyView: onTouchEvent:ACTION_DOWN
 /-------------------ACTION_MOVE事件------------------
 D/MainActivity: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_MOVE
 D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_MOVE
 D/MyView: dispatchTouchEvent:ACTION_MOVE
 D/MyView: onTouchEvent:ACTION_MOVE
 /-------------------ACTION_UP事件------------------
 D/MainActivity: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_UP
 D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_UP
 D/MyView: dispatchTouchEvent:ACTION_UP
 D/MyView: onTouchEvent:ACTION_UP
复制代码

这下ACTION_MOVEACTION_UP事件也有日志了。接下来在MyViewGroupB的onInterceptTouchEvent的方法中修改代码如下:

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_DOWN");
                return false;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_MOVE");
                return true;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"onInterceptTouchEvent:ACTION_UP");
                return true;
        }
        return false;
    }
复制代码

也就是拦截下ACTION_MOVEACTION_UP事件不拦截下ACTION_DOWN事件,然后在运行查看日志:

 /------------------ACTION_DOWN事件------------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
 D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
 D/MyView: dispatchTouchEvent:ACTION_DOWN
 D/MyView: onTouchEvent:ACTION_DOWN
 /------------------ACTION_MOVE事件-----------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_MOVE
 D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_MOVE
 /------------------ACTION_UP事件-------------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_UP
 D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupB: onTouchEvent:ACTION_UP
 D/MainActivity: onTouchEvent:ACTION_UP
复制代码

根据日志可知ACTION_MOVEACTION_UP事件传递到MyViewGroupB就没有再向MyView传递了。接着在MyView的onTouchEvent方法中调用requestDisallowInterceptTouchEvent方法通知父容器不要拦截事件。

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(getClass().getSimpleName(),"onTouchEvent:ACTION_UP");
                break;
        }
        return super.onTouchEvent(event);
    }
复制代码

再次运行查看日志:

 /------------------ACTION_DOWN事件------------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupA: onInterceptTouchEvent:ACTION_DOWN
 D/MyViewGroupB: dispatchTouchEvent:ACTION_DOWN
 D/MyViewGroupB: onInterceptTouchEvent:ACTION_DOWN
 D/MyView: dispatchTouchEvent:ACTION_DOWN
 D/MyView: onTouchEvent:ACTION_DOWN
 /------------------ACTION_MOVE事件-----------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupA: dispatchTouchEvent:ACTION_MOVE
 D/MyViewGroupB: dispatchTouchEvent:ACTION_MOVE
 D/MyView: dispatchTouchEvent:ACTION_MOVE
 D/MyView: onTouchEvent:ACTION_MOVE
 /------------------ACTION_UP事件-------------------------------
 D/MainActivity: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupA: dispatchTouchEvent:ACTION_UP
 D/MyViewGroupB: dispatchTouchEvent:ACTION_UP
 D/MyView: dispatchTouchEvent:ACTION_UP
 D/MyView: onTouchEvent:ACTION_UP
复制代码

这时可以发现ACTION_MOVEACTION_UP事件又传递到了MyView中并且两个ViewGroup中都没有执行onInterceptTouchEvent方法。 明显是requestDisallowInterceptTouchEvent方法起了作用。但是又出现了两个新问题。

  • 问题二:为什么将设置clickable="true"之后ACTION_MOVEACTION_UP事件就会执行了?
  • 问题三:requestDisallowInterceptTouchEvent方法是怎样通知父View不拦截事件,为什么连onInterceptTouchEvent方法也不执行了?

想弄明白这些问题就只能到源码中寻找答案了。

3、事件分发机制源码

在正式看源码之前先讲一个概念:事件序列

我们常说的事件,一般是指从手指触摸到屏幕在到离开屏幕这么一个过程。在这个过程中其实会产生多个事件,一般是以ACTION_DOWN作为开始,中间存在多个ACTION_MOVE,最后以ACTION_UP结束。我们称一次ACTION_DOWN-->ACTION_MOVE-->ACTION_UP过程称为一个事件序列。

ViewGroup中有一个内部类TouchTarget,这个类将消费事件的View封装成一个节点,使得可以将一个事件序列的DOWNMOVEUP事件构成一个单链表保存。ViewGroup中也有个TouchTarget类型的成员mFirstTouchTarget用来指向这个单链表头。在每次DOWN事件开始时清空这个链表,成功消费事件后通过TouchTarget.obtain方法获得一个TouchTarget,将消费事件的View传入,然后插到单链表头。后续MOVEUP事件可以通过判断mFirstTouchTarget来知道之前是否有能够消费事件的View。

TouchTarget的源码:

private static final class TouchTarget {
        private static final int MAX_RECYCLED = 32;
        private static final Object sRecycleLock = new Object[0];
        private static TouchTarget sRecycleBin;
        private static int sRecycledCount;

        public static final int ALL_POINTER_IDS = -1; // all ones

        // The touched child view.
        //接受事件的View
        public View child;

        // The combined bit mask of pointer ids for all pointers captured by the target.
        public int pointerIdBits;

        // The next target in the target list.
        //下一个TouchTarget的地址
        public TouchTarget next;

        private TouchTarget() {
        }

        public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
            if (child == null) {
                throw new IllegalArgumentException("child must be non-null");
            }

            final TouchTarget target;
            synchronized (sRecycleLock) {
                if (sRecycleBin == null) {
                    target = new TouchTarget();
                } else {
                    target = sRecycleBin;
                    sRecycleBin = target.next;
                     sRecycledCount--;
                    target.next = null;
                }
            }
            target.child = child;
            target.pointerIdBits = pointerIdBits;
            return target;
        }

        public void recycle() {
            if (child == null) {
                throw new IllegalStateException("already recycled once");
            }

            synchronized (sRecycleLock) {
                if (sRecycledCount < MAX_RECYCLED) {
                    next = sRecycleBin;
                    sRecycleBin = this;
                    sRecycledCount += 1;
                } else {
                    next = null;
                }
                child = null;
            }
        }
    }
复制代码
Activity中的dispatchTouchEvent方法:

接下来正式按照分发流程来阅读源码,从Activity的dispatchTouchEvent方法开始看起,事件产生时会先调用这个方法:

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
复制代码

方法中先判断事件类型是ACTION_DOWN事件会执行onUserInteraction方法,onUserInteraction方法在Activity中是一个空实现,在当前Activity下按下Home或者Back键时会调用此方法,这里不是重点,这里重点是关注下ACTION_DOWN事件,ACTION_DOWN类型事件的判断,在事件传递的逻辑中非常重要,因为每次点击事件都是以ACTION_DOWN事件开头,所以ACTION_DOWN事件又作为一次新的点击事件的标记。

紧接着看,在第二个if判断中根据getWindow().superDispatchTouchEvent(ev)的返回值决定了整个方法的返回。

如果getWindow().superDispatchTouchEvent(ev)方法返回为truedispatchTouchEvent方法返回true,否则则根据Activity中的onTouchEvent方法的返回值返回。

Activity中的onTouchEvent方法:

先来看Activity中的onTouchEvent方法:

  public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        return false;
    }
复制代码

onTouchEvent方法中根据window的shouldCloseOnTouch方法决定返回的结果和是否finish当前Activity。进入抽象类Window查看shouldCloseOnTouch方法:

 /** @hide */
    public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
        if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
                && isOutOfBounds(context, event) && peekDecorView() != null) {
            return true;
        }
        return false;
    }
复制代码

这是个hide方法,判断当前事件Event是否是ACTION_DOWN类型,当前事件点击坐标是否在范围外等标志位,如果为true就会返回到onTouchEvent方法关闭当前Activity。

看完再回到dispatchTouchEvent方法中,只剩下getWindow().superDispatchTouchEvent(ev)方法,来看他啥时候返回true啥时候返回false。这里的getWindow获取到Activity中的Window对象,调用WidnowsuperDispatchTouchEvent(ev)方法,这个方法不在抽象类Window当中,这里要去查看他的实现类PhoneWindow

  @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
复制代码

superDispatchTouchEvent方法中又调用了mDecor.superDispatchTouchEvent方法,这里的mDecor就是外层的DecorViewsuperDispatchTouchEvent方法:

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
复制代码

方法中又调用了父类的dispatchTouchEvent方法,DecorView继承自FrameLayout,而FrameLayout没有重写dispatchTouchEvent方法所以也就是调用了其父类ViewGroup的dispatchTouchEvent方法。

ViewGroup的dispatchTouchEvent方法:

通过以上这一系列的调用,事件终于从Activity到PhoneWindow再到DecorView最终走到了ViewGroup的dispatchTouchEvent方法中,接下来进入ViewGroup查看它的dispatchTouchEvent方法。

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
		......
	
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
//-----------------代码块-1----------------------------------------------------------------
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
//------------------代码块-1--完------------------------------------------------------------
//------------------代码块-2----------------------------------------------------------------
            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
//------------------代码块-2--完----------------------------------------------------------
            // If intercepted, start normal event dispatch. Also if there is already
            // a view that is handling the gesture, do normal event dispatch.
            if (intercepted || mFirstTouchTarget != null) {
                ev.setTargetAccessibilityFocus(false);
            }

            // Check for cancelation.
			//检查事件是否被取消
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
//------------------代码块-3--------------------------------------------------------------
            if (!canceled && !intercepted) {
             ......
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {													
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
						if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }
//------------------代码块-3--完----------------------------------------------------------
//------------------代码块-4--------------------------------------------------------------
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
				//mFirstTouchTarget为空说明没有子View响应消费该事件
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
//------------------代码块-4--完----------------------------------------------------------
          ......
		  
        return handled;
    }
复制代码

ViewGroup的dispatchTouchEvent方法比较长,虽然已经省略了一部分代码但代码还是非常多,并且代码中存在很多if-else判断,容易看着看着就迷失在ifelse之间。所以这里把他分成了四块代码来看。不过在看这四块代码之前先看dispatchTouchEvent方法中第一个if判断:

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)){
            ......
        }
复制代码

这里初始化的handled就是dispatchTouchEvent方法最后的返回值,onFilterTouchEventForSecurity这个方法过滤了认为不安全的事件,方法里主要是判断了view和window是否被遮挡,dispatchTouchEvent方法中所有的分发逻辑都要在onFilterTouchEventForSecurity返回为true的前提之下,否则直接返回handled即为false
接下来看第一段代码:

 final int action = ev.getAction();
 final int actionMasked = action & MotionEvent.ACTION_MASK;
 // Handle an initial down.
 if (actionMasked == MotionEvent.ACTION_DOWN) {
 // Throw away all previous state when starting a new touch gesture.
 // The framework may have dropped the up or cancel event for the previous gesture
 // due to an app switch, ANR, or some other state change.
 cancelAndClearTouchTargets(ev);
 resetTouchState();
 }
复制代码

第一段比较少比较简单,开始首先判断事件类型ACTION_DOWN事件被认为是一个新的事件序列开始,所以重置touch状态,将mFirstTouchTarget链表置空。这里可以进resetTouchState方法看下,方法中除了重置了一些状态还调用了clearTouchTargets方法清空链表。

    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }
    
    /**
     * Clears all touch targets.
     */
    private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }    
复制代码

接着看到代码块2:

            // Check for interception.
			//检查是否拦截事件
            final boolean intercepted;
			//是ACTION_DOWN事件或者mFirstTouchTarget不为空进入if
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
				//继续判断是否在调用了requestDisallowInterceptTouchEvent(true)设置了禁止拦截标记
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
				//设置禁止拦截设标记disallowIntercept为true,!disallowIntercept即为false
                if (!disallowIntercept) {
					//根据ViewGroup的nInterceptTouchEvent(ev)方法返回是否拦截
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
				//设置了禁止拦截标记,则不拦截
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
				//不是ACTION_DOWN事件或者mFirstTouchTarget=null,就拦截
                intercepted = true;
            }
复制代码

这段代码中主要是判断是否对事件进行拦截,intercepted是拦截标记,true代表拦截,false表示不拦截。这里首先判断是事件类型是DOWN或者mFirstTouchTarget不等于空(不等于空说明有子View消费了之前的DOWN事件),满足这个条件,就进入if进一步判断,否则直接设置interceptedfalse不拦截。在if中判断FLAG_DISALLOW_INTERCEPT这个标记位,这个标记位就是在requestDisallowInterceptTouchEvent()方法中设置的。这里跳到requestDisallowInterceptTouchEvent(true)方法来看一下:

@Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }
复制代码

看到requestDisallowInterceptTouchEvent方法里根据disallowIntercept进行不同位运算,mGroupFlags默认为0,FLAG_DISALLOW_INTERCEPT0x80000,如果传入设置为true,则进行或运算,mGroupFlags结果为0x80000,再回到代码块2里和FLAG_DISALLOW_INTERCEPT做与运算结果仍为0x80000,此时不等于0。反之传入false,最终位运算结果为0。也就是说调用requestDisallowInterceptTouchEvent方法传入true导致disallowInterceptrue,进而导致if条件不满足,使得interceptedfalse此时对事件进行拦截。反之,则进入if代码块调用onInterceptTouchEvent(ev)方法,根据返回值来决定是否拦截。

           if (!canceled && !intercepted) {
            // If the event is targeting accessiiblity focus we give it to the
            // view that has accessibility focus and if it does not handle it
            // we clear the flag and dispatch the event to all children as usual.
            // We are looking up the accessibility focused host to avoid keeping
            // state since these events are very rare.
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;
            //再判断事件类型是DOWN事件继续执行if代码块,这里的三个标记分别对应单点触摸DOWN多点触摸DOWN和鼠标移动事件
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;

                // Clean up earlier touch targets for this pointer id in case they
                // have become out of sync.
                removePointersFromTouchTargets(idBitsToAssign);
                //这里拿到子VIew个数
                final int childrenCount = mChildrenCount;
                //循环子View找到可以响应事件的子View将事件分发
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // Find a child that can receive the event.
                    // Scan children from front to back.
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);

                        // If there is a view that has accessibility focus we want it
                        // to get the event first and if not handled we will perform a
                        // normal dispatch. We may do a double iteration but this is
                        // safer given the timeframe.
                        if (childWithAccessibilityFocus != null) {
                            if (childWithAccessibilityFocus != child) {
                                continue;
                            }
                            childWithAccessibilityFocus = null;
                            i = childrenCount - 1;
                        }
                        //这个子View无法接受这个事件或者事件点击不在这个子View内就跳过这次循环
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                        //到这里说明这个子View可以处理该事件,就到TochTarget链表里去找对应的TochTarget,没找到返回null
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // Child is already receiving touch within its bounds.
                            // Give it the new pointer in addition to the ones it is handling.
                            //不为空说明view已经处理过这个事件,说明是多点触摸,就再加一个指针
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }

                        resetCancelNextUpFlag(child);
                        //调用dispatchTransformedTouchEvent方法将事件分发给子View
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // Child wants to receive touch within its bounds.
                            mLastTouchDownTime = ev.getDownTime();
                            if (preorderedList != null) {
                                // childIndex points into presorted list, find original index
                                for (int j = 0; j < childrenCount; j++) {
                                    if (children[childIndex] == mChildren[j]) {
                                        mLastTouchDownIndex = j;
                                        break;
                                    }
                                }
                            } else {
                                mLastTouchDownIndex = childIndex;
                            }
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            //dispatchTransformedTouchEvent返回true说明子View响应消费了这个事件
                            //于是调用addTouchTarget方法获得包含这个View的TouchTarget节点并将其添加到链表头
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            //将已经分发的标记设置为true
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }

                        // The accessibility focus didn't handle the event, so clear
                        // the flag and do a normal dispatch to all children.
                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }
                //如果newTouchTarget为null且mFirstTouchTarget不为null,说明没找到子View来响应消费该事件,但是TouchTarget链表不为空
                //则将newTouchTarget赋为TouchTarget链表中mFirstTouchTarget.next
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }
复制代码

接着看代码块3,在这段很长的代码里,首先一个if中判断了该事件是否满足没有被拦截和被取消,之后第二个if判断了事件类型是否为DOWN,满足了没有被拦截和取消的DOWN事件,接下来ViewGroup才会循环其子View找到点击事件在其内部并且能够接受该事件的子View,再通过调用dispatchTransformedTouchEvent方法将事件分发给该子View处理,返回true说明子View成功消费事件,于是调用addTouchTarget方法,方法中通过TouchTarget.obtain方法获得一个包含这View的TouchTarget节点并将其添加到链表头,并将已经分发的标记设置为true
接下来看代码块4:

            // Dispatch to touch targets.
			//走到这里说明在循环遍历所有子View后没有找到接受该事件或者事件不是DOWN事件或者该事件已被拦截或取消	
            if (mFirstTouchTarget == null) {
				//mFirstTouchTarget为空说明没有子View响应消费该事件
				//所有调用dispatchTransformedTouchEvent方法分发事件
				//注意这里第三个参数传的是null,方法里会调用super.dispatchTouchEvent(event)即View.dispatchTouchEvent(event)方法
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
				 // mFirstTouchTarget不为空说明有子View能响应消费该事件,消费过之前的DOWN事件,就将这个事件还分发给这个View
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
							//这里传入的是target.child就是之前响应消费的View,把该事件还交给它处理
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
复制代码

之前在代码块3中处理分发了没被拦截和取消的DOWN事件,那么其他MOVEUP等类型事件怎么处理呢?还有如果遍历完子View却没有能接受这个事件的View又怎么处理呢?代码块4中就处理分发了这些事件。首先判断mFirstTouchTarget是否为空,为空说明没有子View消费该事件,于是就调用dispatchTransformedTouchEvent方法分发事件,这里注意dispatchTransformedTouchEvent方法第三个参数View传的null,方法里会对于这种没有子View能处理消费事件的情况,就调用该ViewGroup的super.dispatchTouchEvent方法,即View的dispatchTouchEvent,把ViewGroup当成View来处理,把事件交给ViewGroup处理。具体看dispatchTransformedTouchEvent方法中的这段代码:

            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
复制代码

dispatchTransformedTouchEvent方法中child即传入的View为空则调用super.dispatchTouchEvent方法分发事件,就是View类的分发方法,不为空则调用子View方法,即child.dispatchTouchEvent分发事件,所以归根结底都是调用了View类的dispatchTouchEvent方法处理。

至此,ViewGroup中的分发过流程结束,再来总结一下这个过程:首先过滤掉不安全的事件,接着如果事件类型是DOWN事件认为是一个新的事件序列开始,就清空TouchTarget链表重置相关标志位(代码块一),然后判断是否拦截该事件,这里有两步判断:一是如果是DOWN事件或者不是DOWN事件但是mFirstTouchTarget不等于null(这里mFirstTouchTarget如果等于null说明之前没有View消费DOWN事件,在代码块三末尾,可以看到如果有子View消费了DOWN事件,会调用addTouchTarget方法,获得一个保存了该子View的TouchTarget,并将其添加到mFirstTouchTarget链表头),则进入第二步禁止拦截标记的判断,否则直接设置为需要拦截,进入第二步判断设置过禁止拦截标记为true的就不拦截,否则调用ViewGroup的onInterceptTouchEvent方法根据返回接过来决定是否拦截(代码块二)。接下来如果事件没被拦截也没被取消而且还是DOWN事件,就循环遍历ViewGroup中的子View找到事件在其范围内并且能接受事件的子View,通过dispatchTransformedTouchEvent方法将事件分发给该子View,然后通过addTouchTarget方法将包含该子View的TouchTarget插到链表头(代码块三)。最后如果没有找到能够接受该事件的子View又或者是MOVEUP类型事件等再判断mFirstTouchTarget是否为空,为空说明之前没有View能接受消费该事件,则调用dispatchTransformedTouchEvent方法将事件交给自身处理,不为空则同样调用dispatchTransformedTouchEvent方法,但是是将事件分发给该子View处理。

ViewGroup的onInterceptTouchEvent方法:
public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }
复制代码

在ViewGroup的dispatchTouchEvent中没设置过禁止拦截的事件默认都会通过onInterceptTouchEvent方法来决定是否拦截,onInterceptTouchEvent方法里可以看到默认是返回false,只有在事件源类型是鼠标并且是DOWN事件是鼠标点击按钮和是滚动条的手势时才返回true。所以默认一般ViewGroup的onInterceptTouchEvent方法返回都为false,也就是说默认不拦截事件。

ViewGroup的onTouchEvent方法:

ViewGroup中没有覆盖onTouchEvent方法,所以调用ViewGroup的onTouchEvent方法实际上调用的还是它的父类View的onTouchEvent方法。

View的dispatchTouchEvent方法:

在ViewGroup中将事件无论是分发给子View的时候还是自己处理的,最终都会执行默认的View类的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent event) {
        ......
        boolean result = false;
        ......
        if (onFilterTouchEventForSecurity(event)) {
        
          ......
          
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
         ......
        return result;
    }
复制代码

这里同样省略一些代码只看关键的,首先同样和ViewGroup一样,做了事件安全性的过滤,接着先判断了mOnTouchListener是否为空,不为空并且该View是ENABLED可用的,就会调用mOnTouchListeneronTouch方法,如果onTouch方法返回true说明事件已经被消费了,就将result标记修改为true,这样他就不会走接下来的if了。如果没有设置mOnTouchListener或者onTouch方法返回false,则会继续调用onTouchEvent方法。这里可以发现mOnTouchListeneronTouch方法的优先级是在onTouchEvent之前的,如果在代码中设置了mOnTouchListener监听,并且onTouch返回true,则这个事件就被在onTouch里消费了,不会在调用onTouchEvent方法。

//这个mOnTouchListener就是经常在代码里设置的View.OnTouchListener
mMyView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                //这里返回true事件就消费了,不会再调用onTouchEvent方法
                return true;
            }
        });
复制代码
View的onTouchEvent方法:
 public boolean onTouchEvent(MotionEvent event) {
 /---------------代码块-1-------------------------------------------------------------------
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
/---------------代码块-1------完-------------------------------------------------------------	
/---------------代码块-2-------------------------------------------------------------------
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
/---------------代码块-2------完-------------------------------------------------------------	
/---------------代码块-3-------------------------------------------------------------------	
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            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)) {
									//调用了OnClickListener
                                    performClick();
                                }
                            }
                        }

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

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }

            return true;
        }
/---------------代码块-3------完-------------------------------------------------------------	
        return false;
    }
复制代码

onTouchEvent方法里的代码也不少,不过大部分都是响应事件的一些逻辑,与事件分发流程关系不大。还是分成三块,先看第一个代码块:

    final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        //这里CLICKABLE、CONTEXT_CLICKABLE和CONTEXT_CLICKABLE有一个满足,clickable就为true
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        //这里先判断当前View是否可用,如果是不可用进入if代码块
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
        //如果是UP事件并且View处于PRESSED状态,则调用setPressed设置为false
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            //这里如果View是不可用状态,就直接返回clickable状态,不做任何处理
            return clickable;
        }
复制代码

代码块1中首先获得View是否可点击clickable,然后判断View如果是不可用状态就直接返回clickable,但是没做任何响应。View默认的clickablefalseEnabledture,不同的View的clickable默认值也不同,Button默认clickabletrueTextView默认为false

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
复制代码

代码块2中会对一个mTouchDelegate触摸代理进行判断,不为空会调用代理的onTouchEvent响应事件并且返回true

 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            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)) {
									//调用了OnClickListener
                                    performClick();
                                }
                            }
                        }

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

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }

            return true;
        }
复制代码

代码块3中首先判断了 clickable || (viewFlags & TOOLTIP) == TOOLTIP 满足了这个条件就返回true消费事件。接下来的switch中主要对事件四种状态分别做了处理。这里稍微看下在UP事件中会调用一个performClick方法,方法中调用了OnClickListeneronClick方法。

public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        notifyEnterOrExitForAutoFillIfNeeded(true);
        return result;
    }
复制代码

最后看到onTouchEvent的最后一行默认返回的还是false,就是说只有满足上述的条件之一才会返回ture
至此事件分发的相关源码就梳理完了,我画了几张流程图,能更清晰的理解源码逻辑。

ViewGroup的dispatchTouchEvent逻辑:

ViewGroup的dispatchTouchEvent逻辑
View的dispathTouchEvent逻辑:
View的dispathTouchEvent逻辑
事件分发整体逻辑

4、事件分发机制相关问题

阅读了源码之后,先来解决之前提到的三个问题。

Q1:为什么日志Demo中只有ACTION_DOWN事件有完整的从Activity到ViewGroup再到View的分发拦截和响应的运行日志,为什么ACTION_MOVEACTION_UP事件没有?

A1:日志Demo代码所有事件传递方法都是默认调用super父类对应方法,所以根据源码逻辑可知当事件序列中的第一个DOWN事件来临时,会按照Activity-->MyViewGroupA-->MyViewGroupB-->MyView的顺序分发,ViewGroup中onInterceptTouchEvent方法默认返回false不会拦截事件,最终会找到合适的子View(这里即MyView)dispatchTransformedTouchEvent方法,将事件交给子View的dispatchTouchEvent处理,在dispatchTouchEvent方法中默认会调用View的onTouchEvent方法处理事件,这里因为MyView是继承View的,所以默认clickablefalse,而onTouchEvent方法中当clickablefalse时默认返回的也是false。最终导致ViewGroup中dispatchTransformedTouchEvent方法返回为false。进而导致mFirstTouchTarget为空,所以后续MOVEUP事件到来时,因为mFirstTouchTarget为空,事件拦截标记直接设置为true事件被拦截,就不会继续向下分发,最终事件无人消费就返回到Activity的onTouchEvent方法。所以就会出现这样的日志输出。

 if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action);  
                } else {
                    intercepted = false;
                }
            } else {
                //mFirstTouchTarget为空intercepted为true且不会调用onInterceptTouchEvent方法
                intercepted = true;
            }
复制代码
Q2:为什么将设置clickable="true"之后ACTION_MOVEACTION_UP事件就会执行了?

A2:如A1中所说,clickable设置为true,View的onTouchEvent方法的返回就会为true,消费了DOWN事件,就会创建一个TouchTarget插到单链表头,mFirstTouchTarget就不会是空了,MOVEUP事件到来时,就会由之前消费了DOWN事件的View来处理消费MOVEUP事件。

Q3:requestDisallowInterceptTouchEvent方法是怎样通知父View不拦截事件,为什么连onInterceptTouchEvent方法也不执行了?

A3:源码阅读是有看到,requestDisallowInterceptTouchEvent方法时通过位运算设置标志位,在调用传入参数为true后,事件在分发时disallowIntercept会为true!disallowIntercept即为false,导致事件拦截标记interceptedfalse,不会进行事件拦截。

Q4:View.OnClickListeneronClick方法与View.OnTouchListeneronTouch执行顺序?

A4::View.OnClickListeneronClick方法是在View的onTouchEventperformClick方法中调用的。 而View.OnTouchListeneronTouch方法在View的dispatchTouchEvent方法中看到是比onTouchEvent方法优先级高的,并且只要OnTouchListener.Touch返回为true,就只会调用OnTouchListener.onTouch方法不会再调用onTouchEvent方法。所以View.OnClickListeneronClick方法顺序是在View.OnTouchListeneronTouch之后的。

5、滑动冲突

关于滑动冲突,在《Android开发艺术探索》中有详细说明,我这里把书上的方法结论与具体实例结合起来做一个总结。

1.滑动冲突的场景

常见的场景有三种:

  • 外部滑动与内部滑动方向不一致
  • 外部滑动与内部滑动方向一致
  • 前两种情况的嵌套
2.滑动冲突的处理规则

不同的场景有不同的处理规则,例如上面的场景一,规则一般就是当左右滑动时,外部View拦截事件,当上下滑动时要让内部View拦截事件,这时候处理滑动冲突就可以根据滑动是水平滑动还是垂直滑动来判断谁来拦截事件。场景而这种同个方向上的滑动冲突一般要根据业务逻辑来处理规则,什么时候要外部View拦截,什么时候要内部View拦截。场景三就更加复杂了,但是同样是根据具体业务逻辑,来判断具体的滑动规则。

3.滑动冲突的解决方法
  • 外部拦截法
  • 内部拦截法

外部拦截法是从父View着手,所有事件都要经过父View的分发和拦截,什么时候父View需要事件,就将其拦截,不需要就不拦截,通过重写父View的onInterceptTouchEvent方法来实现拦截规则。

    private int mLastXIntercept;
    private int mLastYIntercept;
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int)event.getX();
        int y = (int)event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                if (满足父容器的拦截要求) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
            default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }
复制代码

按照以上伪代码,根据不同的拦截要求进行修改就可以解决滑动冲突。

内部拦截法的思想是父View不拦截事件,由子View来决定事件拦截,如果子View需要此事件就直接消耗掉,如果不需要就交给父View处理。这种方法需要配合requestDisallowInterceptTouchEvent方法来实现。

private int  mLastX;
private int  mLastY;
@Override
 public boolean dispatchTouchEvent(MotionEvent event) {
     int x = (int) event.getX();
     int y = (int) event.getY();

     switch (event.getAction()) {
     case MotionEvent.ACTION_DOWN: {
         parent.requestDisallowInterceptTouchEvent(true);
         break;
     }
     case MotionEvent.ACTION_MOVE: {
         int deltaX = x - mLastX;
         int deltaY = y - mLastY;
         if (父容器需要此类点击事件) {
             parent.requestDisallowInterceptTouchEvent(false);
         }
         break;
     }
     case MotionEvent.ACTION_UP: {
         break;
     }
     default:
         break;
     }
     mLastX = x;
     mLastY = y;
     return super.dispatchTouchEvent(event);
 }  
 
 //父View的onInterceptTouchEvent方法
  @Override
 public boolean onInterceptTouchEvent(MotionEvent event) {
     int action = event.getAction();
     if (action == MotionEvent.ACTION_DOWN) {
         return false;
     } else {
         return true;
     }
 }  
复制代码

这里父View不拦截ACTION_DOWN方法的原因,根据之前的源码阅读可知如果ACTION_DOWN事件被拦截,之后的所有事件就都不会再传递下去了。

4.滑动冲突实例

实例一:ScrollView与ListView嵌套
这个实例是同向滑动冲突,先看布局文件:

<?xml version="1.0" encoding="utf-8"?>
<cScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/scrollView1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ScrollDemo1Activity">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <com.example.sy.eventdemo.MyView
            android:layout_width="match_parent"
            android:layout_height="350dp"
            android:background="#27A3F3"
            android:clickable="true" />

        <ListView
            android:id="@+id/lv"
            android:layout_width="match_parent"
            android:background="#E5F327"
            android:layout_height="300dp"></ListView>

        <com.example.sy.eventdemo.MyView
            android:layout_width="match_parent"
            android:layout_height="500dp"
            android:background="#0AEC2E"
            android:clickable="true" />
    </LinearLayout>
</cScrollView>
复制代码

这里MyView就是之前打印日志的View没有做任何其他处理,用于占位使ScrollView超出一屏可以滑动。
运行效果:

可以看到ScrollView与ListView发生滑动冲突,ListView的滑动事件没有触发。接着来解决这个问题,用内部拦截法。

首先自定义ScrollView,重写他的onInterceptTouchEvent方法,拦击除了DOWN事件以外的事件。

public class MyScrollView extends ScrollView {

    public MyScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onTouchEvent(ev);
            return false;
        }
        return true;
    }

}
复制代码

这里没有拦截DOWN事件,所以DOWN事件无法进入ScrollView的onTouchEvent事件,又因为ScrollView的滚动需要在onTouchEvent方法中做一些准备,所以这里手动调用一次。接着再自定义一个ListView,来决定事件拦截,重写dispatchTouchEvent方法。

package com.example.sy.eventdemo;

import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ListView;

/**
 * Create by SY on 2019/4/22
 */
public class MyListView extends ListView {
    public MyListView(Context context) {
        super(context);
    }

    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    private float lastY;

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            getParent().getParent().requestDisallowInterceptTouchEvent(true);
        } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
            if (lastY > ev.getY()) {
                // 这里判断是向上滑动,而且不能再向上滑了,说明到头了,就让父View处理
                if (!canScrollList(1)) {
                    getParent().getParent().requestDisallowInterceptTouchEvent(false);
                    return false;
                }
            } else if (ev.getY() > lastY) {
                // 这里判断是向下滑动,而且不能再向下滑了,说明到头了,同样让父View处理
                if (!canScrollList(-1)) {
                    getParent().getParent().requestDisallowInterceptTouchEvent(false);
                    return false;
                }
            }
        }
        lastY = ev.getY();
        return super.dispatchTouchEvent(ev);
    }
}

复制代码

判断是向上滑动还是向下滑动,是否滑动到头了,如果滑到头了就让父View拦截事件由父View处理,否则就由自己处理。将布局文件中的空间更换。

<?xml version="1.0" encoding="utf-8"?>
<com.example.sy.eventdemo.MyScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/scrollView1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ScrollDemo1Activity">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <com.example.sy.eventdemo.MyView
            android:layout_width="match_parent"
            android:layout_height="350dp"
            android:background="#27A3F3"
            android:clickable="true" />

        <com.example.sy.eventdemo.MyListView
            android:id="@+id/lv"
            android:layout_width="match_parent"
            android:background="#E5F327"
            android:layout_height="300dp"></com.example.sy.eventdemo.MyListView>

        <com.example.sy.eventdemo.MyView
            android:layout_width="match_parent"
            android:layout_height="500dp"
            android:background="#0AEC2E"
            android:clickable="true" />
    </LinearLayout>
</com.example.sy.eventdemo.MyScrollView>
复制代码

运行结果:

实例二:ViewPager与ListView嵌套
这个例子是水平和垂直滑动冲突。使用V4包中的ViewPager与ListView嵌套并不会发生冲突,是因为在ViewPager中已经实现了关于滑动冲突的处理代码,所以这里自定义一个简单的ViewPager来测试冲突。布局文件里就一个ViewPager:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ScrollDemo2Activity">

    <com.example.sy.eventdemo.MyViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></com.example.sy.eventdemo.MyViewPager>
</LinearLayout>
复制代码

ViewPager的每个页面的布局也很简单就是一个ListView:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ScrollDemo2Activity">

    <com.example.sy.eventdemo.MyViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></com.example.sy.eventdemo.MyViewPager>

</LinearLayout>
复制代码

开始没有处理滑动冲突的运行效果是这样的:

看到现在只能上下滑动响应ListView的滑动事件,接着我们外部拦截发解决滑动冲突,核心代码如下:

    case MotionEvent.ACTION_MOVE:
                int gapX = x - lastX;
                int gapY = y - lastY;
                //当水平滑动距离大于垂直滑动距离,让父view拦截事件
                if (Math.abs(gapX) > Math.abs(gapY)) {
                    intercept = true;
                } else {
                    //否则不拦截事件
                    intercept = false;
                }
                break;
复制代码

onInterceptTouchEvent中当水平滑动距离大于垂直滑动距离,让父view拦截事件,反之父View不拦截事件,让子View处理。
运行结果:

这下冲突就解决了。这两个例子分别对应了上面的场景一和场景二,关于场景三的解决方法其实也是一样,都是根据具体需求判断事件需要由谁来响应消费,然后重写对应方法将事件拦截或者取消拦截即可,这里就不再具体举例了。

6、总结

  • Android事件分发顺序:Activity-->ViewGroup-->View
  • Android事件响应顺序:View-->ViewGroup-->Activity
  • 滑动冲突解决,关键在于找到拦截规则,根据操作习惯或者业务逻辑确定拦截规则,根据规则重新对应拦截方法即可。
关注下面的标签,发现更多相似文章
评论