美文网首页Android自定义View
Android 事件分发(看了就懂)

Android 事件分发(看了就懂)

作者: JingChen_ | 来源:发表于2020-11-22 18:07 被阅读0次

    一 前言

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

    二 点击和触摸

    首先要明白一点,点击(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的事件分发机制了

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

    相关文章

      网友评论

        本文标题:Android 事件分发(看了就懂)

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