Android事件分发机制

作者: zhongjh | 来源:发表于2021-02-28 20:23 被阅读0次

    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 觉得对自己有帮忙点个赞,创作不易谢谢支持!

    相关文章

      网友评论

        本文标题:Android事件分发机制

        本文链接:https://www.haomeiwen.com/subject/tsobtltx.html