综合开发

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

微信扫一扫,分享到朋友圈

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 自己处理的事件

(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 事件除外。

稍微复习一下:

以上是事件处理方法的优先级顺序,按照这个顺序,

只要 排在前面事件方法 返回 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分发机制,本质上是要理解:
Activity
ViewGroup
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事件的处理。

参考:

Entry does not add class when it is concentrated

上一篇

看完这篇,一起为CSS技能充电

下一篇

你也可能喜欢

评论已经被关闭。

插入图片

热门栏目

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

长按储存图像,分享给朋友