• Android开发——事件分发机制详解


    0. 前言  

    转载请注明出处:http://blog.csdn.net/seu_calvin/article/details/52566965

    深入学习事件分发机制,是为了解决在Android开发中遇到的滑动冲突问题做准备。事件分发机制描述了用户的手势一系列事件是如何被Android系统传递并消费的。


    首先对事件分发机制进行概述:如果当一个点击事件发生时,事件最先传递给当前Activity,再传递给Window,接着传递给顶级View,最后按照事件分发机制去分发事件。事件的传递过程可以用以下伪代码进行描述:

    public boolean dispatchTouchEvent(MotionEvent ev){//事件传递到,那么该方法一定会被调用
    boolean consume = false;
    //onInterceptTouchEvent只存在于ViewGrope,判断是否拦截该事件
    //但不是每次都调用该方法,后面会详细介绍
    if(onInterceptTouchEvent(ev)){ 
        if(OnTouchListener.onTouch(ev)){// OnTouchListener优先级较onTouchEvent高
            consume = onTouchEvent(ev);//处理点击事件
    }
    }else{
        consume = child. dispatchTouchEvent(ev);
    }
        return consume;
    }
    

    对于事件传递后的事件消费,如果一个View设置了OnTouchListener,则OnTouchListeneronTouch会首先被调用。若onTouch返回false,最后才轮到onTouchEvent去消费该事件(我们平时设置的OnclickListener就在onTouchEvent方法中,优先级较低)。

    onTouchEvent返回了true,则表示事件已经被消费了,否则它的父容器的onTouchEvent将会调用,以此类推,直至由ActivityonTouchEvent被调用。

    整个过程的简要流程图如下所示:


     

    1. 事件的分发详解

    如果需要再进一步分析事件的分发机制,那么必须阅读源码。

    一个点击事件发生时,事件第一步最先传递给当前Activity


    1. 1  Activity对事件的分发

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

    从源码里可以看出,事件交给了Activity所附属的Window进行分发,返回true则结束事件分发,否则代表所有的ViewonTouchEvent返回了false(均不处理),这时是由ActivityonTouchEvent来处理。


    1. 2  Window对事件的分发

    WindowsuperDispatchTouchEvent()是一个抽象方法Window的唯一实现是PhoneWindow

    下面代码便来自于PhoneWindow对事件的分发逻辑。

    @Override  
    public boolean superDispatchTouchEvent(MotionEvent event) {  
        return mDecor.superDispatchTouchEvent(event);  
    }
    

    从源码里可以看出,事件交给了DecorView处理。我们继续查看DecorView的定义。


    1. 3  DecorView的定义

    private final class DecorView extends FrameLayout implements RootViewSurfaceTaker{…}
    
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
    

    从源码里可以看出,DecorView继承自FrameLayout的,毫无疑问FrameLayout又继承了ViewGroup,那么剩下的工作就是研读 GroupViewdispatchTouchEvent方法了。

     

    1. 4  ViewGroup

    ViewGroupActivityView比,多了一个onInterceptTouchEvent()事件拦截方法,事件传递到ViewGrouponInterceptTouchEvent返回true,则事件由ViewGroup处理(OnTouchListeneronTouchEvent优先级要高,这一点前面也介绍过了)。若返回falseViewdispatchTouchEvent会被调用。

    但是onInterceptTouchEvent并不是每次都会调用并判断是否拦截事件,ViewGroup.dispatchTouchEvent()的源码分析如下:

    // 检查是否进行事件拦截  
    final boolean intercepted;  
    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {  
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
        if (!disallowIntercept) {  
        //回调onInterceptTouchEvent(),返回false表示不拦截touch,否则拦截touch事件 
        intercepted = onInterceptTouchEvent(ev);  
        ev.setAction(action);
      } else {  
          intercepted = false;  
         }  
    } else {  
       //没有touch事件的传递对象,同时该动作不是初始动作down,ViewGroup继续拦截事件  
       intercepted = true;
    }
    

    从源码里可以看出,ViewGrouponInterceptTouchEvent判断是否去拦截事件的前提ACTION_DOW或者mFirstTouchTarget != null,关于mFirstTouchTarget,如果事件由ViewGroup的子View成功处理,mFirstTouchTarget会指向该子View不为空。

    当面对ACTION_DOW事件时,ViewGrope总是会调用自己的onInterceptTouchEvent来询问是否去拦截事件,因此若ViewGroup拦截了ACTION_DOWN事件,mFirstTouchTarget一定为空。当后续ACTION_MOVEACTION_UP事件到来时,ViewGrouponInterceptTouchEvent不会调用,直接拦截。那么有什么办法可以修改这种默认机制呢?


    我们还注意到源码中的标记位FLAG_DISALLOW_INTERCEPT,该标记位通过子View的getParent().requestDisallowInterceptTouchEvent方法来设置,作用是ViewGrope将无法拦截除ACTION_DOW以外的点击事件。这为我们后面如何处理滑动冲突提供思路。至于为什么无法拦截ACTION_DOW可以由以下源码证明。

    //处理初始的down事件  
    if (actionMasked == MotionEvent.ACTION_DOWN) {  
    // ACTION_DOWN到来时的重置操作 
    //当app切换、 ANR或一些其他的touch状态发生时,framework会丢弃或取消先前的touch状态  
      cancelAndClearTouchTargets(ev);  
      resetTouchState();//该方法中会重置FLAG_DISALLOW_INTERCEPT标记位
    } 
    


    1. 5  View对事件的处理

    ViewGropeonInterceptTouchEvent返回false,会首先遍历所有的子元素,判断子元素是否能够接收点击事件(通过判断子元素是否在播放动画并且点击坐标落在该子元素区域内)。若子元素具备接收事件的条件,那么它的dispatchTouchEvent会被调用,若遍历完所有的子元素均返回false,那么只能ViewGrope自己去处理该事件。子元素的该方法返回true终止遍历子元素

    事件传递就来到了子View的“手里”,处理过程如下。

    public boolean dispatchTouchEvent(MotionEvent event) {  
    boolean result = false;
    //...
        if (onFilterTouchEventForSecurity(event)) {  
           //noinspection SimplifiableIfStatement  
           ListenerInfo li = mListenerInfo;  
           if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED  
                        && li.mOnTouchListener.onTouch(this, event)) {  
             return true; }  
      
           if (onTouchEvent(event)) {  
            return true; }  
    }
        //…
        return result;  
    }
    

    View无法继续向下传递事件,只能处理之。从源码第8行可以看出会执行ViewOnTouchListener.onTouch这个函数,若返回trueonTouchEvent便不会再被调用了。可见OnTouchListeneronTouchEvent优先级更高

     

    1. 6  ViewonTouchEvent

    首先当View处于不可用状态时的处理过程如下:

    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }
    

    可以看出Viewenable属性不影响onTouchEvent的返回值ViewonTouchEvent默认消耗事件并返回true,除非其CLICKABLELONG_CLICKABLE均为false。后者在View中默认为false,前者根据控件本身来决定。通过setClickablesetLongClickable可以改变View的这两个属性。setOnClickListenersetOnLongClickListener本质上也是通过setClickablesetLongClickable来改变View的这两个属性。


    View对事件的具体处理如下:

    public boolean onTouchEvent(MotionEvent event) {    
        //...  
        if (((viewFlags & CLICKABLE) == CLICKABLE ||    
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {    
            switch (event.getAction()) {    
                case MotionEvent.ACTION_UP:  
                    ...  
                    performClick();                  
                    break;    
                case MotionEvent.ACTION_DOWN:  
                    break;    
                case MotionEvent.ACTION_CANCEL:    
                    break;    
                case MotionEvent.ACTION_MOVE:    
                    break;    
            }
            // 
            return true;    
        }    
        return false;    
    }  

    从源码中可以看出,如果一个控件是clickablelongclickable的,那么就会执行ACTION_UPACTION_DOWNcase里面,并最终返回true。需要说明的是,如果在上一个case(比如:ACTION_UP)中返回了false,那么下面所有的case(比如:ACTION_CANCELACTION_MOVE)都不会得到执行。


    ACTION_UP发生时会调用performClick方法,源码如下所示:

    public boolean performClick() {    
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);    
        if (mOnClickListener != null) {    
            playSoundEffect(SoundEffectConstants.CLICK);    
            mOnClickListener.onClick(this);    
            return true;    
        }    
        return false;    
    }    
    

    performClick的实现来看,如果View设置了OnClickListener,在performClick方法中会调用它的onClick方法

     

    至此关于事件分发机制就介绍完毕了

    转载请注明出处:http://blog.csdn.net/seu_calvin/article/details/52566965


  • 相关阅读:
    eval命令的使用
    declare与typeset的使用
    【线型DP模板】最上上升子序列(LIS),最长公共子序列(LCS),最长公共上升子序列(LCIS)
    【线型DP】【LCS】UVA_10635 Prince and Princess
    【经典DP】洛谷 P2782 友好城市
    HDU-1051/POJ-1065 Wooden sticks 木棍子(动态规划 LIS 线型动归)
    BZOJ3659 Which Dreamed It
    CF300D Painting Square
    CF632E Thief in a Shop 和 CF958F3 Lightsabers (hard)
    CF528D Fuzzy Search 和 BZOJ4259 残缺的字符串
  • 原文地址:https://www.cnblogs.com/qitian1/p/6461512.html
Copyright © 2020-2023  润新知