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

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

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

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

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

基础回顾

1. 事件分发的对象是 ?

  • 指用户触摸屏幕时(屏幕指的是 View 和 ViewGroup 派生的所有控件)所产生的点击事件(Touch 事件) 。在 Android 中我们用 MotionEvent 对象来代替 。
  • 主要发生的 Touch 事件有四种


    image.png

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

image.png

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;
}
image.png

**事件分发的过程 **

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

具体分发过程

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

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

**配个图吧 **


image.png

PS2 :明确拦截的作用 。

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

参考文章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 的层级关系如下图

image.png
  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类实现 。

  1. 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 。

  1. DecorVIew 的dispatchTouchEvent 方法
 public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

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

  1. 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的点击事件拦截掉 。

待续 。。。

你可能感兴趣的:(Android 2020年面试系列(02 — View事件分发))