Android 2020年面试系列(02 — View事件分发)

时隔 14 天 ,今天终于出关了 。

继上一篇文章  Android 2020年面试系列(01— Java 集合) 面试干货系列 02 篇 。

参考书籍 《Android 开发艺术探索》第三章。

参考了一些资料竟然不知道从何写起 ,关于 View 的事件分发牵扯的知识点其实挺多的 。。。

基础回顾

1. 事件分发的对象是 ?

  • 指用户触摸屏幕时(屏幕指的是 View 和 ViewGroup 派生的所有控件)所产生的点击事件(Touch 事件) 。在 Android 中我们用 MotionEvent 对象来代替 。
  • 主要发生的 Touch 事件有四种
事件 触发场景 单次事件流中触发的次数
MotionEvent.ACTION_DOWN 在屏幕按下时 1次
MotionEvent.ACTION_MOVE 在屏幕上滑动时 0次或多次
MotionEvent.ACTION_UP 在屏幕抬起时 0次或1次
MotionEvent.ACTION_CANCLE 滑动超出控件边界时 0次或1次

按下、滑动、抬起、取消这几种事件组成了一个事件流。事件流以按下为开始,中间可能有若干次滑动,以抬起或取消作为结束。

2. 事件分发的本质 ?

产生点击事件(MotionEvent)之后向某个 View 进行传递并最终得到拦截处理 。

PS:Android 事件分发的本质是要解决点击事件由那个对象发出 ,经过哪些对象 ,最终达到哪些对象并进行处理 。

PS:有人说事件分发的本质就是递归 。"递归"是一种包含 "递"流程和 "归"流程的算法 ,当我们在找寻目标时,便是处于 “递” 流程,当我们找到目标,打算从目标开始来执行事务时,我们便开启了 “归” 流程。

分发事件的组件 :也称为分发事件者 ,包括 Activity 、ViewGroup 、View 。所以想要理解 Android 事件分发 

  • Activity对点击事件的分发机制
  • ViewGroup对点击事件的分发机制
  • View对点击事件的分发机制

3. 分发的核心方法?

  • dispatchTouchEvent()该方法负责是否向下分发事件(分发事件)。返回结果受当前 View 的 onTouchEvent 和下级 View 的 dispatchTouchEvent 影响 。
  • onInterceptTouchEvent()该方法负责所属 View 是否拦截事件(拦截事件)。如果当前 View 拦截了某个事件 ,那么在同一个事件序列当中 ,此方法不会被再次调用 。
  • onTouchEvent()该方法处理事件(消费事件)。在 dispatchTouchEvent ()方法中调用 ,返回结果表示是否消耗当前事件 ,如果不消耗 ,在同一个时间序列中 ,当前 View 无法再次接收到事件 。

伪代码

 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;
        if(onInterceptTouchEvent(ev)){
            consume = onTouchEvent(ev);
        }else {
            consume = child.dispatchTouchEvent(ev);
        }
        return consume;
}
组件 dispatchTouchEvent onTouchEvent onInterceptTouchEvent
Activity 存在 存在 不存在
ViewGroup 存在 存在 存在
View 存在 存在 不存在

事件分发的过程 

事件分发顺序 :Activity(Window)   —> ViewGroup —>View

具体分发过程

          点击事件首先从 ActivitydispatchTouchEvent 方法开始分发事件的  ,会调用 ViewGroup dispatchTouchEvent方法 。如果 ViewGroup 拦截事件即 onInterceptTouchEvent 方法会返回 true ,事件会交给 ViewGroup 的 onTouchEvent 方法处理 。如果 ViewGrop 不拦截事件 ,则事件会向下传递ViewGrou 所包含的子 View ,这时会调用子 View 的 dispatchTouchEvent 方法 。子 View 是否处理这个事件是由两个因素决定的子 View 是否在播放动画和点击事件的坐标是否落在子元素的区域内 ,如果子 View  满足这两个因素则事件交给他处理 ,调用子 View 的 onTouchEvent 方法 。处理完之后会进行事件回溯 。

PS :每次完整的事件分发流程,都包含自上而下的 “递” ,和自下而上的 “归” 2 个流程。我们常说的是事件传递之后会有事件回溯流程 。这个你可以在打印的时候就能看出来 。

2020-03-08 15:29:26.745 com.developers.myapplication I/SuperEvent: Activity -- dispatchTouchEvent  
2020-03-08 15:29:26.746 com.developers.myapplication I/SuperEvent: ViewGroup -- dispatchTouchEvent  
2020-03-08 15:29:26.746 com.developers.myapplication I/SuperEvent: ViewGroup -- onInterceptTouchEvent  
2020-03-08 15:29:26.746 com.developers.myapplication I/SuperEvent: ViewGroup -- onTouchEvent  
2020-03-08 15:29:26.802 com.developers.myapplication I/SuperEvent: Activity -- dispatchTouchEvent  
2020-03-08 15:29:26.802 com.developers.myapplication I/SuperEvent: ViewGroup -- dispatchTouchEvent  
2020-03-08 15:29:26.802 com.developers.myapplication I/SuperEvent: ViewGroup -- onTouchEvent 

以上打印的前提条件是 ViewGroup 将拦截消费这次事件 。首先调用的是 ActivitydispatchTouchEvent  方法 ,然后是调用 ViewGroup 的一系列拦截消费方法 (到目前为止打印 log 都是符合时间传递规则的)。从 ViewGroup 的 onTouchEvent 方法开始之后便是 Activity 的 dispatchTouchEvent 方法 (这就是事件回溯 ,可以理解为事件消费之后会对上级进行一次上报 )。在此可能会有疑问 ? 回溯之后还有一次 ViewGroup 的调用 。 下文分析 Activity 的事件分发源码的时候就会明白这是因为 dispatchTouchEvent 方法里面的调用链 。

PS1:很多人把事件分发的流程比喻为职称任务的分配 ,领导自上而下、逐级地下达任务、寻找目标执行者 ,若当前执行者无法完成这个任务 ,那么上报给他的上级 ,由他的上级来执行;如果找到合适的执行者时,便开启自下而上的回溯流程。回溯就好比是项目负责人给你分配一个功能任务 ,等你做完之后是不是还要给你负责人反馈一下你完成了 。对 就是这个意思 。

配个图吧 

PS2 :明确拦截的作用 。

网上的内容总是让人误以为,当前层级拦截了,就直接在当前层级消费了。实际上 ,当前层级拦截了 ,只是提前结束了 “递” 流程,并从当前层级步入 “归” 流程而已 。

PS3:简单理解可以总结如下

1. 如果View 、ViewGroup 都没有对事件进行消费 ,会以 Activity 的 onTouchEvent 作为终点 ;

2. 如果 ViewGroup 中断事件并且对事件进行消费 ,会以 ViewGroup 的 onTouchEvent 作为终点 ;

3. 如果 ViewGroup 没有中断事件 ,但 View (如 Button)对事件进行消费 ,会以 Button 的 ACTION_UP 作为终点 ;

4. 如果 ViewGroup 没有中断事件 ,View 也没有对事件进行消费 ,会以 Activity 的 onTouchEvent 作为终点 。

参考文章1:  https://mp.weixin.qq.com/s/JRrtG79A7bxis8YumvhalQ

参考文章2:  https://www.cnblogs.com/chengxuyinli/p/9979826.html

源码分析

根据上述的内容 ,事件分发的本质是事件在 VIew 传递的过程 ,顺序是 Activity(Window)   —> ViewGroup —>View 。所以我们分析源码就按照时间的分发顺序一个一个来进行 。

Activity 的事件分发源码

知识补充 :对于 Android View 的层级关系如下图 

1. 首先点击事件是在 Activity 的 dispatchTouchEvent 方法中 开始的

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

如果 getWindow.superDispatchTouchEvent (ev) 返回 ture ,那么 Activity 的 dispatchTouchEvent 的方法就结束 (该点击事件停止往下传递 ,事件分发结束),否则继续调用 onTouchEvent 方法 。

getWindow () 获取的是 window 对象 ,Window 是抽象类 ,唯一的实现类是 PhoneWindow ,即此处的Window类对象 = PhoneWindow 类对象 Window 类的 superDispatchTouchEvent() = 1个抽象方法,由子类PhoneWindow类实现 。

2. PhoneWindow 的 superDispatcherTouchEvent  方法 


 private DecorView mDecor;


 @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

mDecor ==  DecorVIew ,是顶层 View (DecorVIew)的实例对象 。DecorVIew 继承自 FrameLayout ,FrameLayout 是 ViewGroup 的子类 ,所以 DecorVIew 的间接父类等于 ViewGroup 。

3. DecorVIew 的dispatchTouchEvent 方法

 public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

所以调用的的是 ViewGroup 的dispatchTouchEvent  。在回到 Activity 的 dispatchTouchEvent 方法最后的 onTouchEvent 里面 。

4. Activity 的 onTouchEvent 方法 

    public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

ViewGroup 的事件分发源码

1. 首先看 dispatchTouchEvent 方法 。

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        if (!disallowIntercept) {/*****<分析一>******/
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }

        // 重点分析一下这个 for 循环遍历   /******<分析二>*****/
        for (int i = childrenCount - 1; i >= 0; i--) {
            final int childIndex = getAndVerifyPreorderedIndex(
                    childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(
                    preorderedList, children, childIndex);

           
            if (childWithAccessibilityFocus != null) {
                if (childWithAccessibilityFocus != child) {
                    continue;
                }
                childWithAccessibilityFocus = null;
                i = childrenCount - 1;
            }

            // isTransformeTouchPointView 判断当前 child view 是不是点击的 View  /******<分析三>*****/
            if (!canViewReceivePointerEvents(child)
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                ev.setTargetAccessibilityFocus(false);
                continue;
            }

            newTouchTarget = getTouchTarget(child);
            if (newTouchTarget != null) {
                newTouchTarget.pointerIdBits |= idBitsToAssign;
                break;
            }

            resetCancelNextUpFlag(child);
            /*****<分析四>******/
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                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(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;
                break;
            }

            ev.setTargetAccessibilityFocus(false);
        }

    }

分析《一》:disallowIntercept 指的是是否禁用事件拦截的功能 (默认是 false),可以调用 requestDisallowInterceptTouchEvnent()对值进行修改 。

分析《二》:for 循环遍历当前 ViewGroup 的所有 子 View ,进行事件传递 。

分析《三》:isTransformedTouchPointInView 方法是判断当前遍历的 child view 是不是正在点击的 View ,

   protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }

分析《四》:dispatchTransformedTouchEvent 方法里面对 child view 进行 dispatchTouchEvent ,积是实现了从 ViewGroup 到子 View 的传递 。 调用子View的dispatchTouchEvent后是有返回值的,若该控件可点击,那么点击时,dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立 ,于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出,即把ViewGroup的点击事件拦截掉 。

PS :之前面试遇到的一个问题 。关于事件分发的 。

一个页面三个 LinearLayout 嵌套 (如下图),如果点击 命名为 1 的 LinearLayout 的时候 ,点击事件是如何传递的 ?

如果点击 1 ,在移动到 2的区域 ,点击事件是如何传递的 ?

PS:如有遗漏和其他理解欢迎补充。

如果感觉文章对您有帮助 ,可以关注我的公众号 SuperMaxs (如果有技术问题可以通过公众号加私人微信)。

个人Github 账号 :https://github.com/spuermax

唯有学习才是大势所趋 。

发布了71 篇原创文章 · 获赞 57 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_37492806/article/details/104652331