美文网首页
清晰易懂的Android View事件分发 原理及实例 -- 源

清晰易懂的Android View事件分发 原理及实例 -- 源

作者: AndroidTony | 来源:发表于2017-12-11 17:29 被阅读37次

    1. 基础知识

    1.1 事件MotionEvent

    当用户触摸屏幕时,就会产生点击事件MotionEvent。
    MotionEvent中记录了触摸的位置,时间、历史记录、手势动作等信息。

    1.2 事件种类

    • MotionEvent.ACTION_DOWN:按下View(所有事件的开始)
    • MotionEvent.ACTION_MOVE:滑动View
    • MotionEvent.ACTION_UP:抬起View(与DOWN对应)
    • MotionEvent.ACTION_CANCEL:非人为原因结束本次事件,注意,当ViewGroup中途拦截之前传给其子View的事件时,就会传一个ACTION_CANCEL给子View。

    1.3 事件序列

    从手指接触屏幕至手指离开屏幕,整个过程的触摸事件。
    一个事件序列以DOWN事件开始,中间有无数个MOVE事件,最后以UP事件结束。

    1.4 事件分发

    将事件传递给某个View进行处理的过程。

    1.5 事件分发的对象

    硬件 ViewRootImpl DecorView PhoneWindow Activity PhoneWindow DecorView ->DecorView的子View
    开发中能够接触到的是:
    Activity -> ViewGroup -> View


    image.png

    1.6 事件分发的顺序

    • ViewGrouo优先与View。 事件会从顶层ViewGroup开始向下传递,ViewGroup可以选择拦截事件,这样就不会再往下传递。默认情况下不会拦截,所以会一直传到最下层的View。如果该View还是不消费该事件,则将该事件从下往上传递。
    • 用户设置的监听优先与系统回调。消费一个事件分为两种情况:1 用户给View设置了监听onTouchListener并且返回true 2 回调系统自带的View的OnTouchEvent()并且返回true。注意,只有返回true才是消费了该事件。即如果存在第一种情况,则事件会被onTouchListener 消费掉,不再回调OnTouchEvent。

    2 事件分发的主要方法(概览篇)

    忽略ViewRootImpl DecorView PhoneWindow这三者。

    2.1 Activity(伪代码)

    public boolean dispatchTouchEvent(MotionEvent event)
    {
    //省略代码
              if (getWindow().superDispatchTouchEvent(ev)) {
                return true;
                }
            return onTouchEvent(ev);
        }
    }
    
    public boolean onTouchEvent(MotionEvent event) 
    {
    //省略代码
        return false;
    }
    

    2.2 ViewGroup(伪代码)

    public boolean dispatchTouchEvent(MotionEvent event){
       if(event不是ACTION_DOWN && mFirstTouchTarget  == null){
          return;
          }
       if(!disallowIntercept  && onInterceptTouchEvent(event) ){
           return super.dispatchTouchEvent();
        } 
       if(child.dispatchTouchEvent(event)){
          mFirstTouchTarget.add(child);
          return true;
        } else{
          return super.dispatchTouchEvent();
    }
    

    super.dispatchTouchEvent()伪代码为:

       if(onTouchListener.onTouch()){
          mFirstTouchTarget.add(this);
          return true;
        }
        if(onTouchEvent(event) ){
           mFirstTouchTarget.add(this);
            return true;
          }       
         return false;
    
    public boolean onInterceptTouchEvent(MotionEvent event){
    //默认返回false;
        return false;
    }
    
    //继承自View,ViewGroup并没有重写该方法
    public boolean onTouchEvent(MotionEvent event) 
    

    2.3 View

    public boolean dispatchTouchEvent(MotionEvent event){
       if(设置了touchListener && touchListener.onTouch()){
       return  true;
       }
       return onTouchEvent();
    }
    
    public boolean onTouchEvent(MotionEvent event) {
       if(不可用但是clickable){
       return true;
        }
       if(CLICKABLE || LONG_CLICKABLE || CONTEXT_CLICKABLE ){
        performClick();
        return true;
       }
       return false;
    }
    

    3 事件分发的主要方法(讲解篇)

    3.1 Activity

    3.1.1 boolean dispatchTouchEvent(MotionEvent event)

    表示如何分发事件,事件首先会传递到该方法。

    • 1 DOWN事件发生后,会调用该方法,并把事件往下传递。
    • 2 如果有View进行消费,则getWindow().superDispatchTouchEvent(ev)会返回true,则该方法也会返回true,不调用onTouchEvent()。
    • 3 如果没View消费该事件,getWindow().superDispatchTouchEvent(ev)会返回false,则该方法会调用Activity的onTouchEvent()。
      注意:如果是这种情况,则同一事件序列的后续事件,Activity传递到DecorView的dispatchTouchEvent方法中以后,基于某些判断就不会再往下传递(具体原因后面会讲到)。
    public boolean dispatchTouchEvent(MotionEvent event)
    {
    //省略代码
              if (getWindow().superDispatchTouchEvent(ev)) {
                return true;
                }
            return onTouchEvent(ev);
        }
    }
    

    3.1.2 boolean onTouchEvent(MotionEvent event)

    public boolean onTouchEvent(MotionEvent event) 
    {
    //省略代码
        return false;
    }
    

    3.2 ViewGroup

    3.2.1 boolean dispatchTouchEvent(MotionEvent event)

    表示如何分发事件,事件首先会传递到该方法。

    • 1 DOWN事件传递到这里之后,该ViewGroup(后续简称为VG)首先判断是否要拦截该事件。

      • 如果拦截,则调用是否消费事件的方法super.dispatchTouchEvent()(由于ViewGroup为View的子类,所以会走到View的dispatchTouchEvent方法中),View的dispatchTouchEvent方法中会进行如下操作:

      若用户设置的监听不为空(即mTouchListener不为null),则调用onTouchListener.onTouch(),如果onTouchListener.onTouch()返回true,则表示消费该事件,跳出。如果返回false,会接着调用onTouchEvent(),返回值代表是否消费该事件;
      若用户设置的监听为空(即没有设置该监听),则直接调用onTouchEvent(),返回值代表是否消费该事件。
      注意:存在VG拦截事件但是并不消费事件的情况,例如onInterceptTouchEvent返回true,onTouchEvent返回false。如果是DOWN事件,这种情况就是前面讲的,没有View消费该事件。

      • 如果不拦截,则找到包含点击位置的子控件,调用子控件的dispatchTouchEvent()方法。
        子控件如果也不消费,即子控件的dispatchTouchEvent()返回false。此时该事件会由下往上传递,进入到
        父控件的super.dispatchTouchEvent(),再次询问是否以及如何消费该事件。
    • 2 VG通过disallowIntercept 标志以及onInterceptTouchEvent(event)去判断是否需要拦截该事件。

    • 3 DOWN事件后续的其它事件,如果是该VG自身消费了前面的DOWN事件,则直接调用super.dispatchTouchEvent()。如果是其子View消费了前面的DOWN事件,则先判断是否拦截,再根据结果决定进行后续处理(如果不拦截,则调用子view的dispatchTouchEvent。如果拦截,则传递一个CANCEL事件给子View。同时后续的事件,都直接交给VG处理,不再往下传递)。

    public boolean dispatchTouchEvent(MotionEvent event){
    //如果不是ACTION_DOWN,且之前同一事件序列的ACTION_DOWN事件没有view进行处理(即mFirstTouchTarget 为null),则丢弃该事件。
    //这就是为什么如果没有View处理ACTION_DOWN,后续事件传递到DecorView之后就不会再往下传递了。
    //即使设置了disallowIntercept = true也没用,因为根本走不到disallowIntercept 的校验。
       if(event不是ACTION_DOWN && mFirstTouchTarget  == null){
          return;
          }
       if(!disallowIntercept  && onInterceptTouchEvent(event) ){
    //走到这里,表示父布局进行拦截
    //返回值表示父布局是否消费该事件;
    //父布局如果消费,则mFirstTouchTarget就不为空。
           return super.dispatchTouchEvent();
        } 
    //走到这里说明没有被父布局拦截
    //遍历child,根据滑动点的坐标值找到滑动的child
       if(child.dispatchTouchEvent(event)){
          mFirstTouchTarget.add(child);
          return true;
        } else{
    //走到这里说明没有被拦截,但是子视图也没有消费该事件,
    //则调用view的dispatchTouchEvent()。
          return super.dispatchTouchEvent();
    }
    

    3.2.2 boolean onInterceptTouchEvent(MotionEvent event)

    表示是否要拦截该事件。

    • 注意:在子View消费DOWN事件的前提下,ViewGroup可以在事件序列中途拦截MOVE事件。这种情况下,会传递一个CANCEL事件给其子View.后续的MOVE事件就都交由ViewGroup处理,不再往下传递。
      什么原因?
      • ViewGroup如果没有拦截DOWN事件,且该事件被子view消费,则后续的事件依然会
        走到ViewGroup的dispatchTouchEvent()中,如果没有设置
        requestDisallowInterceptTouchEvent(true)的话,还会走到onInterceptTouchEvent()方法中,最终才传到子view 的dispatchTouchEvent();
      • 所以完全可以在onInterceptTouchEvent中根据某些条件(例如水平滑动距离达到临界值)去中途拦截MOVE事件。
    public boolean onInterceptTouchEvent(MotionEvent event){
    //默认返回false
        return false;
    }
    

    3.2.2 boolean onTouchEvent(MotionEvent event)

    表示是否以及如何消费事件

    • ViewGroup并没有重写该方法,具体见下面的View。
    public boolean onTouchEvent(MotionEvent event) 
    

    3.3 View

    3.3.1 boolean dispatchTouchEvent(MotionEvent event)

    表示如何分发事件,事件首先会传递到该方法。

    • 1 如果给view设置了mOnTouchListener ,且mOnTouchListener.onTouch返回true,则dispatchTouchEvent直接返回true,表示消费了该事件。
    • 2 如果条件1不满足,则会调用onTouchEvent()方法。
    public boolean dispatchTouchEvent(MotionEvent event){
       if(设置了touchListener && touchListener.onTouch()){
       return  true;
       }
       return onTouchEvent();
    }
    

    3.3.2 boolean onTouchEvent(MotionEvent event)

    表示是否以及如何消费事件

    public boolean onTouchEvent(MotionEvent event) {
     // A disabled view that is clickable still consumes the touch  
     // events, it just doesn't respond to them.  
       if(不可用但是clickable){
       return true;
       }
       if(CLICKABLE || LONG_CLICKABLE || CONTEXT_CLICKABLE ){
    //检测到ACTION_UP事件,performClick()中会调用OnClickListener(如果不为空的话)
        performClick();
        return true;
        }
      return false;
    }
    

    4 总结

      1. 默认情况下,滑动某个View,DOWN事件会由自上而下传递。即从Activity传递到ViewGroup、再传递到View。
        如果该View消费了该事件,则DOWN事件以及同一事件序列的其它事件的调用模式一致:
        image.png

      如果该View不消费DOWN事件,则DOWN事件会回传给父控件的dispatchTouchEvent,其中调用onTouchEvent方法。


      image.png
      1. 如果ViewGroup消费了DOWN事件(拦截消费或者回传消费),则后续事件调用模式为:


        image.png

    5 实例讲解

    闲话少说,布局如下:


    image.png
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ffffff"
        tools:context="com.study.test.DispatchActivity">
      <com.study.test.dispatch.ViewOut
            android:layout_width="match_parent"
            android:layout_height="400dp"
            android:background="@color/colorPrimary" >
      <com.study.test.dispatch.ViewIn
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:background="@color/colorAccent"/>
      <com.study.test.dispatch.ViewIn
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:background="#ff0023"
                android:clickable="true"/>
        </com.study.test.dispatch.ViewOut>
    </android.support.constraint.ConstraintLayout>
    

    ViewOut、ViewIn分别继承自ViewGroup与View,复写方法中直接调用父类的对应方法,打印出参数以及函数返回值。

    5.1 滑动蓝色的区域(ViewGroup)

    image.png
    • 1 滑动蓝色区域ViewGroup,则事件只会传到该ViewGroup,不会往下传递(坐标点不在子View上)。
    • 2 DOWN事件过来,VewGroup默认不消费事件,即onTouchEvent返回false,最终没有View消费该DOWN事件。最终Activity的dispatchTouchEvent()会返回true,且本次事件的mFirstTouchTarget 为null。
    • 3 后续MOVE和UP事件,传到Activity,传到DecorView,就会终止向下传递。

    5.2 滑动粉色区域(没有设置clickable的子View)

    image.png
    • 同样没有View消费DOWN事件。

    5.3 滑动红色区域(设置clickable为true的子View)

    image.png
    • 1 DOWN事件过来后,由于该ViewIn为Clickable,则该ViewIn的onTouchEvent会返回true,即默认会消费该事件。
    • 2 后续的MOVE事件,还是会先走到ViewGroup的dispatchTouchEvent()以及onInterceptTouchEvent(),然后走到该View的dispatchTouchEvent()以及onTouchEvent()。

    5.4 滑动红色区域(设置clickable为true的子View),滑动距离大于10时,ViewOut进行拦截

    ViewOut的原有代码为:

        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            Log.w(DispatchActivity.TAG,"ViewOut   onInterceptTouchEvent接收:"+ Utils.getActionString(ev.getAction()));
            boolean flag = super.onInterceptTouchEvent(ev);
            Log.w(DispatchActivity.TAG,"ViewOut   onInterceptTouchEvent返回:"+flag);
            return flag;
        }
    

    修改代码为:

        float mStartX;
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            Log.w(DispatchActivity.TAG, "ViewOut   onInterceptTouchEvent接收:" + Utils.getActionString(ev.getAction()));
            boolean flag = super.onInterceptTouchEvent(ev);
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mStartX = ev.getRawX();
                    break;
                case MotionEvent.ACTION_MOVE:
                    if((ev.getRawX() - mStartX) > 10){
                        flag = true;
                    }
                    break;
                default:
                    break;
            }
            Log.w(DispatchActivity.TAG, "ViewOut   onInterceptTouchEvent返回:" + flag);
            return flag;
        }
    
    image.png

    这里只截取了一部分log。

    • 被拦截的MOVE事件,并没有直接走到ViewGroup的onTouchEvent,而是转化成一个CANCEL事件传递给了子View,并且子View的onTouchEvent返回true。后续的MOVE事件,传到ViewGroup的dispatchTouchEvent()以及onTouchEvent(),不再调用onInterceptTouchEvent()。

    6 tips

    6.1 requestDisallowInterceptTouchEvent的用法

    requestDisallowInterceptTouchEvent为ViewParent接口独有的方法,注意该方法会递归的设置所有祖先的disallowIntercept。

        @Override
        public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
            if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
                // We're already in this state, assume our ancestors are too
                return;
            }
            if (disallowIntercept) {
                mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
            } else {
                mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
            }
            // Pass it up to our parent
            if (mParent != null) {
                mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
            }
        }
    

    6.2 事件冲突处理

    事件冲突一般通过设置事件分发函数的返回值或者设置requestDisallowInterceptTouchEvent(boolean disallowIntercept)这两种方式来处理。

    相关文章

      网友评论

          本文标题:清晰易懂的Android View事件分发 原理及实例 -- 源

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