美文网首页
View事件分发机制

View事件分发机制

作者: wangdy12 | 来源:发表于2017-05-19 20:42 被阅读0次

    事件这里指的是一系列的MotionEvent(android.view.MotionEvent)类对象,实际上是一个动作码和一个坐标轴值的集合,动作码指明了在触摸时发生的变化,坐标轴值含有位置,时间等运动属性信息,常见动作类型即action code如下所示:

    ACTION_MASKED 描述
    ACTION_DOWN 当手指第一次触摸屏幕的时候产生,是一个事件的开始,包含着初始位置等信息,该指针的指针数据索引始终为MotionEvent中0
    ACTION_MOVE 当手机在屏幕上移动的时候,产生一系列的MOVE,包括坐标轴和其他的运动属性
    ACTION_UP 最后一根手指离开屏幕时产生,标志着事件的结束(或者是ACTION_CANCEL)
    ACTION_CANCEL 动作终止,类似于ACTION_UP,但是不执行任何正常状态下要触发的动作
    ACTION_POINTER_DOWN! 超出第一个进入屏幕的触摸手指,多点触控的情况,对应的数据由getActionIndex()返回的索引获取
    ACTION_POINTER_UP 非最后一根手指离开屏幕,对应多点触控的情况

    当屏幕接收到点击,就产生了一个事件,紧接着就会触发一系列特定的方法,一套完整的事件分发机制,从上到下依次是Activity→ViewGroup→View,实际上是按照视图的层次结构进行分发的。

    Activity对事件的分发

    事件首先传递给当前屏幕上对应的Activity,它是用来和用户进行交换的窗口,每个Activity都会有一个用于绘制其用户界面的窗口,窗口通常会充满屏幕。
    Activity要执行的是dispatchTouchEvent(MotionEvent ev)函数,它可以把捕获到的动作传递给根视图。

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

    首先如果是触摸事件的开始,即对应动作是ACTION_DOWN,进入if判断内部的onUserInteraction(),该回调函数的意义是表明了用户早与当前Activity以某种方式进行交动,这些输入事件可能来自键盘,触摸或者轨迹球。与该函数对应的onUserLeaveHint()可以一起重载,用以智能地管理状态栏通知的活动等。

    然后将事件交给Window进行分发,如果返回值不为true(一般因为超出Window边界之外没有View去接收触摸事件),则执行Activity本身的onTouchEvent(),这是最后的保障手段,默认返回值为真,表明动作已经被消耗处理。

    而getWindow()获取的Window类对象是一个抽象类,可以控制顶层类的外观和行为策略,它的唯一实现类是android.view.PhoneWindow 。PhoneWindow实际上把事件传递给了DecorView。

    DecorView是FrameLayout的子类,是最顶层的视图,包含标题栏(title)和内容栏(content)。对应于它的唯一一个子视图结构LinearLayout中的两个FrameLayout子元素,其中一个是标题栏,会随着主题的不同而不同,另一个是内容栏,此外内容栏ID:Android.R.id.content是固定的。而我们经常在调用的setContentView(View view),就是指的这个名称为content的视图。

    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
    

    DecorView中该函数的内容为super.dispatchTouchEvent(),而FrameLayout中并没有这个方法,继续向上,最终实际上对应的是ViewGroup中的这个方法。如此,整个触摸事件的动作便从Activity传到顶级View。

    ViewGroup对事件的分发

    函数dispatchTouchEvent(MotionEvent ev),返回值表示事件是否分发处理。

    清除状态

    首先是通过onFilterTouchEventForSecurity(MotionEvent)函数过滤TouchEvent,如果被拦截就直接返回FALSE。之后获得动作类型,如果是ACTION_DOWN,表示一个新的手势动作开始了,就取消清除所有之前的TouchTarget记录,该类是一个单链表数据结构;并且重置触摸状态,例如将FLAG_DISALLOW_INTERCEPT标志位重置为0,表示允许父视图中断事件。

    判定拦截

    然后是判断是否拦截事件,ViewGroup在两种状态下会拦截事件,当动作为ACTION_DOWN或者mFirstTouchTarget非空,具体代码如下:

    // Check for interception.
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {
        // There are no touch targets and this action is not an initial down
        // so this view group continues to intercept touches.
        intercepted = true;
    }
    

    当一个事件开始时(ACTION_DOWN),必然要判断是否拦截该事件。另一方面,函数会记录下哪一个子View消耗了该事件,以便之后把后同一个事件序列的所有动作都交给它处理。如果当前事件被该ViewGroup拦截,那么mFirstTouchTarget的值就为null,不管后续到来的动作是什么,判决条件都是FALSE,即始终拦截后续的事件。即没有触摸目标并且这个动作不是初始按下,就直接拦截该事件。

    下一步是是获取FLAG_DISALLOW_INTERCEPT标志位的情况,这个标志位可以通过调用requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法设定的,参数值为true,表示不允许拦截事件,不执行onInterceptTouchEvent函数,否则就执行。
    在ViewGroup不拦截正常分发MotionEvent时,每一个动作都会经过onInterceptTouchEvent()方法。即onInterceptTouchEvent()函数使父视图有机会在子视图接收到事件以前,看到它所要接收处理的事件。

    onInterceptTouchEvent()返回值含义

    返回值 描述
    true 拦截MotionEvent事件,这表示它不会被传递给子View,先前正在消耗处理事件的子视图会收到ACTION_CANCEL,并且从该点开始的所有后续事件将发送到父节点的onTouchEvent()方法
    false 简单地监视事件,事件依旧沿着视图层次结构传播到通常的目标,使用目标的onTouchEvent()方法处理事件
    **拦截判断**

    事件向下分发

    如果该ViewGroup没有拦截事件的时候,事件继续向下分发,遍历所有子视图,找到一个子视图来接收事件。

    首先判断视图是否可以接收点事件,当视图是可见的,视图正在或者计划播放动画效果都是可以接收点击事件的,同时判断事件对应的坐标点是否在子视图的区域内,不满足条件的跳过,进行下一个。

    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
    }
    

    最终在dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desirePointerIdBits)函数中,调用了子视图的dispatchTouchEvent方法,这个过程中,对触摸事件的位置进行了转换操作,实际上就是根据Scroll计算了位置偏移。这样就将事件分发给了子视图。

    如果子视图返回值为true,那么就可以确定分发对象,跳出for循环。在此之前调用addTouchTarget方法,函数中对mFirstTouchTarget(TouchTarget类对象)赋值,它影响着ViewGroup的分发拦截方式,不为null时,当后边的一系列动作到来时,就可以直接传递给相应的视图,否则会拦截同一手势序列中的所有触摸事件。

    如果遍历结束以后,都没有找到消耗事件的子视图,那么ViewGroup会自己处理该事件,调用dispatchTransformeTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS)函数,把子视图参数设置为null,即把自身当做一个普通的View,调用父类View的dispatchTouchEvent方法。

    View对事件的处理

    View是一个单独的视图,没有子视图需要分发,所有直接由自身处理。

    调用onTouch方法

    首先是判断View有没有设置触摸监听(View默认情况下是ENABLE的),以及是否设置OnTouchListener接口的回调函数onTouch(View v, MotionEvent event),如果设置了,则调用该函数并取得处理之后的返回值。为true就不会调用后续的onTouchEvent方法,显然onTouch方法优先级高于onTouchEvent方法;如果返回值是false,则调用onTouchEvent方法。

    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
        result = true;
    }
    if (!result && onTouchEvent(event)) {
        result = true;
    }
    

    调用onTouchEvent方法

    onTouchEvent方法进行分析,首先会涉及到视图的状态,视图状态有很多,常用的视图状态有如下:

    名称 描述
    enabled 表示当前视图可用状态。可以通过setEnable方法进行位运算,改变视图的状态。如果对应的为为0,表示不可用,就无法响应onTouch事件,正常情况下(mViewFlags & ENABLED_MASK) == ENABLED
    focused 视图是否获得焦点。判断方式(mViewFlags & FOCUSABLE_MASK) == FOCUSABLE,类似于打游戏通过手柄的上下左右键切焦点,requestFocus方法可以改变焦点
    pressed 视图是否处于按下状态,按下对象,必须为Clickable。调用setPressed方法来对这一状态进行改变,判断方式(mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED
    clickable 视图是否可以点击,CLICKABLE对应的是setClickable函数,在设定点击的响应函数setOnClickListener时,自动会把视图设置为可点击状态

    当View处于不可用状态,即·(viewFlags & ENABLED_MASK) == DISABLED·时,函数的返回值为:

    return (((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    

    即只要是可以点击的,不论是长按LONG_CLICKABLE还是短按CLICKABLE,还是内容(用于触控笔按钮或鼠标右键单击)是可点击的CONTEXT_CLICKABLE,都会消耗点击事件,尽管处于不可用状态,View只是不作出相应的响应。

    接下来判断视图代理TouchDelegate,用于想要视图具有比其实际视图边界更大的触摸面积。触摸区域被更改的视图称为委托视图。mTouchDelegate的使用机制和mTouchListener,mOnClickListener等接口类似。

    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    

    动作处理

    最后是对具体的动作进行处理,当View是可点击视图的时候,最后就一定会返回true,View的CLICKABLE属性状态要分情况看待,实质就是视图是否可以点击,而View的LONG_CLICKABLE属性状态默认是关闭的。

    通过switch-case语句,对不同的动作状态进行不同的响应

    ACTION_DOWN

    初始化长按状态,即把mHasPerformedLongPress赋值为false,尚未执行长按动作;
    判断View是否在一个可以滚动的容器中,比如ListView,进行延时,防止当用户实际上是要滑动容器时,出现按下的状态。
    如果是在一个这样的容器中,把mPrivateFlagsPREPRESSED标识位置1;通过postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout())函数短时间推延这个动作按下的反馈,设置推延的时长为 ViewConfiguration.getTapTimeout(默认115ms),如果在这个时间段内,该Message没有从消息队列中取出,那么等到时间导到以后就运行CheckForTap类内的run函数,内容包括:

    1. 设定视图的PREPRESSED状态位为0;
    2. 调用setPressed方法,把View的PRESSED状态位设置为1;
    3. 执行checkForLongClick函数,检测View的LONG_CLICKABLE标志位,为1就把长按检测的函数通过postDelayed(mPendingCheckForLongPress,ViewConfiguration.getLongPressTimeout()-delayOffset)加入MessageQueue中,设定延时的时间为长按的检测时间(默认500ms)- 之前延时检测按下状态的时间(默认为115ms),即385ms。

    同样如果在这个时间段以内没有从消息队列中取出该Message,执行以下内容:

    1. 执行performLongClick函数,即检测视图的OnLongClickListener接口,如果有定义,就调用onLongClick函数,根据它的返回结果,确定是否把mHasPerformedLongPress状态设为已执行。

    当视图不在滚动容器内时,就立即显示按下的反馈,直接调用setPressed方法,并且发出一个检测长按的延迟事件为0毫秒的任务checkForLongClick(0, x, y)

    总之就是检测Tap和LongClick:把mPendingCheckForTapmPendingCheckForLongPress对应的run添加到Message队列中

    ACTION_MOVE

    调用drawableHotspotChanged(x, y),表明View的热点hotspot发生变化,并将变化传播到视图管理的Drawable对象或子视图时。
    然后用pointInView方法判断确定给定触摸点(在局部坐标中)是否在视图内,其中视图的边界都扩展了一个最小滑动距离TOUCH_SLOP的大小。如果移出了范围:

    1. removeTapCallback函数,把PFLAG_PREPRESSED标志位置0,并把之前CheckForTap对象mPendingCheckForTap要延时执行的Runnable通过removeCallbacks函数从消息队列中取消(如果还未执行的话);
    2. 查看PRESSED状态位,为1,说明已经过了检测Tap的115ms,第一条中的Runnable已经执行了,要移除在run函数中添加的检测长按的CheckForLongPress类对象mPendingCheckForLongPress;并且执行setPressed,把PRESSED标志位置为0。

    即只要用户移出了对应的视图的坐标范围,就将所有关于轻触(tap)和长按(long press)的状态全部取消。

    ACTION_UP:

    1. 动作结束了,对之前的所有标识进行一个总的判断,查看PREPRESSEDPRESSED状态位,不管哪一个是真,都进入下一阶段;
    2. 为视图请求焦点,并且进入触摸模式。如果View可以获得焦点,并且还没有获得焦点,就请求焦点;
    3. 把之前为预按下状态(PFLAG_PREPRESSED)的设置为按下,确保用户可以看到按下状态的出现。即如果 prepressed 值为true,调用 setPressed(true, x, y),把 PRESSED**标志位设定为1,对应于下边的第6点;
    4. 如果表示长按状态的mHasPerformedLongPress为false,并且忽略下一次ACTION_UP事件的mIgnoreNextUpEvent状态标识为false,就移除长按的检测,因为手势已经到此结束了,不可能再有长按了;
    5. 判断mPerformClick,如果为null,初始化一个实例,该类实现了一个Runable接口,然后调用post,通过异步处理Handler发送run函数到消息队列尾部,如果添加Message失败则直接执行performClick函数,确保执行,不直接调用performClick函数,可以让View的其他视觉状态在点击动作开始之前更新。
    6. 如果之前获取的prepressed值为true,64毫秒(ViewConfiguration.getPresedStateDuration)后执行UnsetPressedState类对象mUnsetPresedState,否则立即执行mUnsetPresedState;最后无论如何mUnsetPresedState.run()都会执行,其内部调用了·setPresed(false)·,把的PRESSED标志位重置为0,这样实际上是为了保证之前处于预按下状态的View,变为按下状态,有一个足够的延时(默认为64ms),来让用户观察到。
    7. 最后调用·removeTapCallback·函数,目的是移除PFLAG_PREPRESSED状态位,并且撤销在消息队列中对应的tap延时执行内容 。

    总结

    这就是Android的事件分发机制的主要流程,关键方法如下

    方法 调用位置 描述
    dispatchTouchEvent A, VG, V 分发事件到子视图
    onInterceptTouchEvent VG 在传递到子视图前拦截事件
    onTouchEvent V 处理触摸事件

    A代表Activity,VG代表ViewGroup, V 代表View

    上述内容参照了网上一些博客的描述。

    相关文章

      网友评论

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

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