《View的事件体系》(四)View的事件分发机制

    xiaoxiao2021-03-25  19

             前面几篇博客介绍了View的几个基础知识以及View的滑动实现,但View真正的难点和重点在于View的事件分发机制,因为解决View滑动冲突的理论基础就是View的事件分发机制。

    1传递规则

           首先要声明一点,分析View点击事件事实上分析的就是MotionEvent,所谓View事件的分发机制其实就是对MotionEvent的分发过程。即当一个MotionEvent产生的时候,我们主观来看他就是应该某个View(Button,EditView。。。)响应,但是在Android系统中是需要对MotionEvent进行一系列的判断,才能找到那个正确的具体的View并作出响应。这里分析的就是这个过程。

           先介绍三个方法,onInterceptTouchEvent方法是出现在ViewGroup中的,View中没有该事件,diapatchTouchEvent和onTouchEvent方法在Activity、View、ViewGroup中都可能被调用

           public  boolean  dispatchTouchEvent(MotionEvent ev)

           从方法名中的diapatch(中文指分发)大概能推测出这个方法用于事件的分发。如果事件能传递给当前View(不一定拦截),那么此方法就会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法影响,返回值表示是否消耗当前事件。

          public  boolean   onInterceptTouchEvent(MotionEvent ev)

          Intercept中文指拦截,该方法在上面的方法中被调用,用来判断是否拦截某个事件。

         public  boolean   onTouchEvent(MotionEvent ev)

         也是在上面的第一个方法中被调用,用来处理点击事件,返回结果表示是否消耗此事件,如果不消耗,那么同一事件序列的其他事件不会再交到该View。(事件序列和事件的关系下面会介绍)

         他们三者有什么关系呢?请看下面的伪代码,因为是伪代码所以细节会在下面讲清楚:

    public boolean diapatchMotionEvent(MotionEvent ev){

        boolean consume = false;//是否消耗当前事件

        if(onInterceptTouchEvent(ev)){

               consume =onTouchEvent(ev);

    }else

    {consume = child.diapatchTouchEvent(ev);}

    return consume;

    // 当View的onTouchEvent被调用,但是返回了false,代表他处理不了该事件,那么他的父容器的onTouchEvent就会被调用,依次类推,如果最后所有的View元素都不处理这件事,那该次事件就会被返交给事件传递规则中的最顶层Activity,即Activity的onTouchEvent会被调用。

    }

            从这份伪代码就可以大概了解点击事件的传递规则了:

            当一个点击事件产生后,传递遵循如下规则:Activity-> Window -> DecorView。顶级事件接收到事件后就会按照下面说的事件分发机制分发事件。首先传递给ViewGroup(我们介绍过ViewGroup,ViewGroup是容纳View组件的容器,其本身也是从View派生出来的。ViewGroup派生的子类有LinearLayout,FrameLayout等),这时他的diapatchTouchEvent就会被调用,如果onInterceptTouchEvent返回true,表示这个ViewGroup要拦截这个事件,那么这个事件就会交给这个ViewGroup处理,也就是他的onTouchEvent方法就会被调用;如果这个ViewGroup不拦截当前事件,那当前事件就会传递给他的子元素,接着子元素的diapatchTouchEvent就会被调用,如此反复直到事件被处理。这个设计思想好像在Android系统中非常常见。

             给一个View设置的onTouchListener的优先级比他的onTouchEvent要高,也就是说当一个View需要处理事件时,要看 OnTouchListener中onTouch方法的返回值,返回值是true那么onTouchEvent将不会被调用,返回值是false那么onTouchEvent会被调用。如果onTouchEvent被调用,而且设置的有onClickListener,那么onClickListener的onClick方法会被调用。所以可以看出就调用的优先级来说,onTouchListener> onTouchEvent> onClickListener,onClickListener处于事件传递的尾端。

            下面是事件传递规则中的一些常识,根据这些常识有助于更好的理解整个事件传递机制:

    (1)    当手指触碰屏幕那刻起到手指离开屏幕的那刻结束,这个过程系统中会产生一个以down事件开始,中间有若干move事件,最后up事件结束的事件序列。上面分发的MotionEvent其实是事件序列中的某个序列。

    (2)    正常情况下,一个事件序列只能被一个View拦截并消耗,原因可以参考(3)。但是可以通过特殊手段做到一个事件序列的事件被多个View同时处理,比如将一个View把本该自己处理的事件通过onTouchEvent强行传递给其他View处理。

    (3)    一旦某个View决定拦截,这个事件序列的其他事件也会交给他处理,他的onInterceptToucheEvent也不再被调用。

    (4)    一旦一个事件交给一个View处理,那么他必须消耗掉,否则同一事件序列的其他事件也不会给他处理了。也就是说一个View要处理一个事件序列,必须先处理ACTION_DOWN。

    (5)    如果View消耗了ACTION_DOWN,但是不消耗其他事件,那这个点击就会消失,也不交给父容器的TouchEvent。该View持续接收后续事件最后交给Activity处理。

    (6)    ViewGroup默认不拦截任何时间,也就是说Android源码中onInterceptTouchEvent方法默认返回false。

    (7)    View中没有onInterceptTouchEvent,所以一旦有点击事件传递给他,他的onTouchEvent就会被调用。

    (8)    View的onTouchEvent默认都是返回true的,也就是消耗该事件,除非是不可点击的(clickable和longClickable都是false)。比如TextView的clickable默认就是false。

    (9)    View的enable(可不可用)属性不影响onTouchEvent的返回值,只要View的clickable和longClickable中有一个true,onTouchEvent就返回true。

    (10)  onClick发生的前提是View可点击,并且接收到down和up事件

    (11)  事件传递是由外向内的,就会说事件总是先传递给父元素,再由父元素分发给子元素。依次重复,直到被ViewGroup拦截或者View处理,或者交给了Activity。

     

    2具体分析

            单单看上面的结论,也许对处理、消耗、拦截等出现的时机以及前面的一些常识有很多疑惑,从源码我们就能找出答案。

    1.     Activity对点击事件的分发过程

            当一个点击事件发生时最先传递给当前Activity,由Acticity的dispatchTouchEvent分发,具体的工作由Activity中的Window完成。Window会把事件传递给DecorView,DecorView我们说过,一般是当前界面的底层容器(是setContentView设置的View的父容器),用Activity.getWindow.getDecorView()可以获得。Window是个抽象类,他只有PhoneWindow一个实现,所以我们看PhoneWindow怎么处理点击事件就行了。

    public boolean onInterceptTouchEvent (MotionEvent event){

        return mDecor. onInterceptTouchEvent(event)

    }

           可见PhoneWindow直接将MotionEvent交给了DecorView。到这里事件已经传递到顶级View了。下面就看顶级View对事件怎么分发的

    2.     顶级View对点击事件的分发过程

          关于点击事件如何在View中分发,前面有过介绍了,这里再大致梳理一下:

         点击事件到达顶级View(一般是一个ViewGroup)后,会调用ViewGroup的dispatchTouchEvent方法,该方法中如果顶级View拦截事件也就是onInterceptTouchEvent返回true,则事件交给ViewGroup处理,这时如果ViewGroup的mOnTouchListener被设置,那么他的onTouch就会被调用,否则调用TouchEvent。在TouchEvent中,如果设置了mOnClickListener,那onCLick就会被调用。如果ViewGroup不拦截事件,则事件会被传递到子View,这时子View的dispatchTouchEvent会被调用,如此循环完成事件的分发。如果遍历子View后都没有被合适的处理,可能有两种情况:第一种是ViewGroup没有子元素,第二种是子元素处理了点击事件但是在diapatchTouchEvent方法返回了false,一般是因为onTouchEvent返回了false。这两种情况下ViewGroup会自己处理点击事件。前面已经说了,onTouchEvent的返回值是和View可不可点击有关的,比如TextView就不可点击,但是可以通过setClickable和setLongClickable改变View的CLICKABLE和LONG_CLICKABLE属性。另外setOnCLickListener和setOnLongClickListener将自动把对应的那个属性设为true。

    转载请注明原文地址: https://ju.6miu.com/read-50434.html

    最新回复(0)