欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android 事件分发之追本溯源

程序员文章站 2022-05-14 17:38:33
...

前言

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

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

1.1,Android手机

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

1.2,脑洞一

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

Android 事件分发之追本溯源

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

1.3,脑洞二

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

Android 事件分发之追本溯源

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

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

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

Android 事件分发之追本溯源

2.2,事件

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

MotionEvent的类型:

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

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

3.1,设计模式

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

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

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

3.2,责任链模式

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

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

Android 事件分发之追本溯源

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

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

Android 事件分发之追本溯源

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

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

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

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

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

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

Android 事件分发之追本溯源

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

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

Android 事件分发之追本溯源

1,Activity的事件分发

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

Android 事件分发之追本溯源
step2:进入Activity的dispatchTouchEvent方法里面,看一下做了啥;

Android 事件分发之追本溯源

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

Android 事件分发之追本溯源

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

Android 事件分发之追本溯源

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

Android 事件分发之追本溯源

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

总结为流程图:

Android 事件分发之追本溯源

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

Android 事件分发之追本溯源

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

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

Android 事件分发之追本溯源

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

Android 事件分发之追本溯源

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

流程图:

Android 事件分发之追本溯源

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

Android 事件分发之追本溯源

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

Android 事件分发之追本溯源

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

Android 事件分发之追本溯源

流程图:

Android 事件分发之追本溯源

4. 总结

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

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

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

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

看一下汇总的流程图:

Android 事件分发之追本溯源

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

参考&感谢

关于我

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

相关标签: Android 源码