清晰易懂的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


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)这两种方式来处理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容