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

简介: 1、前言 Android学习一段时间,需求做多了必然会遇到滑动冲突问题,比如在一个ScrollView中要嵌套一个地图View,这时候触摸移动地图或者放大缩小地图就会变得不太准确甚至没有反应,这就是遇到了滑动冲突,ScrollView中上下滑动与地图的触摸手势发生冲突。

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布局层级关系:


webp

除去外层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;
                }
            }
        }
webp


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
3月前
|
JavaScript Android开发
使用贝叶斯曲线滑动安卓屏幕(autojsPro7)
使用贝叶斯曲线滑动安卓屏幕(autojsPro7)
60 0
|
4月前
|
XML Java Android开发
Android Studio App开发之捕获屏幕的变更事件实战(包括竖屏与横屏切换,回到桌面与切换到任务列表)
Android Studio App开发之捕获屏幕的变更事件实战(包括竖屏与横屏切换,回到桌面与切换到任务列表)
38 0
|
4月前
|
小程序 JavaScript 前端开发
微信小程序(十七)小程序监听返回键跳转事件(安卓返回也适用)
onUnload:function(){ wx.redirectTo({ url: '../index/index' }) wx.navigateTo({ url: '../index/index' }) wx.switchTab({ url: '../../member/member' }) }
297 0
|
3月前
|
Android开发 Kotlin 索引
Android Compose——ScrollableTabRow和LazyColumn同步滑动
Android Compose——ScrollableTabRow和LazyColumn同步滑动
|
8月前
|
移动开发 Android开发
h5滑动底部兼容安卓
h5滑动底部兼容安卓
58 0
|
4月前
|
XML Java Android开发
Android App事件交互Event之模仿京东App实现下拉刷新功能(附源码 可直接使用)
Android App事件交互Event之模仿京东App实现下拉刷新功能(附源码 可直接使用)
32 0
Android App事件交互Event之模仿京东App实现下拉刷新功能(附源码 可直接使用)
|
4月前
|
XML Java Android开发
Android App手势冲突处理中上下左右滑动的处理以及侧滑边缘菜单的讲解及实战(附源码 可直接使用)
Android App手势冲突处理中上下左右滑动的处理以及侧滑边缘菜单的讲解及实战(附源码 可直接使用)
66 0
|
4月前
|
XML Java Android开发
Android App事件交互中辨别缩放与旋转手指的讲解与实战(附源码 可直接使用)
Android App事件交互中辨别缩放与旋转手指的讲解与实战(附源码 可直接使用)
36 0
|
4月前
|
XML Java Android开发
Android App事件交互中区分点击和长按动作以及识别手势滑动方向的讲解及实战(附源码 可直接使用)
Android App事件交互中区分点击和长按动作以及识别手势滑动方向的讲解及实战(附源码 可直接使用)
60 0
|
4月前
|
XML Java Android开发
Android App开发触摸事件中手势事件Event的分发流程讲解与实战(附源码 简单易懂)
Android App开发触摸事件中手势事件Event的分发流程讲解与实战(附源码 简单易懂)
43 0