Android 事件分发(看了就懂)

一 前言

最近复习到了事件分发这知识点,之前对这个知识点只能说懂一些,能做到简单描述而已。既然最近复习到了,那我就写下来加深印象。接下来好好看,因为是刚学习到的,写的不是很深,看完基本都能懂这是怎么回事的。

二 点击和触摸

首先要明白一点,点击(click)和触摸(touch)是两个不同的事件,之前一直以为两者都一样,之后才知道。简单说下:
①点击是:手指放下屏幕开始直到手指离开屏幕后,click事件才被触发。你可以想一下,有时候你不小心点下屏幕,但是误点不想,你就往其他地方移动,这样手指离开了,事件没被触发,相当于click事件没被触发。
②触摸是:当你手指接触屏幕开始,这个事件就开始触发了,与click不同的是,click一个完整的操作才触发。
总结一点:触摸事件(touch)优先于点击事件(click)触发

三 事件分发流程

事件分发的流程很复杂,反正就是从硬件到软件。在Android里面,为了能简单描述,我只说Activity开始里面的流程。


布局.png

自己不会画图,在网上找了这张图。相信大家都能看懂这张图吧。Activity不用说的,你看到的页面。ViewGroup就是Activity的XML布局里面的父布局,你可以理解成你写的LinearLayout、ConstraintLayout...。View更清楚啦,ViewGroup里面的TextView、ImageView、Button...

好了,话不多说进入正题。刚才说从Activity开始的,我们假设点击一个ConstraintLayout里面包裹的Button时,弹出一条吐司。这个事件的传递顺序是:Activity -> ViewGroup -> View,即:1个点击事件发生后,事件先传到Activity、再传到ViewGroup、最终再传到 View

大部分之前的做法就是在Activity里面注册这个Button的点击事件,然后再onClick里面写Toast操作,这个大家基本都会的吧。但你只知道是这么写的,但里面的流程怎么走的就不知道了吧?

本来想跟大家分析下源码的,为了简洁点,我只挑重点:
有这么三个东西:
dispatchTouchEvent():事件开始分发,结果表示事件有没有被消费
onInterceptTouchEvent():拦截事件,拦截后就停止往下分发了
onTouchEvent():事件分发的结果,表示有没有被消费,结果回调给dispatchTouchEvent,可以理解成onTouchEvent返回什么,dispatchTouchEvent的结果就是什么

①Activity里面有dispatchTouchEvent()和onTouchEvent()
②ViewGroup里面有dispatchTouchEvent()和onInterceptTouchEvent()
③View里面有dispatchTouchEvent()和onTouchEvent()

接下来我会对这三个部分的事件分发都分析一波😏

四 Activity的事件分发

每次事件分发,都是从Activity里面的dispatchTouchEvent()开始分发的

//Activit.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

    public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

分析:第一个判断是,事件是不是DOWN事件(按下),一般事件列开始都是DOWN事件 = 按下事件,故此处基本是true。onUserInteraction()是个空方法,一般情况不做处理,所以走到了第二个判断,getWindow().superDispatchTouchEvent(ev),至于它是啥,等等再讲,若getWindow().superDispatchTouchEvent(ev)的返回true,则Activity.dispatchTouchEvent()就返回true,则方法结束,true表示这个事件被消费,否则,继续往下走,第三个方法onTouchEvent()。里面有个mWindow.shouldCloseOnTouch(this, event)表示:对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界内等。举个栗子:写过弹窗(dialog)的就是知道,弹窗不是全屏的时候,你的点击弹窗外的地方时,默认情况下弹窗消失,此时mWindow.shouldCloseOnTouch(this, event)返回true,弹窗finish()。一般情况下,Activity.onTouchEvent()返回false,回调,Activity.dispatchTouchEvent()返回false,整个事件没有被消费,分发结束。目前,你只看到了Activity的事件分发,别急~接着往下讲ViewGroup和View的事件分发。

噢对了,刚才还getWindow().superDispatchTouchEvent(ev),还没解释,它是什么呢?看代码就知道,它是Window里面的一个方法,Window是个抽象类,而PhoneWindow就是它的实现类,(这里插入一下其他知识点)每个activity都对应一个窗口window,这个窗口是PhoneWindow的实例,PhoneWindow对应的布局是DecorView,它是一个FrameLayout也是最顶层的View,FrameLayout又是继承ViewGroup,所以最终是进入了ViewGroup的dispatchTouchEvent(),简单点说,getWindow().superDispatchTouchEvent(ev)就是进入ViewGroup判断事件分发。

总结:当一个点击事件发生时,从Activity的事件分发开始


activity事件分发.png

以上就是Activity的事件分发,应该都通俗易懂吧🤭,接下来看ViewGroup的事件分发,就是刚才提及的getWindow().superDispatchTouchEvent(ev)

五 ViewGroup的事件分发

从上面Activity事件分发机制可知,ViewGroup事件分发机制从dispatchTouchEvent()开始,它的dispatchTouchEvent()比Activity的代码量多,为了大家都能听懂所以我只挑重点

//ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) { 

    ... // 仅贴出关键代码

        // ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {  

            // 判断值1:disallowIntercept = 是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
            // 判断值2: !onInterceptTouchEvent(ev) = 对onInterceptTouchEvent()返回值取反
                    // a. 若在onInterceptTouchEvent()中返回false(即不拦截事件),就会让第二个值为true,从而进入到条件判断的内部
                    // b. 若在onInterceptTouchEvent()中返回true(即拦截事件),就会让第二个值为false,从而跳出了这个条件判断
                

                ev.setAction(MotionEvent.ACTION_DOWN);  
                final int scrolledXInt = (int) scrolledXFloat;  
                final int scrolledYInt = (int) scrolledYFloat;  
                final View[] children = mChildren;  
                final int count = mChildrenCount;  

            // 通过for循环,遍历了当前ViewGroup下的所有子View
            for (int i = count - 1; i >= 0; i--) {  
                final View child = children[i];  
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                        || child.getAnimation() != null) {  
                    child.getHitRect(frame);  

                    // 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
                    // 若是,则进入条件判断内部
                    if (frame.contains(scrolledXInt, scrolledYInt)) {  
                        final float xc = scrolledXFloat - child.mLeft;  
                        final float yc = scrolledYFloat - child.mTop;  
                        ev.setLocation(xc, yc);  
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  

                        // 条件判断的内部调用了该View的dispatchTouchEvent()
                        // 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面的View事件分发机制)
                        if (child.dispatchTouchEvent(ev))  { 

                        mMotionTarget = child;  
                        return true; 
                        // 调用子View的dispatchTouchEvent后是有返回值的
                        // 若该控件可点击,那么点击时,dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立
                        // 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出

                                }  
                            }  
                        }  
                    }  
                }  

        // 若点击的是空白处(即无任何View接收事件) / 拦截事件(手动复写onInterceptTouchEvent(),从而让其返回true)
        if (target == null) {  
            ev.setLocation(xf, yf);  
            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
                ev.setAction(MotionEvent.ACTION_CANCEL);  
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
            }  
            
            return super.dispatchTouchEvent(ev);
            // 调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()
            // 因此会执行ViewGroup的onTouch() ->> onTouchEvent() ->> performClick() ->> onClick(),即自己处理该事件,事件不会往下传递(具体请参考View事件的分发机制中的View.dispatchTouchEvent())
            // 此处需与上面区别:子View的dispatchTouchEvent()
        } 

        ... 

}


public boolean onInterceptTouchEvent(MotionEvent ev) {  
    
    return false;

  } 

关键代码比Activity里面多多了😓,我刚开始看的时候一见这么多都不想接着看了,但是都学到这里了就一起看完吧。
分析:

首先判断,disallowIntercept || !onInterceptTouchEvent(ev),disallowIntercept = 是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改,所以不做任何操作情况下就是false了,此时进不进入这个判断就要看onInterceptTouchEvent(ev)。刚刚上面有讲到,onInterceptTouchEvent(ev)只在ViewGroup里面出现,这是什么东西呢?看英文就知道“拦截触摸事件”,若在onInterceptTouchEvent()中返回false(即不拦截事件),就会让第二个值为true,从而进入到条件判断的内部,若在onInterceptTouchEvent()中返回true(即拦截事件),就会让第二个值为false,从而跳出了这个条件判断。

假设onInterceptTouchEvent()不拦截,就是false,取反为true就进入内部,内部又是什么呢?接这看。里面很多字段,大家看得懂和看不懂都没关系,只要知道for循环那里就是遍历当前ViewGroup下的所有子View。好的,取到每个子View后要干嘛呢?对每个子View判断frame.contains(scrolledXInt, scrolledYInt),那两个参数就是点击时的坐标表示,这个判断就是判断当前遍历的View是不是被你点击时选中的,如果是!进入最后一层判断child.dispatchTouchEvent(ev),这个又是什么呢?child表示是ViewGroup的子View,即View.dispatchTouchEvent(),也就是说这里实现了点击事件从ViewGroup到子View的传递,后面会讲到View里面的处理,如child.dispatchTouchEvent(ev)返回true,表明事件被子View消费了,整个ViewGroup.dispatchTouchEvent()返回true,回调到Activity就是Activity.dispatchTouchEvent()返回true,结束事件分发。child.dispatchTouchEvent(ev)返回false相反,返回流程也是一样的,表明事件没被子View消费,结束事件分发。

假设onInterceptTouchEvent()拦截了,就是true,取反为false就不进入内部,直接走到了判断条件target == null,target不做处理时为空,进入后返回super.dispatchTouchEvent(ev),这个又是什么呢?再提一个知识点,ViewGroup是继承View的,所以说到底它就是一个View,只不过它里面可以包含很多子View,所以刚才的super.dispatchTouchEvent(ev),就是父类的dispatchTouchEvent(),即View.dispatchTouchEvent()。返回结果跟第②点一样的逻辑,结果都回调给Activity。

onInterceptTouchEvent(MotionEvent ev)的默认返回值是false,我们可以复写onInterceptTouchEvent(),从而让其返回true,实现拦截效果,即事件不往子View里面传递,给本身去处理,父类View.dispatchTouchEvent()。

注:②、③最终都是走到View里面的dispatchTouchEvent(),但表现不一样,②是子View的,③是父类的。

ViewGroup事件分发.png

至此,假如你能都懂了,那你就对这个事件分发理解了三分之二了😆,想想还是有点激动的。接着往下看最后的View里面事件分发怎么处理的吧。

六 View的事件分发

从上面ViewGroup事件分发机制知道,View事件分发机制从dispatchTouchEvent()开始,你可以理解成,事件分发都会走到这里,事件有没有消费最终都在这里回调出去。

//View.java
  public boolean dispatchTouchEvent(MotionEvent event) {  
  // 说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent()
  //     1. mOnTouchListener != null
  //     2. (mViewFlags & ENABLED_MASK) == ENABLED
  //     3. mOnTouchListener.onTouch(this, event)
        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
                mOnTouchListener.onTouch(this, event)) {  
            return true;  
        } 
        return onTouchEvent(event);  
  }

分析:

mOnTouchListener是什么呢?View里面有这个setOnTouchListener方法,类似于注册点击事件一样,我们在给控件注册Touch事件时,就已经给mOnTouchListener赋值了,所以注册后mOnTouchListener不为空。

public void setOnTouchListener(OnTouchListener l) { 

    mOnTouchListener = l;  
    // 即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)
        
} 

(mViewFlags & ENABLED_MASK) == ENABLED,这个很好理解,该条件是判断当前点击的控件是否enable,由于很多View默认enable,故该条件恒定为true。(你也可以给控件把enable属性设置为false,这样就跳出内部了)

mOnTouchListener.onTouch(this, event),回调控件注册Touch事件时的onTouch(),已Button为例

    button.setOnTouchListener(new OnTouchListener() {  
        @Override  
        public boolean onTouch(View v, MotionEvent event) {  
     
            return false;  
        }  
    });

若在onTouch()返回true,就会让上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束;若在onTouch()返回false,就会使得上述三个条件不全部成立,从而使得View.dispatchTouchEvent()中跳出If,执行onTouchEvent(event)

View的onTouchEvent(event),重点来了,以上条件中有一个不成立时都走这个方法,源码也是有点多,同样我也只讲重点,先贴出来吧

       public boolean onTouchEvent(MotionEvent event) {  
                ···//仅贴出关键代码

            // 若该控件可点击,则进入switch判断中
            if (((viewFlags & CLICKABLE) == CLICKABLE ||  
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  

                switch (event.getAction()) { 

                    // a. 若当前的事件 = 抬起View(主要分析)
                    case MotionEvent.ACTION_UP:  
                        boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  

                            ...// 经过种种判断,此处省略

                            // 执行performClick() 
                            performClick();  
                            break;  

                    // b. 若当前的事件 = 按下View
                    case MotionEvent.ACTION_DOWN:  
                        if (mPendingCheckForTap == null) {  
                            mPendingCheckForTap = new CheckForTap();  
                        }  
                        mPrivateFlags |= PREPRESSED;  
                        mHasPerformedLongPress = false;  
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
                        break;  

                    // c. 若当前的事件 = 结束事件(非人为原因)
                    case MotionEvent.ACTION_CANCEL:  
                        mPrivateFlags &= ~PRESSED;  
                        refreshDrawableState();  
                        removeTapCallback();  
                        break;

                    // d. 若当前的事件 = 滑动View
                    case MotionEvent.ACTION_MOVE:  
                        final int x = (int) event.getX();  
                        final int y = (int) event.getY();  
        
                        int slop = mTouchSlop;  
                        if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                                (y < 0 - slop) || (y >= getHeight() + slop)) {  
                            // Outside button  
                            removeTapCallback();  
                            if ((mPrivateFlags & PRESSED) != 0) {  
                                // Remove any future long press/tap checks  
                                removeLongPressCallback();  
                                // Need to switch from pressed to not pressed  
                                mPrivateFlags &= ~PRESSED;  
                                refreshDrawableState();  
                            }  
                        }  
                        break;  
                }  
                // 若该控件可点击,就一定返回true
                return true;  
            }  
             // 若该控件不可点击,就一定返回false
            return false;  
        }

首先判断 ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE),这个意思该控件是否能点击,若能,则进入switch判断,若不能直接跳出,最外层返回false,也就是说控件不可点击时,onTouchEvent直接返回false,表明事件没被消费。继续看switch判断,其实就是一些动作判断,什么DOWN啊,UP啊,MOVE啊...里面每个动作的代码可以先不看,你最后看下,不管什么动作进来了,它最后处理完都会返回true,总结一点就是控件可点击时,一定返回true。接下来重点看下UP动作,执行了performClick()。

    public boolean performClick() {  

        if (mOnClickListener != null) {  
            playSoundEffect(SoundEffectConstants.CLICK);  
            mOnClickListener.onClick(this);  
            return true;  
        }  
        return false;  
    }  

只要我们通过setOnClickListener()为控件View注册1个点击事件,那么就会给mOnClickListener变量赋值(即不为空),则会往下回调onClick(),onClick()就是你注册点击事件时里面要做的东西,就像开头里面弹吐司的操作。之后 performClick()返回true。

注:onTouch()的执行先于onClick(),也就验证开头说的总结

View事件分发.png

View的事件分发是比其他两个复杂一点,但也不是很难懂,跟着源码走,所有逻辑都能缕清了。

七 总结

Android 的事件分发说难不难,说简单不简单,最重要的是要先从布局开始,然后从源码入手,跟着源码一步步走下去,思路就很清楚啦!结合刚才三个图,用一幅图总结下:


事件分发.png

若您已经看到此处,那么恭喜你,你已经能非常熟悉掌握Android的事件分发机制了

本人也是刚学不久的,写的不好的地方麻烦大伙帮忙指出,大牛轻拍 = =

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容