美文网首页
Android 事件分发之追本溯源

Android 事件分发之追本溯源

作者: Amter | 来源:发表于2020-03-18 21:12 被阅读0次

    前言

    • Android设备的界面交互带来了非常好的体验,在我们日常使用中,无时无刻不在触发着事件的分发;比如点击了淘宝某个图片,比如点击了掘金APP的某个按钮,都会触发系统的事件分发;
    • Android 事件分发也是自定义View很重要的一个知识点,搞懂事件分发,当自定义View或者解决滑动冲突的问题,都会显得胸有成竹了;
    • 和以往结合源码的方式讲解相比,更想通过另一种有趣的角度来分析,接下来让我们正式开始吧;

    1. 为什么要有事件分发机制?

    1.1,Android手机

    Android手机作为手持设备,界面显示区域并不是很大,为了有便携的效果,只能牺牲手机的显示区域;这就会带来一个问题,可视内容少;为了不影响用户体验,我们必须要在有限的区域做更多的展示,这就对界面的设计有很高的要求了;假如我们是Google的工程师,我们要怎么来设计界面,以此带来好的体验效果呢?

    1.2,脑洞一

    第一种设计:将界面显示区域切割,根据所需要显示的视图,切割为无数块,每一块对应着一部分视图;如下:

    image

    这种界面设计简单粗暴,需要多少个视图,就将界面切割成多少个视图模块,以此来放下所有的视图内容;当然,这样设计显而易见会有问题,当视图越来越多的时候,每一个视图的模块所能展示的区域就会越来越小,这样体验效果是肯定不行的;

    1.3,脑洞二

    第二种设计:既然通过切割显示区域以此来展示视图的方案有问题,那么我们就来试试重叠的效果吧;如下:

    image

    这种设计很好的解决了视图模块过多时,显示区域不够展示的问题;但是也会存在问题,每一个显示区域和用户的交互顺序混乱了,比如我要和模块为4的视图做交互,结果触发了视图5的交互效果,而脑洞一方案则没有该问题;既然如此,那么我们能不能针对脑洞二的方案来进行优化呢?
    答案是:有的!

    1.3,设计交互机制

    当多个模块视图重叠时,要协调好与用户的交互就极其重要了,毕竟涉及到用户体验;

    当用户的触碰屏幕的显示区域,我们并不知道哪个模块需要和用户进行交互,而我们又不能让用户和其中一个模块的交互失效,那么我们只能去遍历重叠的模块,由内部的视图来决定是否需要相应用户的操作;

    这样就可以解决多个模块视图重叠时,哪个模块需要相应用户交互的问题了;

    而这正是Android的事件分发机制;

    当然上面只是我的脑洞,用于方便理解,如果你有更好的想法,可以和我交流;

    那么这种机制是怎么来实现这种效果的呢?请继续往下看;

    在深入分析事件分发之前,先来了解一下事件的来源;

    2. 事件是什么,是怎么产生的?

    2.1,事件的来源

    当屏幕被触摸,Linux内核会将硬件产生的触摸事件包装为Event存到/dev/input/event[x]目录下。

    接着,系统创建的一个InputReaderThread线程loop起来让EventHub调用getEvent()不断的从/dev/input/文件夹下读取输入事件。

    然后InputReader则从EventHub中获得事件交给InputDispatcher。

    而InputDispatcher又会把事件分发到需要的地方,比如ViewRootImpl的WindowInputEventReceiver中。

    这里只是简单了解一下大概的流程,源码过于复杂,这里不做具体的分析;

    概括之:当触摸屏幕的时候,硬件会捕捉到用户的触摸动作,告诉系统内核,系统内核将该事件保存下来,然后有一个线程会将这个事件读取出来,交由专门分发的类进行分发;

    image

    2.2,事件的类型

    当屏幕被触摸时,系统底层会将触摸事件(坐标和时间等)封装成MotionEvent事件返回给上层 View;从用户首次触摸屏幕开始,经历手指在屏幕表面的任何移动,直到手指离开屏幕时结束都会产生一系列事件;

    MotionEvent的类型:

    • MotionEvent.ACTION_DOWN:当屏幕检测到第一个触点按下之后就会触发到这个事件
    • MotionEvent.ACTION_MOVE:当触点在屏幕上移动时触发;
    • MotionEvent.ACTION_UP:当触点松开时被触发;
    • MotionEvent.ACTION_CANCEL:由系统在需要的时候触发,不由用户直接触发;

    2.3,事件的是怎么传到Activity的?

    我们知道事件分发是从Activity的dispatchTouchEvent方法分发到子类的,但是是否有这样的疑惑:事件是怎么传到Activity的呢?

    让我们来跟踪源码分析一下吧!

    image

    首先,在Activity里面调用Thread.dumpStack()来打印调用栈,来看看方法的调用流程;

    image

    打印之后的日志是这样的;怎样,是不是方法调用来源一目了然!

    源码这么多,要怎么看呢?别急,我们一步步来,先从WindowInputEventReceiver来开始分析,为什么呢?因为这个类会接收到Linux内核传递到应用层的事件,并将其传递到Activity;

    我们先来看一下这个类:

    image

    这里主要看onInputEvent方法,这个方法接受到内核传递过来的事件,然后通过enqueueInputEvent来进行传递,这里经过一系列的调用,会走到ViewRootIml的deliverInputEvent方法;

    image

    这里会通过InputStage调用deliver来传递事件,这个InputStage是一个抽象类,具体的实现有好几个类,每个类功能都不同,这里我们只关注和事件有关的ViewPostImeInputStage类;

    image

    最终会调用到ViewPostImeInputStage的processPointerEvent方法;

    image

    下面来看一下这个processPointerEvent方法里面做了啥?

    image

    调用了mView的dispatchPointerEvent方法,这个mView是什么呢?我们来看看这个mView是在哪里赋值的?

    查看源码最终发现是在ViewRootIml的setView方法里面赋值的,这个方法是用来干嘛的呢? 我这里简短说一下,这个方法是启动Activity的时候通过WindowManager调用addView方法,将DecorView传进去,最终赋值给ViewRootIml的mView,也就是说这里的mView,其实就是DecorView;

    image

    看这个任务栈的调用,确实是走到了DecorView的dispatchPointerEvent方法,

    image

    来看一下DecorView的dispatchTouchEvent里面做了啥?

    image

    通过mWindow.getCallback()获取到Window.Callback接口,然后再回调给Activity,而这个mWindow就是Activity的创建PhoneWindow;

    那么这个Window.Callback是在哪里设置的呢?有看过Activity的启动流程的应该有注意到,Activity的启动会调用attach方法进行初始化,而这个Window.Callback就是在attach方法里面通过PhoneWindow来进行设置的;

    image

    也就是说最终实现是在Activity的dispatchTouchEvent方法,那么上面DecorView的dispatchTouchEvent最终会走到Activity的dispatchTouchEvent,后面就是Activity的分发流程了;

    看一下流程图:

    image

    3. 事件分发机制是怎么实现的?

    3.1,设计模式

    在分析事件分发机制之前,我们先来看一下事件分发涉及的设计模式;

    这个设计模式是事件分发机制的核心,Google工程师是通过这个设计模式来设计事件分发机制的;理解了这个设计模式有助于我们理解事件分发机制;

    而这个设计模式就是责任链模式;

    3.2,责任链模式

    顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。

    在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。

    image

    下面我们通过一段伪代码来解读这个模式:

        // 请求
            switch (request) {
                case 0:
                    // 对象一接收请求并处理
                    break;
                case 1:
                    // 对象二接收请求并处理
                    break;
                case 2:
                    // 对象三接收请求并处理
                    break;
                case 3:
                    // 对象四接收请求并处理
                    break;
                case 4:
                    // 对象五接收请求并处理
                    break;
                default:
                    // 默认对象接收请求并处理
            }
    

    上面这个就是我们用的最熟悉的责任链模式,当有一个请求进入责任链的时候,会遍历当前责任链上所有的对象,如果匹配到了则提前结束遍历,如果匹配不到则会被默认的对象接收;

    责任链的本质是一个单向的链表结构,当有请求进入时,只会单向传递,直到被接收;

    3.3,具体实现

    上面我们理解了责任链设计模式之后,接下来我们来看看事件分发机制的具体实现;

    在上上篇博客里面分析了View的绘制流程,里面提到了View的层次关系,Activity是View的宿主,而最顶层的View是DecorView,而DecorView里面则是View树的结构,那么我们将这些关系一一对应到了责任链里面,来看看效果吧;

    image

    当有一个事件进入责任链时,会从最顶层的DecorView开始往View树传递,直到被其中一个对象所消费;

    那么由此可知事件分发总共可以分为三个部分;

    • Activity的事件分发
    • ViewGroup的事件分发
    • View的事件分发

    接下来先来看一下事件分发机制的核心方法,主要有三个;

    • dispatchTouchEvent():传递事件,当前对象可以将事件通过这个方法传递给下一个对象;
    • onInterceptTouchEvent():拦截事件;当前对象通过拦截事件,来终止事件的传递;
    • onTouchEvent():处理事件,事件的最终去处;

    下面我们通过Demo来看看事件是怎么传递的?

    image

    写了一个简单的布局,一个RelativeLayout里面放一个按钮;

    接下来点击屏幕,看看流程会怎么走;

    image

    1,Activity的事件分发

    step1:当点击屏幕的时候,会产出一个ACTION_DOWN的事件,传递到了Activity的dispatchTouchEvent方法里,来看一下Activity的dispatchTouchEvent方法,这里调用了super.dispatchTouchEvent(ev),也就是走了父类的dispatchTouchEvent方法;

    image

    step2:进入Activity的dispatchTouchEvent方法里面,看一下做了啥;

    image

    这里面有三个方法,第一个onUserInteraction()是空方法;

        /**
         * Called whenever a key, touch, or trackball event is dispatched to the
         * activity.  Implement this method if you wish to know that the user has
         * interacted with the device in some way while your activity is running.
         * This callback and {@link #onUserLeaveHint} are intended to help
         * activities manage status bar notifications intelligently; specifically,
         * for helping activities determine the proper time to cancel a notfication.
         *
         * <p>All calls to your activitys {@link #onUserLeaveHint} callback will
         * be accompanied by calls to {@link #onUserInteraction}.  This
         * ensures that your activity will be told of relevant user activity such
         * as pulling down the notification pane and touching an item there.
         *
         * <p>Note that this callback will be invoked for the touch down action
         * that begins a touch gesture, but may not be invoked for the touch-moved
         * and touch-up actions that follow.
         *
         * @see #onUserLeaveHint()
         */
        public void onUserInteraction() {
        }
    

    将注释翻译过来的意思就是:
    每当Key,Touch,Trackball事件分发到当前Activity就会被调用。如果你想当你的Activity在运行的时候,能够得知用户正在与你的设备交互,你可以override该方法。

    这个回调方法和onUserLeaveHint是为了帮助Activities智能的管理状态栏Notification;特别是为了帮助Activities在恰当的时间取消Notification。

    所有Activity的onUserLeaveHint 回调都会伴随着onUserInteraction。这保证当用户相关的的操作都会被通知到,例如下拉下通知栏并点击其中的条目。
    这个方法不是重点,不需要过多关注;

    需要关注的是第二个方法getWindow().superDispatchTouchEvent(ev),这个方法最终走的是PhoneWindow的superDispatchTouchEvent();

    image

    step3:这个mDecor是DecorView,看看DecorView里的superDispatchTouchEvent(ev)方法做了啥?

    image

    这里面还是调的super,走的父类的方法;

    image

    最终走的是ViewGroup的dispatchTouchEvent()方法;在这个方法里面通过遍历当前所有的子View,通过子View的dispatchTouchEvent()方法将事件传递下去;ViewGroup的事件分发请看下面的分析;

    到这里Acitivity事件就已经传递到ViewGroup了,如果后续的对象都没有处理该事件,即getWindow().superDispatchTouchEvent(ev)方法返回false时,Activity就会通过onTouchEvent()把当前的事件处理掉;

    看一下Activity的onTouchEvent()里面做了啥?

    public boolean onTouchEvent(MotionEvent event) {
            if (mWindow.shouldCloseOnTouch(this, event)) {
                finish();
                return true;
            }
    
            return false;
        }
        
    // Window里面的方法;
    public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
        final boolean isOutside =
                event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
                || event.getAction() == MotionEvent.ACTION_OUTSIDE;
        if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
            return true;
        }
        return false;
    }
    

    Activity的onTouchEvent()会判断当前的事件是否在屏幕的边缘触发的,如果是,则返回true,否则返回false;

    总结为流程图:

    image

    2,ViewGroup的事件分发

    接下来我们来分析一下ViewGroup的事件分发;

    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
        // step1;
        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;
                }
        }
        ...
        for (int i = childrenCount - 1; i >= 0; i--) {
            ...
            // step2;
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){
                ...
            }
            ...
        }
    }
    
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
                ...
                if (child == null) {
                        handled = super.dispatchTouchEvent(event);
                    } else {
                        final float offsetX = mScrollX - child.mLeft;
                        final float offsetY = mScrollY - child.mTop;
                        event.offsetLocation(offsetX, offsetY);
                        // 将当前的事件分发下去;
                        handled = child.dispatchTouchEvent(event);
    
                        event.offsetLocation(-offsetX, -offsetY);
                    }
                    ...
    }
    

    step1:在ViewGroup的dispatchTouchEvent()方法里面,在进行事件分发之前,会先调用onInterceptTouchEvent(ev)方法,用于判断当前的事件是否拦截,如果被拦截了,则事件不分发给子类了,如果没有拦截则继续分发下去;

    这里需要注意的是,当事件为MotionEvent.ACTION_DOWN,才会走进onInterceptTouchEvent(ev)方法;

    在走这个onInterceptTouchEvent(ev)方法之前,还有一个判断条件,disallowIntercept,这个条件是用来判断是否要禁用拦截事件,如果禁用了,则不会调用拦截的方法了;子类可以通过调用requestDisallowInterceptTouchEvent()方法修改;

    image

    如果ViewGroup的子类如果没有重写onInterceptTouchEvent(ev)这个方法,那么就会走ViewGroup的方法,这里用了4个判断条件,但是默认都是走的false,不拦截事件;


    image

    step2:如果事件没有被拦截,那么就会遍历当前所有的子View,然后调用子View的dispatchTouchEvent()方法,将事件分发下去;

    image

    那如果被拦截了,则会走super.dispatchTouchEvent(event)方法,也就是View的dispatchTouchEvent(event)方法;这个逻辑写在dispatchTransformedTouchEvent()方法里;

    image

    到这里ViewGroup的分发就讲完了,至于ViewGroup拦截事件后,怎么处理事件,请看下面的View事件分析;

    流程图:

    image

    3,View的事件分发

    View的事件分发也是调用的dispatchTouchEvent(event)方法,让我们来看一下这个方法的逻辑;

    public boolean dispatchTouchEvent(MotionEvent event) {
            
           ...
            if (onFilterTouchEventForSecurity(event)) {
                if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                    result = true;
                }
                //noinspection SimplifiableIfStatement
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                        // step1
                    result = true;
                }
    
                if (!result && onTouchEvent(event)) {
                // step2
                    result = true;
                }
            }
    
           ...
    
            return result;
        }
    

    通过源码发现,当事件分发到了View的dispatchTouchEvent(event)后,事件就不会再继续分发下去了;那么这里面的逻辑是怎样的呢?

    step1:先判断当前View的状态是可响应的((mViewFlags & ENABLED_MASK) == ENABLED),再判断触摸监听mOnTouchListener的onTouch()的返回值,如果子类实现了OnTouchListener这个监听,并且返回了true,那么dispatchTouchEvent(event)就会返回true,表示当前View已经处理该事件;

    step2:判断当step1的状态为false时,则调用了onTouchEvent(event)来判断子类是否返回true,返回true则表示当前View已经处理该事件;

    看一下onTouchEvent(event)的源码:

    public boolean onTouchEvent(MotionEvent event) {
            
            // 判断当前状态是否是可点击的
            final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
            ...
    
            if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
                switch (action) {
                    case MotionEvent.ACTION_UP:
                        ...
    
                        performClickInternal();
                        break;
    
                    case MotionEvent.ACTION_DOWN:
                        ...
                        checkForLongClick(0, x, y);
                        break;
    
                   ...
                }
    
                return true;
            }
    
            return false;
        }
    

    这里需要关注的是MotionEvent.ACTION_DOWN和MotionEvent.ACTION_UP事件;

    • MotionEvent.ACTION_UP:调用了performClickInternal()触发了点击监听的回调onClick(),这个是我们最常用的点击事件回调;具体是在performClick()方法里面实现的;
    image
    • MotionEvent.ACTION_DOWN:在这个判断里面,调用了checkForLongClick(0, x, y)触发了长按监听的回调,也就是onLongClick()方法;
    image

    通过判断当前的视图是否处于按压状态,且判断此视图添加的窗口数量是否和原始的一致,如果这两种状态都满足,就会触发长按监听回调;最终调用是在performLongClickInternal()方法里面;

    image

    流程图:

    image

    4. 总结

    到这里,事件分发的流程就已经讲完了;

    让我们来回忆一下上面提到的三个方法:

    • dispatchTouchEvent(event):将事件传递给下一层,当传递到View这一层的时候,就不会再继续往下传了;
    • onInterceptTouchEvent(ev):将事件拦截下来,只有ViewGroup有这个方法,当拦截后,就会走View的dispatchTouchEvent(event)方法来处理事件;
    • onTouchEvent(event):处理事件,在Activity层时,只有触摸边界的时候才会处理事件,在ViewGroup和View层时,会先判断是否有touch监听,没有的话,才会触发这个方法去处理事件;

    分析到这里,关于上面脑洞一的设计,这种分发机制是不是完美的解决了交互的问题;
    无论你视图重叠多少,事件都会一层层的传递过去,直到被某一层处理掉;有了这个机制,Android的界面就变的更灵活,更有创造性了;

    看一下汇总的流程图:

    image

    关于自定义View相关的文章,之前也总结了几篇,感兴趣的可以看一下;

    参考&感谢

    关于我

    兄dei,如果我的文章对你有帮助的话,请给我点个❤️,也可以关注一下我的Github博客;

    相关文章

      网友评论

          本文标题:Android 事件分发之追本溯源

          本文链接:https://www.haomeiwen.com/subject/gocgehtx.html