Android 事件分发流程

1,018 阅读12分钟

Read The Fucking Source Code

引言

学习 View 事件分发,就像外地人上了黑车!      — KunMinX

源码版本(Android Q — API 29)

1. 顶层俯瞰

1.1 自顶向下分发

1.2 递归事件分发

2 DecorView Touch 从何而来?

2.1 input 随 Vsync 而来

ViewRootImpl注册Vsync信号的input回调(主要围绕ViewRootImpl展开,更下层的处理流程后续会单独讲解:本文省略 EventHub -> InputReader -> InputDispatcher -> Choreographer流程)

void scheduleConsumeBatchedInput() {
        if (!mConsumeBatchedInputScheduled) {
            mConsumeBatchedInputScheduled = true;
            //通过Choreographer注册vsync信号的input回调
            mChoreographer.postCallback(Choreographer.CALLBACK_INPUT,
                    mConsumedBatchedInputRunnable, null);
        }
    }

2.2 input 事件的处理

ViewRootImpl的input组装责任链(后续有专门文章讲解),我们只分析TouchEvent(ViewPostImeInputStage)的处理

private int processPointerEvent(QueuedInputEvent q) {
            final MotionEvent event = (MotionEvent)q.mEvent;

            mAttachInfo.mUnbufferedDispatchRequested = false;
            mAttachInfo.mHandlingPointerEvent = true;
            //mView就是DecorView,由此则进入了DecorView的分发流程
            boolean handled = mView.dispatchPointerEvent(event);
            maybeUpdatePointerIcon(event);
            maybeUpdateTooltip(event);
            mAttachInfo.mHandlingPointerEvent = false;
            if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
                mUnbufferedInputDispatch = true;
                if (mConsumeBatchedInputScheduled) {
                    scheduleConsumeBatchedInputImmediately();
                }
            }
            return handled ? FINISH_HANDLED : FORWARD;
        }

3 DecorView 只是个工具人

  • 你以为从现在开始就进入了Touch事件的分发流程了吗?并没有……
  • 接着上面的流程,刚才说到了 dispatchPointerEvent。

3.1 View 分发

我们来看View中的dispatchPointerEvent()方法。

public final boolean dispatchPointerEvent(MotionEvent event) {
        if (event.isTouchEvent()) {
            //TouchEvent的流程进行分发。
            return dispatchTouchEvent(event);
        } else {
            return dispatchGenericMotionEvent(event);
        }
    }

3.2 DecorView 分发

我们来看DecorView的dispatchTouchEvent方法。

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //看到没有,要检查window中的CallBack是否为空,为空才进行分发,不为空则要走CallBack的回调。
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }

3.3 Window CallBack 设置

我们来看Activity中的attach方法。

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);

        //不为空,Activity设置了自己的CallBack给Window(也就是PhoneWindow)
        mWindow.setCallback(this);
        
        //代码省略……
    }

3.4 Activity 分发

我们来看Actvitiy中的dispatchTouchEvent方法。

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            //第一次按下操作时,进行事件反馈,空实现,可以覆写来监控touch的触发。
            onUserInteraction();
        }
        //通过顶层Window(PhoneWindow)来分发
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        //无任何处理,交给Activity的onTouchEvent处理。
        return onTouchEvent(ev);
    }

3.5 PhoneWindow 分发

我们来看PhoneWindow中的superDispatchTouchEvent方法。

@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        //调到了DecorView的superDispatchTouchEvent方法。
        return mDecor.superDispatchTouchEvent(event);
    }

3.6 DecorView 分发

我们来看DecorView中的superDispatchTouchEvent方法。

public boolean superDispatchTouchEvent(MotionEvent event) {
        //哈拉少,这才真正进入到了DecorView的事件分发流程
        return super.dispatchTouchEvent(event);
    }

3.7 工具人的自我修养

为什么作为一个顶层的View,会沦落为一个工具人?

  • 其实道理很简单,事件分发是按照View树进行递归分发的。这就要从不同的视角来剖析了。
  • 从View树的视角来看是这样子的(DecorView是最顶层的View)
  • 但是从整个页面的视角来看,并不是这样子的,应该是Activity -> Window -> DecorView -> ViewGroup/View。
  • 我们可以把它看成一个等级制度分明的狼群(自顶向下的结构),顶级管理者必须要有一票否决权(才有清晰的层级结构)。
  • 接下来再看下面这张图,是不是从事件分发的角度,就可以更好的理解他们的结构。

4 Touch 事件分发初探

4.1 事件分发的本质是递归

递归的本质是任务的下发和结果的上报。这句话理解起来晦涩。我们还是从实际案例来举证,再来理解这个问题。

4.2 ViewGroup getChildAt()

4.2.1 我们在DecorView的Content布局(就是onCreate中setContentView的布局)中,打印一些元素。

@Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        Log.d(TAG, "onFinishInflate: 0 = " + getChildAt(0));
        Log.d(TAG, "onFinishInflate: 1 = " + getChildAt(1));
        Log.d(TAG, "onFinishInflate: 2 = " + getChildAt(2));
        Log.d(TAG, "onFinishInflate: 3 = " + getChildAt(3));
    }

4.2.2 布局长啥样(xml就不拿出来看了,效果一样)?

4.2.3 看下打印出的结果(子元素)。

4.2.4 带着结论看源码。

 我们可以看出,在ViewGoup中的子元素包括View/ViewGroup,子ViewGroup虽然是一个包装布局,但是在它父亲(DecorViewContent)看来,它永远都是个孩子(ViewGroupChild)。

 那么事件分发是如何进行的呢?我们就用上面的例子简单说明。假如点击的是ViewNoChild

  • 首先拿到根布局(DecorViewContent)的子元素个数,图中的话是3个(上面的log中已经有打印了)。
  • 然后将子元素进行逆序遍历(为什么要进行逆序遍历?因为如果布局存在覆盖的话,逆序拿出来的子元素就是用户的可视元素,也就是最有可能消费事件分发的View)。
  • 首先拿出ViewGroupNoChild,判断是否在点击区域,发现不在,pass,继续遍历。
  • 然后拿出ViewGroupChild,判断是否在点击区域,发现在,继续遍历ViewGroupChild的子元素。
  • 遍历规则同上,最终的递归遍历顺序是:DecorViewContent -> ViewGroupChild -> ViewNoChlid。(ps:当然不考虑事件分发时可能会进行Z轴列表的查找优化)。
  • 下图附上遍历结果:

5 Touch 事件分发原理

Touch 事件自顶向下分发 + 自底向上消费。

事件分发的流程ViewGroup -> ViewGroup(n个) -> View。 上面我们分析到了DecorView的事件分发:DecorView继承自FrameLayout,那就从ViewGroup看起。

5.1 ViewGroup 事件分发

我们来看ViewGroup的dispatchTouchEvent。

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //代码省略……
        boolean handled = false;
        //符合隐私政策则继续分发,否则忽略
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            //DOWN事件发生,清除重置TouchTargets & 重置触摸状态
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            final boolean intercepted;
            // 发生ACTION_DOWN事件或者已经存在分发的目标(mFirstTouchTarget != null)
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                //子View可通过调用requestDisallowInterceptTouchEvent,不让父View拦截事件
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                //判断是否允许调用拦截器
                if (!disallowIntercept) {
                    //拦截方法(可以覆写onInterceptTouchEvent进行拦截)
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                //不是DOWN事件,也不存在分发目标,那么开始拦截
                intercepted = true;
            }
            //代码省略……
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            //不是取消事件 & 不拦截事件,则进入内部流程处理
            if (!canceled && !intercepted) {
                // 代码省略……
                //DOWN事件或者多点触控事件(这就是为什么TouchTarget设计成链表的原因:因为分发目标可能不止一个)
                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;

                    // 清空之前触摸对象
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    //存在子视图
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x =
                                isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                        final float y =
                                isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                        // 获取一个视图组的先序列表,通过虚拟的Z轴来排序。
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //逆序遍历,从用户视角来看,就是从最顶层的view开始遍历。
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            //按照前面的Z轴优化序列,更快的进行查找最顶层的视图
                            //从传统的逆序变为了:逆序 + Z轴优先的策略
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            //如果View不可见或者不在View范围内,则跳过本次循环
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            //当前view是否已经在分发目标列表中,是则更新内容
                            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);
                            //如果触摸位置在View的区域内,则把事件递归分发给子View或ViewGroup
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                //表示找到分发目标
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                //如果Z轴的先序列表存在,则找到对应的view对应的原始的逆序index
                                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;
                                }
                                //记录touch坐标
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                //添加view到分发目标
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                //标记进行了目标view的分发,防止重复分发(逻辑在后面)
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                            ev.setTargetAccessibilityFocus(false);
                        }
                        //辅助排序列表清空
                        if (preorderedList != null) preorderedList.clear();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        //未找到接收事件的视图。
                        //将坐标内容设置给最近添加的分发目标。
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            // Dispatch to touch targets.
            //如果没有找到分发目标,则child的参数以null传递进行分发(其实就是调用父类(当前ViewGroup)的分发,子view不处理)
            // mFirstTouchTarget赋值是在通过addTouchTarget方法设置的
            // 只有处理ACTION_DOWN事件,才会进入addTouchTarget方法。(这就为什么View没有处理DOWN事件,就不会接收到后续的事件了)
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // 发送到触摸目标(不包括新的触摸目标,如果我们已经发送到它)。必要时取消触摸目标。
                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;
                        //如果是拦截事件,那么子View会进行分发ACTION_UP事件
                        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;
                }
            }

            //当发生抬起或取消事件,清空分发目标
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        } //onFilterTouchEventForSecurity结束

        //输入事件一致性验证器,比如事件上报是否符合预期,DOWN与UP匹配等。
        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        //返回是否处理,这个结果
        //广义来说:当前层的dispatchTouchEvent / onTouch 结果影响上层是否会继续执行onTouch方法。
        return handled;
    }

5.2 ViewGroup 分发递归执行者

ViewGroup处理递归的核心方法:dispatchTransformedTouchEvent。

  • 分支1:当子视图(Child)是View时,调用Child的dispatchTouchEvent(父类View方法)方法进行处理 dispatchTouchEvent -> onTouchEvent。
  • 分支2:当子视图(Child)是ViewGroup时,Child不为空,则继续执行ViewGroup的dispatchTouchEvent 方法进行递归调用。
  • 分支3:当子视图(Child)是ViewGroup时,Child为空,调用Child的dispatchTouchEvent(父类View方法)方法进行处理 dispatchTouchEvent -> onTouchEvent。

我们来看ViewGroup的dispatchTransformedTouchEvent。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // 发生取消操作 或者 父类拦截操作,则处理下面流程
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        // Calculate the number of pointers to deliver.
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

        //如果出于某种原因,我们最终处于一种不一致的状态
        //可能会生成一个没有指针的运动事件,然后删除该事件。
        if (newPointerIdBits == 0) {
            return false;
        }

        //如果touch id的数量是相同的,我们不需要执行任何不可逆的转换,
        //那么可以重用运动事件来进行调度,只要安全还原所做的任何更改制造。
        //否则我们需要复制一份。
        final MotionEvent transformedEvent;
        //touch id是否相同
        if (newPointerIdBits == oldPointerIdBits) {
            //child为空 或者 变换矩阵是单位矩阵
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    //分支3(前面有归纳)
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    //分支1(前面有归纳) 或者 分支2(前面有归纳)
                    handled = child.dispatchTouchEvent(event);
                    //调整事件位置
                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            //拷贝事件
            transformedEvent = MotionEvent.obtain(event);
        } else {
            //分离事件
            transformedEvent = event.split(newPointerIdBits);
        }

        // 执行任何必要的转换和调度。
        if (child == null) {
            //分支3(前面有归纳)
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                //不是单位矩阵则进行转换
                transformedEvent.transform(child.getInverseMatrix());
            }
            //分支1(前面有归纳) 或者 分支2(前面有归纳)
            handled = child.dispatchTouchEvent(transformedEvent);
        }

        //回收transformedEvent
        transformedEvent.recycle();
        return handled;
    }

5.3 ViewGroup 分发拦截器

我们来看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;
    }

5.4 View 事件分发

我们来看View中的dispatchTouchEvent方法。

public boolean dispatchTouchEvent(MotionEvent event) {
       //代码忽略……
        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 新手势的防御性清理(针对滚动操作)
            stopNestedScroll();
        }

        //  优先级:OnTouchListener.onTouch > onTouchEvent
        if (onFilterTouchEventForSecurity(event)) {
            //如果有滚动条拖动,则代表消费
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //OnTouchListener.onTouch优先处理
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //onTouchEvent其次处理(如果OnTouchListener.onTouch处理了,则忽略)
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        //事件校验器
        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // 处理取消或者抬起操作(针对嵌套滚动)
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        //返回处理结果
        return result;
    }

6 汇总小结

  • 事件分发的核心 是在ViewGroup的dispatchTouchEvent的action_down事件处理中,找到分发目标(mFirstTouchTarget),步骤如下:   1. 通过逆序遍历子View的dispatchTouchEvent方法,如果发现有子View的dispatchTouchEvent方法返回true,那么找到了分发目标。
      2. 当然找到了分发目标,后续的事件不进行递归,直接交由分发目标进行处理消费。
      3. 事件的分发过程中,任何一层的ViewGroup都可以拦截事件分发。拦截的概念有两种:
        3.1 覆写ViewGroup的onInterceptTouchEvent方法,返回true,表示拦截成功。
        3.2 子View设置请求requestDisallowInterceptTouchEvent,表示是否允许父ViewGroup可以拦截。
      4. 如果父ViewGroup进行了拦截,则会先向子View分发一个ACTION_CANCEL事件,然后清空分发目标。

  • 正常的事件分发时序是:DOWN事件 + 若干MOVEW事件 + UP事件/CANCEL事件。

  • 跟踪一个触控点必须是依靠一个触控点的id,而不是他的索引 。

  • OnTouchEvent由下往上递归时,任何一层的OnTouchEvent消费该事件,则不再往上传递,消费结束。

  • 如果View没有消费ACTION_DOWN事件,则之后的ACTION_MOVE等事件都不会再接收。

  • 优先级:onTouchListener >>> onTouchEvent >>> setOnLongClickListener >>> OnClickListerner。

  • clickable和longClickable的设置为true就表示要消费分发的事件。

7. 问题思考

分发目标进行记录(TouchTarget责任链)的作用是什么?

  • 当事件触发(ACTION_DOWN)时,进行递归查找到分发目标,在后续的分发流程中,就可以跳过递归遍历的环节,直接分发。
  • 这也就是为什么View没有消费ACTION_DOWN事件,之后也不会接收到其他事件。
  • 这也就是为什么View消费了ACTION_DOWN事件,即使滑出当前View(不松手也不离开屏幕),当前View依然可以消费事件。

分发目标TouchTarget为什么是个链表?是个对象不行吗?

  • 我们知道只要DOWN事件返回true 遍历就会结束,就找到了分发目标。
  • 但是看了源码我们就知道,寻找分发对象的条件不止DOWN一个,当多点触控时,就会有多个分发目标。

拦截就表示在当前层消费了吗?

  • 拦截,表示在当前层结束了向下分发,只进行向上冒泡,提前结束了完整的递归调用。

解决滑动冲突的常见思路是什么?

  • 重写父ViewGroup组件的onInterceptTouchEvent方法,根据MotionEvent来判断是否拦截。

View的onClick和onTouch有什么区别?

  • onTouch是在dispatchTouchEvent中触发的。
  • onClick是在onTouchEvent消费事件中的action_up触发的。
  • 所以onTouch要先于onClick事件,我们也可以通过onTouch返回true来屏蔽掉onClick事件。

ViewGroup和View同时设置了onClick,哪个会执行?

  • 消费自底向上冒泡,会优先被View消费掉,ViewGroup不会响应。

当ViewGroup的子View重叠时,事件会如何分配?

  • 因为ViewGroup进行递归分发时,会进行 逆序 + Z轴优先列表 来遍历,所以会优先分配到最上面的View,也就是用户可见的最顶层的View。

ACTION_CANCEL事件 知多少?

  • View 收到 ACTION_DOWN 事件以后,上一个事件还没有结束(可能因为 APP 的切换、ANR 等导致系统扔掉了后续的事件),这个时候会先执行一次 ACTION_CANCEL(源码中有很多…)。
  • 最常见的拦截处理:父ViewGroup拦截了事件,子View会收到Cancel事件。
  • 最常见的拦截处理举例:   1. 比如纵向的ListView有很多Item(Item实现了onClick)。
      2. 如果在Item上纵向滑动,首先会进入到 ListView.dispatchTouchEvent
      3. Listview(父ViewGroup)检测满足拦截条件,进行拦截。
      4. 拦截后,ListView对它里面的分发目标责任链(一般来说就是只有一个Item的子View)发送Cancel事件。
      5. 子Item会收到Cancel事件,进行自己的事件分发结束处理。
      6. ListView会清空它里面的分发目标责任链,那么ListView中的分发目标责任链为空。
      7. 在 ListView.dispatchTouchEvent 的返回值会是true(拦截成功会返回true)。
      8. 因为事件分发是递归执行的,此时 ListView.dispatchTouchEvent 返回了true,则此次事件分发结束。
      9. 事件分发是连续上报的,比如MOVE事件,当下一个MOVE事件分发到来时。
      10. ListView的父ViewGroup中的分发目标责任链不为空,就是ListView。
      11. 所以会直接调用父ViewGroup的目标责任链的 dispatchTouchEvent 方法。
      12. 上面步骤 1 - 8 的直接流程中,已经将 ListView中的目标责任链清空了。
      13. 所以在执行 ListView.dispatchTouchEvent 方法时,会执行到责任链为空的处理流程(不会执行里面的递归,因为:不是Down事件 & 目标责任链为空)。
      14. 责任链为空的处理流程,就是 dispatchTransformedTouchEvent 的Child参数为空。
      15. 那么会执行 ListView的 super.dispatchTouchEvent, 这是父类View的方法。
      17. 前面我们讲过 View.dispatchTouchEvent 方法,会执行到 onTouchEvent方法。
      18. 好了,拦截流程全部分析结束。
      19. 当拦截后,事件分发流程就是:ListView的父辈们递归dispatchTouchEvent -> ListView.dispatchTouchEvent -> ListView.onTouchEvent。

小编的扩展链接

《Android 视图模块 全家桶》

优秀博客推荐

学习 View 事件分发,就像外地人上了黑车!
Android事件分发机制
Android View事件传递图解
Android进阶思考|Android 事件分发机制的设计与实现
Android事件分发机制二:viewGroup与view对事件的处理