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

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


清晰易懂的Android View事件分发 原理及实例 -- 源码伪代码版_第1张图片
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事件以及同一事件序列的其它事件的调用模式一致:
      清晰易懂的Android View事件分发 原理及实例 -- 源码伪代码版_第2张图片
      image.png

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


    清晰易懂的Android View事件分发 原理及实例 -- 源码伪代码版_第3张图片
    image.png
    1. 如果ViewGroup消费了DOWN事件(拦截消费或者回传消费),则后续事件调用模式为:


      清晰易懂的Android View事件分发 原理及实例 -- 源码伪代码版_第4张图片
      image.png

5 实例讲解

闲话少说,布局如下:


清晰易懂的Android View事件分发 原理及实例 -- 源码伪代码版_第5张图片
image.png


  
  
  
    

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

5.1 滑动蓝色的区域(ViewGroup)

清晰易懂的Android View事件分发 原理及实例 -- 源码伪代码版_第6张图片
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)

清晰易懂的Android View事件分发 原理及实例 -- 源码伪代码版_第7张图片
image.png
  • 同样没有View消费DOWN事件。

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

清晰易懂的Android View事件分发 原理及实例 -- 源码伪代码版_第8张图片
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;
    }
清晰易懂的Android View事件分发 原理及实例 -- 源码伪代码版_第9张图片
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事件分发 原理及实例 -- 源码伪代码版)