Android事件分发机制

1. 入门了解

1.1 事件分发会涉及哪些对象?

  • 有哪些对象
    从用户点击屏幕的时候,会涉及到Activity,Window,触摸的ViewGroup或者View,将这些对象关联起来将产生分发触摸事件dispatchTouchEvent,而这个事件会传递MotionEvent对象
  • MotionEvent是什么?
    MotionEvent是个将当前触摸的时间、坐标、具体动作封装的一个对象
方法 简介
getAction() 与指定View的左边界一致
getDownTime() 返回当屏幕刚被按下时的时间(毫秒),按下后移动此时间不变
getEventTime() 返回MotionEvent所在的事件被激发的时间(毫秒)
getX()、getY() 获得触摸点在当前 View 的 X ,Y轴坐标。
getRawX()、getRawY() 获得触摸点在整个屏幕的 X ,Y轴坐标。

关于 getgetRaw 的区别可以参考这一篇文章 安卓自定义View基础-坐标系

  • 关于getAction有以下类型
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
                // 手指按下(所有事件的开始) 0
                break;
            case MotionEvent.ACTION_MOVE:
                // 手指移动(会多次触发) 2
                break;
            case MotionEvent.ACTION_UP:
                // 手指抬起(与DOWN对应的结束) 1
                break;
            case MotionEvent.ACTION_CANCEL:
                // 事件被拦截 (非人为原因) 3
                break;
            case MotionEvent.ACTION_OUTSIDE:
                // 超出区域 4
                break;
    }
    return super.onTouchEvent(event);

  • 从手指触摸屏幕到离开屏幕时会发生以下系列事件


    系列事件

1.2 事件分发在哪些对象之间传递?

答:Activity、ViewGroup、View


Activity、ViewGroup、View

1.3 事件分发对象的顺序?

答:当一个简单的单击触发后,Activity -> ViewGroup -> View

1.4 事件分发过程由哪些方法协作完成?

答:dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()
这些方法会在下面详细的深入解答什么时候触发,之间是如何协作完成的。

2 从源码深入了解

从上面的文章我们得知顺序是Activity -> ViewGroup -> View,所以是有事件从第一个传到最后一个的

  • 所以我们拆分了解Activity,ViewGroup,View三个分别的事件分发机制,源码取自于SDK26版

2.1 Activity的事件分发机制

2.1.1 源码解析

Activity的源码:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 开始事件都是Dwon,一般第一次都会进入到onUserInteraction
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        // 若Window返回true,则会告诉Activity也返回True。True在所有Touch代表着终止,不再继续往下一个事件传递了
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

onUserInteraction在源码里面是个空方法,这个暂时不讲解,是提供给开发者重写该方法的

public void onUserInteraction() {
    }

getWindow().superDispatchTouchEvent(ev)的源码就比较有意思了

  • Window类是抽象类,其唯一实现类 = PhoneWindow类;即此处的Window类对象 = PhoneWindow类对象
  • 找出PhoneWindow的源码,快捷键ctrl+n,选择All,然后右侧勾选Include
  • 了解PhoneWindow的superDispatchTouchEvent源码,以下是该源码
PhoneWindow的源码:
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

mDecor又是个什么东西呢?

  • DecorView类是PhoneWindow类的一个内部类
  • DecorView为整个Window界面的最顶层View。
  • DecorView只有一个子元素为LinearLayout。代表整个Window界面,包含通知栏,标题栏,内容显示栏三块区域。
  • LinearLayout里有两个FrameLayout子元素。 一个元素是标题栏显示界面,一个元素是内容栏显示界面。就是我们平时setContentView()方法载入的布局界面,加入其中。
  • DecorView继承于FrameLayout,FrameLayout即是ViewGroup,所执行的dispatchTouchEvent即是跟ViewGroup一样的触摸分发处理
DecorView的源码:
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

2.1.2 Activity事件分发总结

先看如下图的Activity界面结构


Activity界面结构

然后再看总结后的流程图,假设该流程从一个简单的点击开始


事件分发流程图

2.1.2 Demo实例操作

具体在Demo网址中,查看Log打印,并且在MyConstraintLayout类的方法中dispatchTouchEvent或者onTouchEvent修改true 或者 false观察日志了解

2.2 ViewGroup事件的分发机制

2.2.1 源码解析

从上面Activity事件分发机制可知,ViewGroup事件分发机制从dispatchTouchEvent()开始
由于API30的源码过长,这边有个文章详细解说了源码:https://www.jianshu.com/p/e57372c0b032
简单概述如下:

  • ViewGroup每次事件分发时,调用本身方法onInterceptTouchEvent()询问是否拦截事件
  • 遍历了当前ViewGroup下的所有子View
  • 若当前点击的是可点击的View类似button等,则拦截触发button的onTouchEvent,否则触发Activity和它下面的ViewGroup的onTouchEvent

流程图如下(在跑ViewGroup分发事件之前会先跑Activity的分发事件):


流程图

2.2.2 Demo实例操作

具体在Demo网址中,查看Log打印,并且在MyConstraintLayout类的方法中onInterceptTouchEvent修改true 或者 false观察日志了解

2.3 View事件的分发机制

2.3.1 源码解析

API30的源码过长,取一部分核心的源码讲解,首先我们看dispatchTouchEvent源码

public boolean dispatchTouchEvent(MotionEvent event) {
  /```/
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
  /```/
}

说明:只有以下4个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent()

    1. li != null
    1. mOnTouchListener != null
    1. (mViewFlags & ENABLED_MASK) == ENABLED
    1. mOnTouchListener.onTouch(this, event)

下面对这4个条件逐个分析
li != null
li即是ListenerInfo,ListenerInfo是封装了所有事件,所以只要赋值任一事件,这个都不可能会为null
mOnTouchListener != null
mOnTouchListener变量在View.setOnTouchListener()方法里赋值,即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)
(mViewFlags & ENABLED_MASK) == ENABLED
该条件是判断当前点击的控件是否enable,由于很多View默认enable,故该条件恒定为true
mOnTouchListener.onTouch(this, event)
即 回调控件注册Touch事件时的onTouch();需手动复写设置,具体如下(以按钮Button为例)

findViewById(R.id.myButton).setOnTouchListener((v, event) -> true);
  • 若在setOnTouchListener返回true,就会满足以上4个条件,并且返回了true,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束,不会执行onTouchEvent(event)
  • 若不赋值该事件或者返回false,就不满足,照常默认运行,执行onTouchEvent(event)

接着下来我们看onTouchEvent(event)的源码

    public boolean onTouchEvent(MotionEvent event) {
        /``````/

        // clickable代表该控件是否可点击,可点击就进入下面条件判断
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                // 1. 当前的事件 = 抬起View
                case MotionEvent.ACTION_UP:
                            // 经过种种判断,此处省略
                            ........
                            if (!focusTaken) {
                                // 执行performClick()
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                    break;
                // 2. 当前的事件 = 按下View
                case MotionEvent.ACTION_DOWN:
                    // 经过种种判断,此处省略
                    break;
                // 3. 当前的事件 = 结束事件(非人为原因)
                case MotionEvent.ACTION_CANCEL:
                    // 经过种种判断,此处省略
                    break;
                // 4. 当前的事件 = 滑动View
                case MotionEvent.ACTION_MOVE:
                    // 经过种种判断,此处省略
                    break;
            }
            // 若该控件可点击,就一定返回true
            return true;
        }
        // 若该控件不可点击,就一定返回false
        return false;
        /``````/
    }

2.3.2 View事件分发总结

image.png

2.4 总结

总结

那么到了这里,就是Activity、ViewGroup、View之间的事件分发机制了

2.5 Demo示例讲解

那么接下来,我将通过Demo形式模仿各种场景来更加的详细解答
在讲解前,先跟大家总结下这几个方法的作用

方法 作用
dispatchTouchEvent 如果返回false就调用本身的onTouchEvent,否则反之
onTouchEvent 如果返回true表示消费该事件,返回false表示不消费该事件,如果还对消费模糊,在下面DEMO具体介绍会详细解说
onInterceptTouchEvent 只有ViewGroup有,返回false就调用子的触摸分发事件,否则反之
setOnTouchListener 设置本身的触屏事件,最开始的源头,如果返回true,那么子View就不会收到任何触摸分发事件

2.5.1 布置Demo布局

先布置一个这样的布局


布局一览

该布局下面的控件ConstraintLayout,Button,TextView全部自定义继承,给dispatchTouchEventonTouchEventonInterceptTouchEvent等相关方法重写添加Log

public class MyConstraintLayout extends ConstraintLayout {

    private final static String TAG = "OnTouch MyConstraintLayout";

    public MyConstraintLayout(@NonNull Context context) {
        super(context);
    }

    public MyConstraintLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MyConstraintLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(TAG,"dispatchTouchEvent" + ev.getAction());
        boolean isDispatch = super.dispatchTouchEvent(ev);
//        Log.d(TAG," super.dispatchTouchEvent(ev):" + isDispatch);
        return isDispatch;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG,"onTouchEvent ACTION_DOWN");
                // 手指按下(所有事件的开始) 0
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG,"onTouchEvent ACTION_MOVE");
                // 手指移动(会多次触发) 2
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG,"onTouchEvent ACTION_UP");
                // 手指抬起(与DOWN对应的结束) 1
                break;
            case MotionEvent.ACTION_CANCEL:
                Log.d(TAG,"onTouchEvent ACTION_CANCEL");
                // 事件被拦截 (非人为原因) 3
                break;
            case MotionEvent.ACTION_OUTSIDE:
                Log.d(TAG,"onTouchEvent ACTION_OUTSIDE");
                // 超出区域 4
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG,"onInterceptTouchEvent" + ev.getAction());
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean performClick() {
        Log.d(TAG,"performClick");
        return super.performClick();
    }
}

2.5.2 源码下载

https://github.com/zhongjhATC/TouchTest

2.5.3 演示点击ViewGroup或者TextView的代码

注意:配合Demo源码来深入理解效果更佳
根据目前布局图可知,ConstraintLayout是包含另外一个ConstraintLayout的,如下图


布局一览

根据上面的总结,赋值button的setOnTouchListener事件,返回false是默认的

findViewById(R.id.myButton).setOnTouchListener((v, event) -> false);

点击MyConstraintLatyouChild布局后,打印Log如下


点击ViewGroup的Log

可以看到,先MainActivity进行dispatchTouchEvent分发,然后到MyConstraintLayout进行dispatchTouchEvent和onInterceptTouchEvent分发,再到MyConstraintLatyouChild进行dispatchTouchEvent和onInterceptTouchEvent分发,最后依次进行onTouchEvent触发,因为离开了屏幕,所以又触发了MainActivity进行dispatchTouchEvent一次。那为什么只触发一次MainActivity的ACTION_UP一次呢,因为第一轮ACTION_DWON下去以后,别的都没有消费onTouchEvent,就会由最外面的一层来消费后续的onTouchEvent了,这边我用图表来总结这个过程,相信大家会更好理解


默认流程,ACTION_DOWN都没被消费
  • 都不拦截,ACTION_DOWN会依次向下传递
  • 都不消费,onTouchEvent会依次向上传递
  • 后续的ACTION_UP和ACTION_MOVE都不会再被传递了

2.5.4 演示点击button的代码

如果点击的是TextView是跟上面的情况一样的,那么如果点击的是button之类的呢?
同样步骤,点击按钮,打印Log如下:


点击按钮的Log

可以看到,onTouchEvent只有Button打印了,因为它消费之后不会传递到上一层了,并且后续的dispatchTouchEvent也开始从最外层开始传递给Button了
用流程图表示如图:


点击button的流程图

请注意,因为button在onTouchEvent消费了事件,所以上一层的MyConstraintLayout的dispatchTouchEvent返回true,不会触发本身的onTouchEvent
那如果按下按钮不松开一直移动呢,那么ACTION_MOVE也会一直从外层传递到Button,是一样的流程

2.5.5 演示ViewGroup拦截的代码

让我们做个拦截处理,在MyConstraintLayout的方法onInterceptTouchEvent添加return true;代码如下:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG, "onInterceptTouchEvent " + Utils.getAction(ev.getAction()));
        return true;
    }

点击button,打印如下


MyConstraintLayout拦截后的Log

可以看到MyConstraintLayout拦截后,就再也不触发button的有关事件,同时因为没人消费onTouchEvent,又返回到最顶层MainActivity触发ACTION_UP了
流程图如下:


被拦截的流程图

2.5.4 演示ViewGroup只拦截ACTION_MOVE的代码

那么如果只拦截一部分动作呢,MyConstraintLayout的方法onInterceptTouchEvent改成如下:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG, "onInterceptTouchEvent " + Utils.getAction(ev.getAction()));
        if (ev.getAction() == MotionEvent.ACTION_MOVE)
            return true;
        return super.onInterceptTouchEvent(ev);
    }

打印Log如下:


拦截ACTION_MOVE的Log

这次操作方式是按下button后,不放开手然后移动,会发现,执行了button的ACTION_CANCEL,那是因为并没有拦截button的ACTION_DEWN,接收后,因为拦截了ACTION_MOVE,所以执行了ACTION_CANCEL

2.6 总结

那么回过头来总结之前的表格,朋友们明白了吗?!

方法 作用
dispatchTouchEvent 如果返回false就调用本身的onTouchEvent,否则反之
onTouchEvent 如果返回true表示消费该事件,返回false表示不消费该事件,如果还对消费模糊,在下面DEMO具体介绍会详细解说
onInterceptTouchEvent 只有ViewGroup有,返回false就调用子的触摸分发事件,否则反之
setOnTouchListener 设置本身的触屏事件,最开始的源头,如果返回true,那么子View就不会收到任何触摸分发事件

2.7 学习的参考资料

Android事件分发机制 详解攻略,您值得拥有专注分享 Android开发 干货-CSDN博客安卓事件分发
Android事件分发详解 - 简书 (jianshu.com)
MotionEvent详解_醉离歌醉的专栏-CSDN博客_motionevent
https://blog.csdn.net/xyz_lmn/article/details/12517911

2.8 觉得对自己有帮忙点个赞,创作不易谢谢支持!

推荐阅读更多精彩内容