Android 的事件分发机制

导读

  • 移动开发知识体系总章(Java基础、Android、Flutter)
  • Android 的事件分发机制

Android 的事件分发机制

  • 事件分发的定义
  • 事件分发的源码分析
  • 通过自定义View分析事件分发的拦截逻辑
  • 疑问:Activity是怎么接收到事件的?
  • 疑问:ViewGroup是如何判断该把事件传递给那个child的?
  • 总结

事件分发的定义

在Android中的事件分发,就是将点击事件传递到某个具体的View的进行处理的整个过程。

举个不恰当的列子,你打开手机通讯录,找到你的女神,点击了她的电话,然后手机就会进入拨打界面的这一整个过程。你最终会通过手机接收到女神是否接了电话都有一个直观的反馈。
那么我们重点关注事件分发的顺序,和核心的方法:

  • 事件分发的顺序
  • 事件分发的核心方法
事件分发的顺序

在Android中UI通常是Activity+ViewGroup+View组合而成

类型 简介 说明
Activity 控制生命周期、处理事件 统筹视图的添加、显示;与Window、View交互
View 所有UI组件的父类 Button、TextView等控件都是View的子类
ViewGroup 含多个View的容器 也是View的子类;是所有布局的父类

事件分发则先到Activity,再到容器(ViewGroup),再精准到具体View。具体的View往往是实际处理者。

事件分发的核心方法

dispatchTouchEvent() 、onTouchEvent()、onInterceptTouchEvent()。

方法 功能 说明
dispatchTouchEvent() 分发点击事件 当点击事件能够传递给当前View时,调用该方法
onTouchEvent() 处理点击事件 在dispatchTouchEvent()内部调用,false表示不处理事件
onInterceptTouchEvent() 事件拦截 在ViewGroup的dispatchTouchEvent()内部调用,ViewGroup独有的方法

结合事件分发的顺序和核心方法,这里引用一张经典的流程图:

事件分发之张经典的流程图

总结一下,整体流程就是到了每个层面,拦不拦截,处不处理,如果不拦截不处理,最终回流到Activity行成闭环, 事件分发机制就是逐级逐层,去寻找一个有消费能力的。

举个不恰当的列子,
老板Activity某天醉酒时接了一个“xxxx”相关的项目,然后交给了项目总监ViewGroup985,
项目总监ViewGroup985一听这玩意儿没经验,就问了问下面最得力的项目经理ViewGroup996会不会搞
项目经理ViewGroup996听的一脸尴尬,就叫来了主管ViewGroup035
主管ViewGroup035说我还是问问小组长吧
小组长View251一脸懵逼,就去问了程序员View404
程序员View404给小组长View251说这事搞不了
小组长View251等逐级上报最后项目总监ViewGroup985跟老板说这货公司干不了
妥妥的的一个闭环,其中任何一个环境说能干的这事儿就执行下去了

现在我们对事件分发机制有了一个清晰的认识,那么源码具体是如何实现的呢?

事件分发的源码分析

老司机都知道在Android中的顶层View其实是抽象类Window,具体实现类是PhoneWindow,而PhoneWindow的根本是DecorView,DecorView则是ViewGroup的子类,点击查看关于Android的组成。故此,我们通过观察这些类的关键代码入手。
Activity类dispatchTouchEvent(MotionEvent ev)方法:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
  • 第一个if逻辑判断是ACTION_DOWN新的事件,也就是说,当用户进行按下动作的时候,事件就开始了,这里触发了onUserInteraction()方法,该方法返回void的空方法,并且有一些不常见的用处,这里就不展开了。
  • 第二个逻辑是去判断Window类下的 superDispatchTouchEvent(ev)方法,前面说到了Window类是抽象类,具体实现类是PhoneWindow类,那么我们在Android Studio中双击SHUFT键搜索该类,并找到superDispatchTouchEvent(ev)方法:
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
  • PhoneWindow类下的superDispatchTouchEvent(ev)方法中直接使用返回了变量mDecor的superDispatchTouchEvent(event)方法,变量mDecor即DecorView类,这里也不展开了,继续看核心方法superDispatchTouchEvent(event)
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
  • DecorView类下的superDispatchTouchEvent(MotionEvent event)方法中则直接调用了父类方法,前面说到了DecorView类其实是ViewGroup类的子类,所以我们需要继续追踪ViewGroup类dispatchTouchEvent(event)方法,由于该方法非常的复杂和庞大,这里先贴上一段伪代码
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if (onInterceptTouchEvent(ev)) {
        consume = onTouchEvent();
    }else {
        consume = child.dispatchTouchEvent(ev);
    }
    
    return consume;
}
  • 这段伪代码简单明了的讲清楚了ViewGroup类接收到事件后的逻辑,即先调用onInterceptTouchEvent()询问是否拦截,拦截则去走自己的onTouchEvent()方法,反之去走子类的方法。

现在贴上精简后的ViewGroup类onInterceptTouchEvent()源码:

       @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            ...
            intercepted = onInterceptTouchEvent(ev);
            ...
        }
        ...
        if (!canceled && !intercepted) {
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            ...
            }
            ...
        }
        ...
        return handled;
    }
  • 会进入第一个if逻辑去执行onInterceptTouchEvent(ev)方法获取是否拦截
  • 第二个if逻辑则是不拦截的情况下会进入去执行dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法(后面会说道拦截的情况下)
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                                  View child, int desiredPointerIdBits) {
        final boolean handled;
        ...
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        ...
        return handled;
    }
  • super.dispatchTouchEvent(event)表示执行自己父类的方法
  • child.dispatchTouchEvent(event)表示事件分发给了子类。比如一个我们这里点击的是一个AppCompatButton按钮,最终执行的是AppCompatButton的父类TextView类下的**onTouchEvent(MotionEvent event) **方法(其他父类并没有重写onTouchEvent()方法):
    @Override
    public boolean onTouchEvent(MotionEvent event) {}

以上便是对源码的事件分发机制的分析,使用的都是系统默认提供的ViewGroup(ConstraintLayout)、View(AppCompatButton),正常情况(无拦截事件)下的分发流程,确实符合前面我们分析的情况。下面我们通过简单的自定义View来继续分析。

通过自定义View分析事件分发的拦截逻辑

这里我们进行简单的自定义ViewGroup+自定义View来分析拦截和不处理(消费)事件。
简单的自定义ViewGroup:

public class MyLinearLayout extends LinearLayout {
    public MyLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean isTouch = true;
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            System.out.println("ViewGroup===onTouchEvent--"+isTouch);
        }
        return isTouch;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean isIntercept = false;
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            System.out.println("ViewGroup===onInterceptTouchEvent--" + isIntercept);
        }
        return isIntercept;
    }
}
  • 重构onTouchEvent(MotionEvent event)方法,使用isTouch变量控制是否处理事件
  • 重构了onInterceptTouchEvent(MotionEvent event)方法,使用isIntercept 变量控制是否拦截事件
  • 这里没有重构dispatchTouchEvent(MotionEvent event)方法,因为我们不需要去变更ViewGroup的分发机制

简单的自定义View:

public class MyAppCompatButton extends AppCompatButton {
    public MyAppCompatButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean isTouch=true;
        if(event.getAction()==MotionEvent.ACTION_DOWN){
            System.out.println("View===onTouchEvent--"+isTouch);
        }
        return isTouch;
    }
}

-重构了onTouchEvent(MotionEvent event)方法,使用isTouch变量控制是否处理事件
具体在xml中的使用:

    

        

            

                
            
        

        

            
        

        
    

在布局文件中应用了3组布局

  • 自定义ViewGroup+自定义ViewGroup+自定义View(模拟多重ViewGroup嵌套布局页面)
  • 自定义ViewGroup+自定义View(模拟普通常见布局页面)
  • 自定义自定义View(模拟极简布局页面)

最终的Acitvity页面

public class ActivityG extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_g);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            System.out.println("Activity===onTouchEvent");
            System.out.println("========================");
        }
        return super.onTouchEvent(event);
    }
}
  • ActivityG页面仅重写了onTouchEvent(MotionEvent event)方法,主要用于检验,都没有处理事件的时候是否闭环回到Acitvity。

下面是不同情况下的运行情况:
ViewGroup默认不拦截,View处理事件时,依次点击三个按钮的打印记录:

ViewGroup默认不拦截,View处理事件时
  • 自定义ViewGroup+自定义ViewGroup+自定义View时:2层ViewGroup都没有拦截,View处理事件处理
  • 自定义ViewGroup+自定义View:1层ViewGroup没有拦截,View处理事件处理
  • 自定义自定义View:View处理事件处理

ViewGroup进行拦截,ViewGroup自身响应事件,View不处理事件时,依次点击三个按钮的打印记录:

此时需要把MyLinearLayout类下的boolean isIntercept = false;变更为true,MyAppCompatButton类下的boolean isTouch=true;变更为false

ViewGroup进行拦截,ViewGroup自身响应事件,View不处理事件时

  • 自定义ViewGroup+自定义ViewGroup+自定义View时:第一层ViewGroup进行拦截,并执行自身事件处理。
  • 自定义ViewGroup+自定义View:同上
  • 自定义自定义View:View不处理事件处理,Activity响应事件。

ViewGroup进行拦截,ViewGroup自身不处理事件,依次点击三个按钮的打印记录:

此时需要把MyLinearLayout类下的boolean isTouch=true;变更为false

ViewGroup进行拦截,ViewGroup自身不处理事件

  • 由于一层两层ViewGroup都不会进行事件分发,这里就只点了按钮1
  • 自定义ViewGroup:事件拦截,自身不进行处理,Activity响应事件

到这里,印证了前面的介绍以及经典图上所绘制的流程即事件会按照顺序一层一层进行分发,每一层都会都会判断是否拦截(ViewGroup),如果不拦截则继续往下,反之则查看当前ViewGroup是否消费事件,若最终找不到有能力消费的,则返回到Activity形成闭环。

当然,截止到目前,还有一点没有说到,即在ViewGroup拦截事件之后,后续是如何处理的呢?下面来看看关键源码:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean handled = false;
        ...
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            ...
            intercepted = onInterceptTouchEvent(ev);
            ...
        }
        ...
         if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
        }
        ...
        return handled;
    }
  • 还是ViewGroup类下的dispatchTouchEvent(MotionEvent ev)方法
  • 第一个if逻辑中会执行onInterceptTouchEvent(ev)查询是否拦截
  • 第二个if逻辑中执行的是不拦截的逻辑,同样会进入dispatchTransformedTouchEvent()方法:
  private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                                  View child, int desiredPointerIdBits) {
        final boolean handled;
        ...
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        ...
        return handled;
    }
  • 前面已知事件不拦截时,最终会执行child.dispatchTouchEvent(event)方法。
  • 此时,事件进行了拦截,则执行super.dispatchTouchEvent(event)方法,由于我们自定义的MyLinearLayout类重写了onTouchEvent()方法:
public class MyLinearLayout extends LinearLayout {
    ...
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean isTouch = true;
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            System.out.println("ViewGroup===onTouchEvent--"+isTouch);
        }
        return isTouch;
    }
    ...
}
  • 此时的isTouch = true,则MyLinearLayout类onTouchEvent()方法消费事件,事件分发到此结束。
  • isTouch = false 呢,自然是继续往上最终执行到我们的ActivityG类的**onTouchEvent(MotionEvent event) **方法,这里就不贴代码了

这里再提一提,如果想要更好的理解源码是如何进行事件分发的,最好通过DEBUG跟踪事件分的逻辑,

  • 使用DUBUG模式启动项目
  • 在父类Activity类下的dispatchTouchEvent()方法内打上断点
  • 在DecorView类下dispatchTouchEvent()方法内打上断点
  • 在ViewGroup类下的dispatchTouchEvent()、dispatchTransformedTouchEvent()方法内打上断点

这里发现android-29和android-28、android-26略有差异,但不影响本篇内容事件分发机制的,看到文章的朋友可以试试。
另外,在ViewGroup类的执行dispatchTouchEvent()事件分发,对child进行判断的时候,可以看到系统源码中并不是简单的Activity-PhoneWindow(Window)-DecorView-我们的布局Layout-具体的View,而是:

  DecorView@ebcfe75[ActivityG]
  android.widget.LinearLayout{93999f3 V.E...... ........ 0,0-1440,2712}
  android.widget.FrameLayout{17cada3 V.E...... ........ 0,84-1440,2712}
  androidx.appcompat.widget.ActionBarOverlayLayout{ea7fb39 V.E...... ........ 0,0-1440,2628 #7f070056 app:id/decor_content_parent}
  androidx.appcompat.widget.ContentFrameLayout{32e548 V.E...... ........ 0,196-1440,2628 #1020002 android:id/content}
  androidx.constraintlayout.widget.ConstraintLayout{d004903 V.E...... ........ 0,0-1440,2432}
  android.widget.LinearLayout{11a8c3f V.E...... ........ 0,0-1440,2432}
  com.futurenavi.demo1.views.MyLinearLayout{9d95de9 V.E...... ........ 0,168-1440,336}
...
  • 可以确定DecorView是顶层View(Root View)
  • ConstraintLayout为我给ActivityG类的xml中设定的顶层布局
  • LinearLayout为我给ActivityG类的xml中设定的第二次布局
  • MyLinearLayout是前面自定义的ViewGroup
  • 另外如果没有View消费事件,则会倒序再执行一次,完成闭环
    有兴趣的朋友不妨试试

待续

  • 疑问:Activity是怎么接收到事件的?
  • 疑问:ViewGroup是如何判断该把事件传递给那个child的?

总结

  • 我们知道了

  • 第一个if逻辑判断是ACTION_DOWN新的事件,也就是说,当用户进行按下动作的时候,事件就开始了,这里触发了onUserInteraction()方法,可以看到该方法返回void的空方法,也就是说,如果想在Activity接收到按下事件做些什么的话,可以直接重写onUserInteraction()方法处理,这里就不展开了。

  • 第二个逻辑触发getWindow().superDispatchTouchEvent(ev)方法,那么先看看getWindow()方法:

       @UnsupportedAppUsage
        private Window mWindow;
        public Window getWindow() {
            return mWindow;
        }
    

    getWindow()返回了一个Window类,继续查看Window类:

    /**
       * Abstract base class for a top-level window look and behavior policy.  An
       * instance of this class should be used as the top-level view added to the
       *  window manager. It provides standard UI policies such as a background, title
       * area, default key processing, etc.
       *
       * 

    The only existing implementation of this abstract class is * android.view.PhoneWindow, which you should instantiate when needing a * Window. */ public abstract class Window { ... public void setCallback(Callback callback) { mCallback = callback; } ... }

    发现Window是抽象类,注释里大意说用于顶层窗口外观和行为策略的抽象基类,此类的实例应用作添加到窗口管理器。 它提供标准的UI策略,例如背景,标题区域,默认密钥处理等,此抽象类的唯一现有实现是android.view.PhoneWindow,需要使用Window时应实例化
    这里先关注三个点:

    • 用于顶层窗口外观和行为策略。
    • android.view.PhoneWindow。
    • 需要使用Window时应实例化。

    继续去看看PhoneWindow,在Android Studio中双击Shift搜索PhoneWindow类,并查看superDispatchTouchEvent()方法:

         ...
        private DecorView mDecor;
        @Override
        public boolean superDispatchTouchEvent(MotionEvent event) {
            return mDecor.superDispatchTouchEvent(event);
        }
        ...
    

    这里就牵扯出了DecorView对象:

    public class DecorView extends FrameLayout implements       RootViewSurfaceTaker, WindowCallbacks {
    ...
      public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
      }
        @Override
      public boolean dispatchTouchEvent(MotionEvent ev) {
            final Window.Callback cb = mWindow.getCallback();
            return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                    ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
      }
    ...
    }
    

    可以看到DecorView是FrameLayout的子类,是一个ViewGroup。
    在dispatchTouchEvent() 方法中用到了Window.Callback():

      public abstract class Window {
      ...
      public void setCallback(Callback callback) {
            mCallback = callback;
        }
      public final Callback getCallback() {
            return mCallback;
        }
      public interface Callback {...}
      ...
    }
    

    由于这个callback是在Activity中实现的:

    public class Activity extends ContextThemeWrapper
          implements LayoutInflater.Factory2,
          Window.Callback, ...{
       @UnsupportedAppUsage
        final void attach(Context context, ActivityThread aThread,...,Window window, ...) {
              ...
              mWindow.setCallback(this);
              ...
        }
    }
    

    所以前面DecorView的dispatchTouchEvent(MotionEvent ev) 中的判断是传递给给Activity还是直接传递给父类(VIewGroup)的结果是cb.dispatchTouchEvent(),即将事件传递给了Activity,反之则会走super.dispatchTouchEvent(ev),即父类VIewGroup的dispatchTouchEvent()的方法,这里后面在来说。


  • 最后执行onTouchEvent(ev),即前面说到的onTouchEvent()在dispatchTouchEvent()中执行

结合前面的三个关注点:每个Activity包含一个Window,而Window其实是一个PhoneWindow,每个PhoneWindow则包含一个DecorView,而每个DecorVIew都是一个Framelayout(也就是一个ViewGroup)。
也就是说,在开发者层面,顶层窗口是Window,顶层布局是DecorView(FrameLayout )

在Activity中的onCreate()方法第一时间会去执行了setContentView方法,即设置页面UI(布局),前面说到ViewGroup是Android中的布局的父类:

Activity的组成结构 View类及其子类
Activity的组成结构
View类及其子类(ViewGroup及其子类(布局类))

场景/疑问/好奇

讲讲 Android 的事件分发机制
查看过源码中的事件拦截方法吗?或者说在进行事件分发的时候如何让正常的分发方式进行拦截?
在一个列表中,同时对父 View 和子 View 设置点击方法,优先响应哪个?为什么会这样?

上面的问题无论是面试还是平时开发都是很常见/常用的,各位看官心里是否或清晰或模糊或好奇呢?

在Android中,通常以一个Activity作为一个页面,在Activity的组成情况中我们知道每个Activity包含一个Window,而Window其实是一个PhoneWindow,每个PhoneWindow则包含一个DecorView,而每个DecorVIew都是一个Framelayout。

而事件,总是有一个触发点的,对于手机的事件通常从用户按下开始的,事件分发对应着三个重要方法:

  • dispatchTouchEvent()
  • onTouchEvent()
  • onInterceptTouchEvent()
    (一下让我想起了当年在宿舍一遍遍背onInterceptTouchEvent的样子,捂脸)

上面三个方法,从字面就非常好理解,分别是事件分发、事件处理、事件拦截,在Android中无论AppCompatActivity、FragmentActivity都是Activity的子类,

举个不恰当的列子,Activity接到了一个“水幕电影”的项目,然后扭头就转包给了ViewGroup,这个ViewGroup是没有能力搞这东西的,但是又想赚钱就接了,然后又转包给了另外一个ViewGroup2,正所谓人以类聚,这ViewGroup2也是不会搞想赚钱的主,最后这个ViewGroup2把这活给了一个专业对口的View,这个View漂亮的完成了项目,引起了一片好评。

总结一下,Activity是第一时间接收到点击事件的,然后会分发给顶层ViewGroup,顶层ViewGroup会去检查是否拦截,不拦截则继续往下传递,若UI复杂则会重复该步骤,直到事件分发到具体View进行事件处理。看起来似乎也是挺简单明的,那么,通过源码来看看,Android具体在这如何进行三大环节中进行事件分发的。

DecorView@ebcfe75[ActivityG]
android.widget.LinearLayout{93999f3 V.E...... ........ 0,0-1440,2712}
android.widget.FrameLayout{17cada3 V.E...... ........ 0,84-1440,2712}
androidx.appcompat.widget.ActionBarOverlayLayout{ea7fb39 V.E...... ........ 0,0-1440,2628 #7f070056 app:id/decor_content_parent}
androidx.appcompat.widget.ContentFrameLayout{32e548 V.E...... ........ 0,196-1440,2628 #1020002 android:id/content}
androidx.constraintlayout.widget.ConstraintLayout{d004903 V.E...... ........ 0,0-1440,2432}
android.widget.LinearLayout{11a8c3f V.E...... ........ 0,0-1440,2432}
com.futurenavi.demo1.views.MyLinearLayout{9d95de9 V.E...... ........ 0,168-1440,336}

你可能感兴趣的:(Android 的事件分发机制)