Android开发艺术笔记 | View的事件分发机制原理详析与源码分析(ing)

原理解析

  • 这里要分析的对象就是MotionEvent,即点击事件
    点击事件事件分发,本质是对MotionEvent事件分发过程
    即,
    当一个MotionEvent产生了以后,
    系统需要把这个事件传递给一个具体的View
    而这个传递的过程就是分发过程

分发与拦截

  • 点击事件的分发过程由三个重要方法共同完成:dispatchTouchEventonInterceptTouchEventonTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev)

  • 用来进行事件的分发传递。
  • 如果事件能够传递给当前View,那么此方法一定会被调用,
  • 返回值是boolean类型,
    返回结果受当前ViewonTouchEvent
    下级ViewdispatchTouchEvent方法的影响;
  • 表示是否消耗当前事件。

public boolean onInterceptTouchEvent(MotionEvent event)

  • dispatchTouchEvent()内部调用,用来判断是否拦截某个事件;
  • 如果当前View拦截了某个事件,那么在同一个事件序列当中,
    此方法不会被再次调用
  • 返回结果表示是否拦截当前事件
  • 该方法只在ViewGroup中有,View(不包含 ViewGroup)是没有的。
  • 一旦拦截,
    则执行ViewGroup的onTouchEvent,
    在ViewGroup中处理事件,而不接着分发给View。
  • 且只调用一次,所以后面的事件都会交给ViewGroup处理。

public boolean onTouchEvent(MotionEvent event)

  • 同样在dispatchTouchEvent方法中调用,用来处理点击事件

  • 返回结果表示是否消耗当前事件

  • 如果不消耗,则在同一个事件序列中,
    当前View无法再次接收到事件。

  • 上述三个方法的区别与关系,可以用如下伪代码表示:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;
        if (onInterceptTouchEvent(ev)) {
                consume = onTouchEvent(ev);
        } else {
                consume = child.dispatchTouchEvent(ev);
        }
        return consume;
    }
  • 通过以上伪代码,可以大致了解点击事件在View层传递规则
    • 对于一个根ViewGroup来说,
      点击事件产生后,首先会传递给它,
      这时其dispatchTouchEvent会被调用;

    • 如果这个ViewGroup的onInterceptTouchEvent方法
      返回true就表示它要拦截当前事件,
      接着事件就会交给这个ViewGroup处理,
      即它的onTouchEvent方法就会被调用;!!!

    • 如果这个ViewGroup的onInterceptTouchEvent方法
      返回false就表示它不拦截当前事件,
      这时当前事件就会继续传递给它的子元素
      接着子元素dispatchTouchEvent方法就会被调用
      如此反复直到事件被最终处理。

  • 即,
    接收到事件 --> 分发 --> 是否拦截
    --> 拦截则就地处理【ViewGroup/View:调用自身onTouch()-->onTouchEvent() -->performClick() --> onClick()】!!!,
    否则继续往下传!

这里可以看一下文末的两篇博客!

事件处理

  • 当一个View需要处理事件时,
    如果它设置了OnTouchListener
    OnTouchListener中的onTouch方法会被回调;

  • 这时事件如何处理还要看onTouch返回值

    • 如果返回false,【事件不消费,继续往下传递】
      当前ViewonTouchEvent方法会被调用,
      接着是performClick() --> onClick()被调用;
      然后
      它的父容器的onTouchEvent将会被调用,
      依此类推。

      【注意这里跟onInterceptTouchEvent不一样,
      onInterceptTouchEvent仅在ViewGroup级,
      true表拦截处理,调用ViewGroup自身的onTouch()-->onTouchEvent()
      onTouch在View级时候,
      false继续流程,调用View自身的onTouchEvent()

    • 如果返回true,【事件被消费】
      那么onTouchEvent方法将不会被调用。

  • 由此可见,
    给View设置的OnTouchListener,其优先级比onTouchEvent要高。
    onTouchEvent方法中,
    如果当前设置的有OnClickListener,那么它的onClick方法会被调用。
    而常用的OnClickListener,其优先级最低,即处于事件传递的尾端。

优先级:onTouch()-->onTouchEvent() -->performClick() --> onClick()

以上是事件处理方法的优先级顺序,按照这个顺序,
只要排在前面事件方法返回true消耗处理点击事件了,
点击事件便就地结束,不再下发,
排在后面点击事件也就不会再被调用和响应了;
【文末有实例】


另,
onTouch()的实现需要实现onTouchListener
onTouchEvent()/performClick()直接在自定义View文件中重写即可;
onClick()的实现需要实现onClick

  • 当一个点击事件产生后,
    其传递过程顺序:Activity -> Window -> 顶级View(上述说的表示View层中的顺序);

  • 顶级View接收到事件后,就会按照事件分发机制去分发事件。

  • 如果一个View的onTouchEvent返回false
    那么它的父容器的onTouchEvent将会被调用,
    依此类推。
    【除非下往上回传到某个返回true的onTouchEvent(),
    则在那里停止,否则——】

  • 如果所有的元素都不处理这个事件,
    那么这个事件将会最终传递给Activity处理,
    ActivityonTouchEvent方法会被调用。

  • 形象地举个例子,
    假如点击事件是一个难题,
    这个难题最终被上级领导分给了一个程序员去处理(类似事件分发过程),
    结果这个程序员搞不定(onTouchEvent返回了false),
    但难题必须要解决,
    那只能交给水平更高的上级解决(上级的onTouchEvent被调用),
    如果上级再搞不定,那只能交给上级的上级去解决,
    就这样将难题一层层地向上抛。
    【即一个从上到下(分发传递),再从下到上的过程(onTouchEvent(),
    例见事件拦截机制大概流程(Android群英传)中的图例】

关于事件传递机制的一些结论(每一个点前面的短语是一个笔者自提的概况中心,便于记忆)

根据它们可以更好地理解整个传递机制:
(1)【事件序列,定义】
同一个事件序列” 的定义:
指从手指接触屏幕的那一刻
到手指离开屏幕的那一刻结束
在这个过程中所产生的一系列事件,
这个事件序列以down事件开始,
中间含有数量不定move事件,
最终以up事件结束。

(2)【处理事件,独一无二】
正常情况下,一个事件序列只能被一个View拦截且消耗!!!
这一条的原因可以参考(3),
因为一旦一个元素拦截了某此事件,
那么同一个事件序列内所有事件都会直接交给它处理!!!
因此同一个事件序列中事件不能分别由两个View同时处理!!!
除非,
将本该由某个View自己处理的事件
通过onTouchEvent强行传递给其他View处理。


(3)【事件序列,从一而终】
某个View一旦决定拦截,则这一个事件序列都只能由它来处理
(如果事件序列能够传递给它的话),
并且它的onInterceptTouchEvent不会再被调用!!!
当一个View决定拦截一个事件后,
那么系统会把同一个事件序列内其他方法都直接交给它来处理,
因此
就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。

(4)【短期失信】
某个View一旦开始处理事件
如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),
那么同一事件序列中其他事件都不会再交给它来处理,
【即,View放弃处理ACTION_DOWN,便放弃了整个事件序列!!!】
并且事件将重新交由它的父元素去处理,
即父元素的onTouchEvent会被调用。【事件向上“回传”】
即,
事件一旦交给一个View处理,那么它就必须消耗掉!!!
否则同一事件序列中剩下的事件就不再交给它来处理了!!!
好比上级交给程序员一件事,如果这件事没有处理好,
短期内上级就不敢再把事情交给这个程序员做。

(5)【余粮上缴】
如果View不消耗除ACTION_DOWN以外的其他事件,
那么这个点击事件会消失,
此时父元素的onTouchEvent并不会被调用,
并且当前View可以持续收到后续的事件,
最终这些消失的点击事件会传递给Activity处理。

(6)ViewGroup默认不拦截任何事件。
Android源码中
ViewGroup的onInterceptTouch-Event方法默认返回false

(7)View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。

(8)View的onTouchEvent默认都会消耗事件(返回true)!!!!!!!
除非它是不可点击的(clickablelongClickable同时为false)。
View的longClickable属性默认都为false
clickable属性要分情况,
比如Button的clickable属性默认为true
TextView的clickable属性默认为false

(9)【enable无用,clickable居上】
View的enable属性不影响onTouchEvent默认返回值。哪怕一个View是disable状态的!!!!!
只要它的clickable或者longClickable有一个为true
那么它的onTouchEvent就返回true!!!

(10)onClick会发生的前提是当前View是可点击的,并且它收到了downup的事件。

(11)【由外而内;以下犯上】
事件传递过程是由外向内的,
即事件总是先传递给父元素,然后再由父元素分发给子View
通过requestDisallowInterceptTouchEvent方法可以在子元素干预父元素事件分发过程,但是ACTION_DOWN事件除外。

稍微复习一下:
事件方法的优先级:onTouch()-->onTouchEvent() -->performClick() --> onClick()


以上是事件处理方法的优先级顺序,按照这个顺序,
只要排在前面事件方法返回true消耗处理点击事件了,
点击事件便就地结束,不再下发,
排在后面点击事件也就不会再被调用和响应了;



下面是关于事件优先级的一个实例:

public class DragView3 extends View implements View.OnClickListener {

    private int lastX;
    private int lastY;

    public DragView3(Context context) {
        super(context);
        ininView();
    }

    public DragView3(Context context, AttributeSet attrs) {
        super(context, attrs);
        ininView();
    }

    public DragView3(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        ininView();
    }

    private void ininView() {
        setBackgroundColor(Color.BLUE);
        this.setOnClickListener(this);//测试onTouchEvent与onClick的优先级!!
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 记录触摸点坐标
                lastX = (int) event.getX();
                lastY = (int) event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                // 计算偏移量
                int offsetX = x - lastX;
                int offsetY = y - lastY;

                ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
//                LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft() + offsetX;
                layoutParams.topMargin = getTop() + offsetY;
                setLayoutParams(layoutParams);
                break;
        }
        return true;
    }

    //测试onTouchEvent与onClick的优先级!!
    @Override
    public void onClick(View v) {
        setBackgroundColor(Color.RED);
    }
}
  • 如上代码,
    • 给自定义View配置了onClick监听器
      如果onClick响应,点击View之后会从蓝色变成红色
      但是运行之后我们发现并没有变色,即onClick没有被调用;
      View响应的只是onTouchEvent中的滑动逻辑而已。
      (下面图一)

    • 这是因为onTouchEvent返回true,把事件消耗掉了!!
      于是事件在onTouchEvent处理结束,不再往下传,传不到onClick那里!!!

    • 如果,
      将以上代码中的onTouchEvent注释掉,
      使之默认返回false,不消耗事件,这时onClick则会响应!
      那么再次运行程序,可以发现点击View之后,
      View从蓝色变成红色!!!
      (下面图二)

  • 由此,事件处理方法优先级不言而喻!
    图一
    图二

小结

  1. 三个关键方法:dispatchTouchEventonInterceptTouchEventonTouchEvent;分别的作用和关系;
  2. 分发与拦截,是一个依据分发顺序从上往下的过程!!!!!
    逻辑骨架就是,
    接收到事件 --> 分发 --> 是否拦截
    --> 拦截则就地处理【ViewGroup/View:调用自身onTouch()-->onTouchEvent() -->performClick() --> onClick()】!!!,
    否则继续往下传,传到最下层的View为止,接着进入处理过程!
    分发的顺序是Activity -> Window(PhoneWindow) -> DecorView -> 顶级View(上述说的表示View层中的顺序) -> ViewGroup -> View

    这里可以看一下文末的两篇博客!
  3. 事件的处理则是分发的“回溯”,!!!!!
    顺序与分发相反,是一个从下到上的过程,
    最下层的View开始到最上层(即Activity),
    如果所有元素都不消耗这个事件,事件最终就传回Activity;
    消耗指onTouch、onTouchEvent、onClick等;









源码分析

  • 上面说了,
    Android事件分发流程: Activity -> ViewGroup -> View;

  • 所以,想充分理解Android分发机制,本质上是要理解:
  1. Activity对点击事件的分发过程
  2. ViewGroup对点击事件的分发过程
  3. View对点击事件的分发过程

Activity对点击事件的分发过程

  • 点击事件MotionEvent来表示,
    当一个点击操作发生时,事件最先传递给当前Activity,
    由Activity的dispatchTouchEvent来进行事件派发,
    具体的工作是由Activity内部Window来完成的!!!!!!!!

  • Window会将事件传递给decor view
    decor view一般就是当前界面的底层容器(即setContentView所设置的View的父容器),
    通过Activity.getWindow.getDecorView()可以获得。

  • 先从Activity的dispatchTouchEvent开始,源码:

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

如上,
首先事件开始交给Activity所附属的Window进行分发,如果返回true
整个事件循环就结束了:

if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }

返回false意味着事件没有元素处理,
所有View的onTouchEvent都返回了false,
那么Activity的onTouchEvent就会被调用。

return onTouchEvent(ev);
  • 接下来看Window是如何将事件传递给ViewGroup的;
    Window是个抽象类!!!
    WindowsuperDispatchTouchEvent方法也是个抽象方法!!!
    因此我们必须找到Window的实现类才行。源码:
    public abstract boolean superDispatchTouchEvent(MotionEvent event);

  • Window的实现类其实是PhoneWindow
    这一点从Window的源码中有这么一段话:

Abstract base class for a top-level window look and behavior policy. 
An instance of this class should be used as the top-level view added to 
the window manager. It provides standard UI policies such as a background, title area, 
default key processing, etc.
The only existing implementation of this abstract class is android. policy. 
PhoneWindow,which you should instantiate when needing a Window. 
Eventually that class will be refactored and a factory method added for creating 
Window instances without knowing about a particular implementation.
  • 大概是说,

    • Window类可以控制顶级View外观行为策略!!!
    • 它的唯一实现位于android.policy.PhoneWindow中!!!
    • 当你要实例化这个Window类的时候,
      你并不知道它的节,因为这个类会被重构
      只有一个工厂方法可以使用。
  • 所以可以看下android.policy.PhoneWindow
    尽管实例化的时候此类会被重构,仅是重构而已,功能是类似的。

  • 由于Window的唯一实现是PhoneWindow
    接下来看PhoneWindow是如何处理点击事件的,PhoneWindow.superDispatchTouchEvent源码:

    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
  • 可以清楚看到,
    PhoneWindow将事件直接传递给了DecorView!!!!!!!!!!

  • DecorView是什么:

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
    // This is the top-level view of the window,containing the window decor.

    private DecorView mDecor;
    @Override
public final View getDecorView() {
        if (mDecor == null) {
                installDecor();
        }
        return mDecor;
    }
  • 通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)可以获取Activity所设置的View!!!!!!!!
    这个mDecor就是getWindow().getDecorView()返回的View!!!
    而通过setContentView设置的View是它(DecorView mDecor)的一个子View【所谓顶级View】!!!

  • 至此,事件传递到了DecorView这儿,
    由于DecorView继承自FrameLayout且是父View
    所以最终事件会传递给View!!!
    从而应用响应点击事件!!

  • 从这里开始,
    事件已经传递到顶级View了,

    Activity中通过setContentView所设置的View
    另外顶级View也叫根View
    顶级View一般都是ViewGroup

顶级View对点击事件的分发过程

  • 点击事件达到顶级View(一般是一个ViewGroup)以后,
    会调用ViewGroupdispatchTouchEvent方法,
    然后,
    如果顶级ViewGroup拦截事件即onInterceptTouchEvent返回true,
    则事件由ViewGroup处理,
    如果ViewGroup的mOnTouchListener被设置则onTouch会被调用,
    否则onTouchEvent会被调用。
    如果都提供的话,onTouch会屏蔽掉onTouchEvent。

  • 在onTouchEvent中,如果设置了mOnClickListener,则onClick会被调用!!!!!!!!!
    如果顶级ViewGroup不拦截事件,
    则事件会传递给它所在的点击事件链上的子View,
    这时子ViewdispatchTouchEvent会被调用。
    到此,事件已经从顶级View传递给了下一层View,接下来的传递过程和顶级View是一致的,如此循环,完成整个事件的分发。

以上是对原理部分的回顾;
下面开始顶级View的源码分析;

  • ViewGroup对点击事件的分发过程,
    其主要实现在ViewGroup的dispatchTouch-Event方法中,
    这个方法比较长,这里分段说明。

首先下面一段,描述当前View是否拦截点击事情这个逻辑。

    // 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;
    }
  • 如上,
    • ViewGroup在如下两种情况下会判断是否要拦截当前事件:
      事件类型为ACTION_DOWN或者mFirstTouchTarget != null
      ACTION_DOWN事件好理解,那么mFirstTouchTarget != null是什么?

    • 这个从后面的代码逻辑可以看出来,
      当事件由ViewGroup的子元素成功处理时,
      mFirstTouchTarget会被赋值并指向子元素【于是 != null】,
      换种方式来说,
      当ViewGroup【不拦截事件并将事件交由子元素处理
      mFirstTouchTarget != null】。
      反过来,
      一旦事件由当前ViewGroup拦截时,
      mFirstTouchTarget != null就不成立。

    • 那么当ACTION_MOVE和ACTION_UP事件到来时,由于(actionMasked == MotionEvent. ACTION_DOWN || mFirstTouchTarget != null)这个条件为false,将导致ViewGroup的onInterceptTouchEvent不会再被调用,并且同一序列中的其他事件都会默认交给它处理。
      当然,这里有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件。为什么说是除了ACTION_DOWN以外的其他事件呢?这是因为ViewGroup在分发事件时,如果是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT这个标记位,将导致子View中设置的这个标记位无效。因此,当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件,这一点从源码中也可以看出来。在下面的代码中,ViewGroup会在ACTION_DOWN事件到来时做重置状态的操作,而在resetTouchState方法中会对FLAG_DISALLOW_INTERCEPT进行重置,因此子View调用request-DisallowInterceptTouchEvent方法并不能影响ViewGroup对ACTION_DOWN事件的处理。

...


参考:

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

推荐阅读更多精彩内容