Android事件分发机制浅析

1,203 阅读8分钟

Android事件分发机制是初级程序员应该熟悉掌握的一个环节,只有当你了解分发机制,你才能更游刃有余地去解决实际生产中滑动冲突、自定义控件事件如何分发等常见问题。接下来自己会抛砖引玉,简单地分析一下这个机制,希望能对没接触此概念或者印象不深的开发者有所帮助。

责任链模式及应用

1.为何讲到责任链模式?

为什么要讲到责任链模式呢?当然是因为Android的事件分发机制其实是有责任链模式的影子的。我们先看一下关于责任链模式的定义:

责任链模式是一种对象行为模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。

当然定义是从百度上扒下来的,按照我自己的理解,责任链模式有点像是自顶向下的管理机构。当某个职位遇到属于自己职能的工作就去完成,不属于自己的工作或者处理不了的工作就向下(上)丢,交给能够处理的人去处理。当然具体怎么定义并不固定,大致是这个思路。

2.责任链模式的简单应用

下面来整个具体的活,我们自顶向下是Boss,Manager,Staff的模式,各个职位都有自己的金额处理范围,遇到不属于自己范围的活就抛出,下面是一个简单的UML图:

简单解释一下UML图,建立一个抽象处理者,通过内部方法进行对象链的建立,定义一个抽象处理方法,让接下来的子类自定义具体实现,因此最后的Code:

public abstract class Handler {

    //用来创建对象链
    protected Handler mHandler;
    public Handler getHandler(){
        return mHandler;
    }
    public void setHandler(Handler handler){
        this.mHandler = handler;
    }
    public abstract void doSomething(int money);

    //静态内部类,偷懒
    public static class Boss extends Handler{

        @Override
        public void doSomething(int money) {
            if (money<1000){
                System.out.println("金额不足1000,Boss不理会,向下派送。");
                getHandler().doSomething(money);
            }else {
                System.out.println("Boss批准。");
            }
        }
    }

    public static class Manager extends Handler{

        @Override
        public void doSomething(int money) {
            if (money<100){
                System.out.println("金额不足100,经理不理会,向下派送。");
                getHandler().doSomething(money);
            }else {
                System.out.println("经理批准。");
            }
        }
    }

    public static class Staff extends Handler{

        @Override
        public void doSomething(int money) {
            if (money<0){
                System.out.println("金额不足0,逻辑得重新写。");
            }else {
                System.out.println("员工批准。");
            }
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Handler.Boss boss = new Handler.Boss();
        Handler.Manager manager = new Handler.Manager();
        Handler.Staff staff = new Handler.Staff();
        boss.setHandler(manager);
        manager.setHandler(staff);

        int money = 50;
        System.out.println("申请金额" + money);
        boss.doSomething(money);

    }
}
申请金额50
金额不足1000,Boss不理会,向下派送。
金额不足100,经理不理会,向下派送。
员工批准。

最后看到每个人都能处理到属于自己职能的工作,而使用责任链模式不仅能够让类各司其职,更重要的是它使整个构架具有了动态性。从上面的代码可以看到,当你想添加一个中间职位的时候,可以很方便的创建对应类,只需要继承抽象类,进行方法的具体定义即可。Android的事件分发机制正是采用类似的架构,因此整个事件流程和我们自定义View的时候才能够处理事件的分发,接着我们把重心放在Android的事件分发。

Android的事件分发

1.分发的事件是什么?

之前的例子里可以看到,分发的事件其实是金额,各个职位类根据金额的不同,把事件“发送到”对应的类去处理,而Android分发的事件是什么呢?

MotionEvent

MotionEvent又是什么呢?其实MotionEvent是系统架构对我们的输入事件创建的一个封装集合,里面含有单点触控、多点触控、鼠标事件等等。更通俗易懂地解释,我们使用Android设备时会进行触摸,滑动等的输入事件,系统构架把这些真实操作,转换成一个个具体的MotionEvent,比如手指刚刚接触屏幕时,硬件感知后通过一系列转换会变为一个名为ACTION_DOWN的MotionEvent,这样我们可以在代码里接收这个事件,并实现相应逻辑。因此Android分发的事件本质上是用户具体的行为操作。

2.分发的层次

了解完分发的事件是什么后,既然之前提到了Android的事件分发有责任链模式的影子,那么大致的责任链是什么样呢?下面是网上盗的层次图(由于大意比较简单,侵权乖乖删除):

根据层次关系可以看到链大致是Activity-> Window-> DecorView-> ViewGroup-> View,抛开实现细节不谈,Android的事件分发跟之前的例子类似,是自顶向下地进行事件的分发,具体的控件根据逻辑去实现对应的功能,最后实现完整的事件分发机制。到这里,我们明白事件是什么,抽象的责任链是什么样的,最后只需要把重心放到具体的层去看细节。

3.分发的细节

根据上图,责任链的顶层似乎是Activity。但是事件如何产生?责任链怎么组装?事件又如何传递到Activiity?这三个问题对作者来说有点超纲,在这里安利一位大佬,却把清梅嗅,相信大佬们或多或少有点印象,每次偶然瞧见这位大佬的文章,总让我觉得道阻且长,下面是文章的地址:

juejin.cn/post/684490…

那么前提条件都具备的情况下,各个层级如何对事件进行处理和分发的?接下来我们会对ViewGroup层级进行详细地分析,如果弄明白ViewGroup的细节,其它层级也可见一斑。在开始之前,我们先列出三个重要的函数:

public boolean dispatchTouchEvent(MotionEvent ev) //进行事件的分发,只要事件能传递到当前层级,一定会调用它,返回的结果受当前View和下级View是否消耗事件影响
public boolean onInterceptTouchEvent(MotionEvent ev) //根据返回值表示是否拦截事件,如果当前View拦截事件,后续同事件系列不会再调用
public boolean onTouchEvent(MotionEvent ev) //在dispatchTouchEvent方法中被调用,返回值表示是否消耗当前事件,如果不消耗,后续同事件序列不再被接收

如果一上来就贴源码肯定很不自然,我们慢慢来捋一捋:

  1. 事件传递到ViewGroup,之前说过只要能到达当前层级,一定会调用dispatchTouchEvent方法。
  2. 既然事件过来了,我们可以选择拦截它或者不拦截它,如果不拦截它,肯定是false,事件就会传递到下一层级;如果我们选择拦截事件,onInterceptTouchEvent返回的是true,那么就会接着触发onTouchEvent方法。
  3. 这时候我们可以选择是否消耗这个事件,如果不消耗,肯定是false,那么我们整体的结果是不消耗事件,整个的dispatchTouchEvent方法返回false,回传到上一层级;如果我们选择消耗这个事件,onTouchEvent返回true,整体的结果是返回true,依然回传处理结果。

整个过程可以简单理解为一个自上而下的责任链,一层一层地去判断是否消耗事件,最后在底层又一层一层地返回事件处理结果,经典递归,伪代码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean result = false;
    if(onInterceptTouchEvent()){
        result = onTouchEvent();
    }else {
        result = child.dispatchTouchEvent();
    }
    return result;
}

伪代码将上述流程描述得十分清晰,只要你理解了事件分发的思想,具体实现又是头疼的啃源码环节(源码的规范和优化思想有很必要学习,进阶必备):

public boolean dispatchTouchEvent(MotionEvent ev) {
	boolean handled = false;
	...
    final boolean intercepted;
    // 省略N步,通过onInterceptTouchEvent方法确定是否拦截,ViewGroup通常返回false
    intercepted = onInterceptTouchEvent(ev);
    ...
        
	// mFirstTouchTarget用来记录下层的View,为空则super.dispatchTouchEvent();
   if (mFirstTouchTarget == null) {
       handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
   }else{
       ...
       // mFirstTouchTarget不为空,事件分发下去
       handled = child.dispatchTouchEvent(event);
   }
   ...
    return handled;
}

以上仅仅是源码的一部分缩略描述,可以看到跟之前的伪代码的思想相差不大,具体的实现思路和实现优化,读者可以自行阅读源码仔细体会,其他层级的具体实现跟ViewGroup虽然有差异,但是整体思想是一致的,如果想了解的朋友可以参考其他分析文章,至此,Android的事件分发的浅析也告一段落了。

滑动冲突的由来与解决方案

1.何为滑动冲突?

其实滑动冲突体现在滑动两个字,能够进行滑动的控件产生了冲突,于是带来了跟预期不一样的结果。我们常用的滑动方式无非横向或者纵向,当控件嵌套时,我们期望它们能够正确的滑动,但是控件本身并不知道自己是否应该响应此滑动,如以下几种情况:

反向冲突是嵌套的两个滑动控件,滑动方式不同,滑动时可能出现”混乱“的场景,如两个控件都在滑动,这很正常,毕竟只要满足滑动条件,它们就会响应;而同向冲突类似,嵌套的控件滑动方式相同,控件本身并不知道自己什么时候应该滑动,这需要我们通过需求逻辑去解决,而N向冲突则是两种情况的混合,拆开即可,这里不多解释了。

2.依据事件分发机制进行解决

从上面来看,滑动冲突的产生似乎是因为不同滑动控件嵌套时,它们不知道自己应该做什么。结合之前提到的事件分发机制,我们有两种解决办法:

  1. 在对应层级的控件拦截对应滑动事件,并进行消耗。(外部拦截法)
  2. 顶层控件不拦截事件,事件交给子层级处理,然后子层级抛出不需要处理的事件给上层,就能够解决滑动冲突。(内部拦截法)

这里有一个例子:Scrollview嵌套RecyclerVIew 实现顶部导航栏和列表的组合

如果Google官方没有在控件内自适应滑动冲突,滑动的体验肯定不好的,比如会出现滑动时,导航栏滑动的同时列表也在滑动,或者从幅度上来看明明是想往下滑动,导航栏却在左右滑动。

分析问题:由于这个例子比较简单,根据我们的需求,导航栏是只能左右滑动的,因此上下滑动的响应都应该交给

Scrollview,换句话说就是顶层的Scrollview拦截并处理所有上下滑动事件即可,代码如下:

private float mLastX = 0f;
private float mLastY = 0f;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    int action = ev.getAction();
    float x = ev.getX();
    float y = ev.getY();
    boolean intercept = false;
    switch (action){
        case MotionEvent.ACTION_DOWN:
        case MotionEvent.ACTION_UP:
            intercept = false;
            break;
        case MotionEvent.ACTION_MOVE:
            //通过位移增量判断属于纵向滑动还是横向滑动
            float mDeltaX = x - mLastX;
            float mDeltaY = y - mLastY;
            intercept = Math.abs(mDeltaX) < Math.abs(mDeltaY);
            break;
    }
    mLastX = x;
    mLastY = y;
    return  intercept;
    //return super.onInterceptTouchEvent(ev);
}

这是通过在顶层控件拦截事件解决冲突的方式,之前提到还能采用顶层控件不拦截,交给子层控件去消耗事件,抛出不需要的事件给顶层的方式。不过内部拦截法需要使用子层控件使用requestDisallowInterceptTouchEvent方法来设置顶层控件是否拦截,解决问题的思路很相似,感兴趣的朋友可以去看一下,这里不多介绍。

最后同向以及混合冲突就不一一解释了,只要你弄清事件分发的本质,以及如何通过外部拦截、内部拦截去解决,这类型的问题都是照葫芦画瓢,相信大家都已经很熟悉了。

tip:Scrollview + RecyclerVIew只是小例子,实现此功能千万不能用!目前有NestedScrollView等解决方案,这一块作者不是很了解,慎入坑!