事件分发(二)(Android Java层事件分发流程)

315 阅读8分钟

我们用手指点击手机屏幕之后,经过底层传递之后传给了Activity,首先由Activity的dispatchTouchEvent方法接收,整体的流程如下:

-->Activity.dispatchTouchEvent(MotionEvent ev)

--->PhoneWindow.superDispatchTouchEvent(MotionEvent ev)

---->DecorView.superDispatchTouchEvent(MotionEvent ev)

---->ViewGroup.dispatchTouchEvent(MotionEvent ev)

---->View.dispatchTouchEvent(MotionEvent ev)

----->View.onTouchEvent(MotinEvent ev)

来通过下面一张霸气的图来帮助我们理解

DecorView本质上就是一个FrameLayout,而FrameLayout并没有重写

dispatchTouchEvent,所以直接进入了ViewGroup的dispatchTouchEvent

Android的事件分发主要关注以下几个核心方法:

1.dispatchTouchEvent: 负责事件的分发,该方法在Activity、ViewGroup、View中都有

2.onInterceptTouchEvent:负责事件拦截,只有在ViewGroup中有此方法

3.onTouchEvent: 负责事件消费,在Activity、View中有此方法

Android触摸事件的类型:手指触摸到手机屏幕的一瞬间会触发 一个 ACTION_DOWN 事件,之后手指开始在屏幕上移动会触发 **多个ACTION_MOVE事件,**最后手指抬起的瞬间会触发 一个ACTION_UP事件

首先看一下ViewGroup的dispatchTouchEvent方法

ViewGroup.java

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ......
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {//触摸事件安全性检查,正常返回true
        final int action = ev.getAction();//获取事件ACTION
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        // Handle an initial down.
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            //每一次DOWN事件一定会先走到这里,以下方法之后会详细介绍
            //如果之前DOWN事件被处理,接下来MOVE事件来时便 不会走这里,也就是MOVE事件状态不会被重置
            cancelAndClearTouchTargets(ev);//清除TouchTarget
            resetTouchState();//重置触摸状态
        }
        // Check for interception.
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {//每次ACTION_DOWN一定询问是否拦截
             //mGroupFlags 开始未初始化,这里disallowIntercept返回false
            //如果之前DOWN事件被处理,接下来当第一个MOVE事件来时也会走这里
           //因为DOWN事件被处理后 mFirstTouchTarget!=null
     //子View可以通过调用getParent().requestDisallowInterceptTouchEvent()方法
     //来改变 disallowInterept 变量的值,从而决定父View是否拦截子View(DOWN事件除外)
     //如果是DOWN事件,由于前面调用了 resetTouchState(),所以 disallowIntercept一定为false
            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 {
            ...
    //既不是ACTION_DOWN事件,mFirstTouchTarget又为null(也就是下面讲到的cancelChild为true的情况)
    //此时将会直接拦截而不再调用onInterceptTouchEvent进行询问
            intercepted = true;
        }
        ......
        // Check for cancelation.
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;
        ...
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        //正常点击canceled=false,如果ViewGroup没有拦截会进入下面的流程
        if (!canceled && !intercepted) {
            ......
            //如果之前DOWN事件被处理,接下来当第一个MOVE事件来时将不会进入下面的逻辑
           //而是直接跳过进入之后的 mFirstTouchTarget 的判断
            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 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.
                 //buildTouchDispatchChildList()是对该ViewGroup中的子View进行排序
                 //排序方法是按子View的层叠顺序进行的(也就是根据所有子View的 Z坐标 的值排序)
//preorderedList数组中 Z坐标值从低到高 排列,但是后面获取的时候是从下标为size-1处获取的
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        //按层叠顺序获取子View(获取到的子View有可能又是另一个ViewGroup)
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);
                        ......
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                           ......
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }
                        resetCancelNextUpFlag(child);
                      //dispatchTransformedTouchEvent会继续向子View传递消息
                      //child是上边获取到的子View,false 表示cancle为false,不取消这次点击事件

                      if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                       //child处理了事件之后返回true,接着进入下面的流程
                      //如果child没有处理事件将会重新进入上边的for循环继续按层叠顺序获取子View
                            // Child wants to receive touch within its bounds.
                            mLastTouchDownTime = ev.getDownTime();
                            if (preorderedList != null) {
                                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方法
                            //最后会将处理事件的View保存在newTouchTarget中
                            //同样也将该target赋值给了mFirstTouchTarget
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;//表示事件已经被分发
                            break;//事件得到处理后,这里break掉按层叠顺序获取子View的循环
                        }
                        ...
                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }
                ...
            }
        }

        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
          //如果DOWN事件被拦截会直接进入这里
          //如果是上面提到的cancelChild为true的情况也会直接进入这里,
          //并且在之后的MOVE事件里 下面的canceled参数 为false
          //因为在resetCancelNextUpFlag()方法里cancel默认设置为false
          // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);//注意:child参数为null!!
        } else {
            //DOWN事件中如果事件被某View处理,mFirstTouchTarget会被赋值给该View走下面的步骤
            //如果之前DOWN事件被处理,接下来MOVE事件来时将会直接进入这里
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;//这里next为null
    //注意:如果之前DOWN事件被处理,接下来第一个MOVE事件来时 alreadyDispatchedToNewTouchTarget为false
    //因为 alreadyDispatchedToNewTouchTarget 赋值为true是在上边的 if 语句里面
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;//DOWN事件被处理
                } else {
              //如果之前DOWN事件被处理,接下来当第一个MOVE事件来时将会进入这里
             //此时下面的 cancleChild 仍将返回 false,接下来会调用 dispatchTransformedTouchEvent
             //但是如果我们调用了 requestDisallowInterceptTouchEvent(false),之后又在
            //onInterceptTouchEvent中返回true, 此时 intercepted将为true,cancleChild将为true
            //特别注意:上面刚刚提到的就是 cancelChild为true的情况!
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                        //特别注意下面的情况!
                        //cancelChild为true时 mFirstTouchTarget将会被赋值为null
                        //那么之后的一系列MOVE事件将不再询问直接拦截(intercept直接设置为true)
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
        ......
    }
    ......
    return handled;
}

进入 dispatchTransformedTouchEvent 方法

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    final int oldAction = event.getAction();
    //DOWN事件时cancle为false
    //如果之前DOWN事件被处理,接下来MOVE事件来时cancle仍为false, child 不变
    //如果是上面提到的cancelChild为true的情况将会直接进入下面的if语句
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        //cancelChild为true的情况直接设置ACTION
        //说明事件被上层拦截时将会触发 ACTION_CANCEL
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            //cancelChild为true的情况执行到这里,之后在child的 dispatchTouchEvent 中
            //检测到ACTION的值为ACTION_CANCEL,将会直接执行cancel的操作
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    ......
    // Perform any necessary transformations and dispatch.
    if (child == null) {//DOWN事件被拦截后直接进入View的dispatchTouchEvent
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
       ......
       //DOWN事件时最终调用了View的dispatchTouchEvent,进入了事件分发(一)中的流程
       //但是也可能获取到的是ViewGroup,此时又会进入ViewGroup的dispatchTouchEvent
        handled = child.dispatchTouchEvent(transformedEvent);
    }

    // Done.
    transformedEvent.recycle();
    return handled;//如果child处理了事件返回true
}

当事件被某一View处理时,进入 addTouchTarget 方法,传入的child 参数就是处理事件的View

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);//进入obtain方法
    target.next = mFirstTouchTarget;//next被赋值为null
    //注意:mFirstTouchTarget在这里被赋值,保存了处理事件的View
    mFirstTouchTarget = target;
    return target;
}

进入 TouchTarget的obtain方法

public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
   ......
    //注意:这里将处理事件的View保存在了Target中并最终返回给上边的newTouchTarget变量
    target.child = child;
    target.pointerIdBits = pointerIdBits;
    return target;
}

拦截处理

对于拦截的处理需要注意ViewGroup的以下代码

 if (actionMasked == MotionEvent.ACTION_DOWN) {
            //每一次DOWN事件一定会先走到这里,以下方法之后会详细介绍
            cancelAndClearTouchTargets(ev);//清楚TouchTarget
            resetTouchState();//重置触摸状态
        }
        // Check for interception.
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN //每次ACTION_DOWN一定询问是否拦截,也就是
                || mFirstTouchTarget != null) {    //DOWN事件一定会调用onInterceptTouchEvent
            //mGroupFlags 开始未初始化,这里disallowIntercept返回false
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);//Down事件一定会执行拦截方法
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            ...
            intercepted = true;
        }

如果是ACTION_DOWN以外事件的拦截处理,可以通过调用ViewGroup的以下方法

@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    ...
    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
    ...
}

如果传入的参数为 true ,将不会进入 onInterceptTouchEvent 方法, intercept 将为 false 也就是不拦截,如果传入参数为 false 将会进入 onInterceptTouchEvent  方法询问是否拦截。

滑动冲突处理

现在有一个这样的场景,一个ViewPager里面嵌套了一个ListView,这个时候就会引起滑动冲突,假设我们采用内部拦截法在ListView中重写dispatchTouchEvent进行如下处理:

public boolean dispatchTouchEvent(MotionEvent ev) {
    swich(ev.getAction){
        case MotinoEvent.ACTOIN_DOWN:
           getParent().requestDisallowInterceptTouchEvent(true);
           break;
         ......
    }
    return onTouchEvent(ev);
}

但是运行后发现ListView的DOWN事件并没有被拦截,原因就处在以下方法中

private void resetTouchState() {
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    //每次DOWN事件来时ViewGroup会重置不允许拦截的标志位,这样 disallowIncept就为false
    //之后便会进入onInterceptTouchEvent 询问是否拦截,也就是只要是DOWN事件一定会询问是否拦截
   //onInterceptTouchEvent 默认返回 false,默认不拦截,如果拦截需要重写该方法
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

最后来一张时序图(字小可以放大下页面)

注:该时序图以一个Button的点击事件为例,时序图展示了ACTION_DOWN事件从Activity传递到View的流程,View中展示的是ACTION_UP事件在View中的处理流程

后续更新