阅读 1306

Android 事件分发之追本溯源

前言

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

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

1.1,Android手机

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

1.2,脑洞一

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

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

1.3,脑洞二

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

这种设计很好的解决了视图模块过多时,显示区域不够展示的问题;但是也会存在问题,每一个显示区域和用户的交互顺序混乱了,比如我要和模块为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中。

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

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

2.2,事件的类型

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

MotionEvent的类型:

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

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

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

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

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

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

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

我们先来看一下这个类:

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

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

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

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

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

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

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

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

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

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

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

看一下流程图:

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

3.1,设计模式

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

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

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

3.2,责任链模式

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

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

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

    // 请求
        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树的结构,那么我们将这些关系一一对应到了责任链里面,来看看效果吧;

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

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

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

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

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

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

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

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

1,Activity的事件分发

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

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

这里面有三个方法,第一个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();

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

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

最终走的是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;

总结为流程图:

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()方法修改;

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

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

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

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

流程图:

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()方法里面实现的;

  • MotionEvent.ACTION_DOWN:在这个判断里面,调用了checkForLongClick(0, x, y)触发了长按监听的回调,也就是onLongClick()方法;

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

流程图:

4. 总结

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

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

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

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

看一下汇总的流程图:

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

参考&感谢

关于我

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