前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android开发艺术笔记 | View的事件分发机制原理详析与源码分析(ing)

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

作者头像
凌川江雪
发布2020-04-01 17:47:32
9750
发布2020-04-01 17:47:32
举报
文章被收录于专栏:李蔚蓬的专栏

原理解析

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

分发与拦截

  • 点击事件的分发过程**由三个重要方法共同完成:**dispatchTouchEvent**、**onInterceptTouchEvent**和**onTouchEvent**。**
public boolean dispatchTouchEvent(MotionEvent ev)
  • 用来进行事件的分发传递。
  • 如果事件能够传递给当前View,那么此方法一定会被调用,
  • 返回值是boolean类型, 返回结果受**当前View**的**onTouchEvent** 和**下级View**的**dispatchTouchEvent**方法的影响;
  • 表示是否消耗当前事件。
public boolean onInterceptTouchEvent(MotionEvent event)
  • dispatchTouchEvent()内部调用,用来判断是否拦截某个事件;
  • 如果当前View**拦截**了某个事件,那么在**同一个事件序列**当中, 此方法不会**被再次调用**,
  • 返回结果表示**是否拦截当前事件**。

  • 该方法只在ViewGroup中有,View(不包含 ViewGroup)是没有的。
  • 一旦拦截, 则执行ViewGroup的onTouchEvent, 在ViewGroup中处理事件,而不接着分发给View。
  • 且只调用一次,所以后面的事件都会交给ViewGroup处理。
public boolean onTouchEvent(MotionEvent event)
  • 同样在**dispatchTouchEvent**方法中调用,用来**处理点击事件**;
  • 返回结果表示**是否消耗当前事件**,
  • 如果**不消耗**,则在**同一个事件序列**中, 当前View无法**再次接收**到事件。
  • 上述三个方法的区别与关系,可以用如下伪代码表示:
代码语言:javascript
复制
    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**的**返回值**,
代码语言:txt
复制
- **如果返回****`false`****,【事件不消费,继续往下传递】** 

【注意这里跟**onInterceptTouchEvent**不一样,

onInterceptTouchEvent**仅在ViewGroup级,**

true表拦截处理,调用**ViewGroup**自身的**onTouch()**-->**onTouchEvent()**,

onTouch**在View级时候,**

false**表**继续流程**,调用**View**自身的**onTouchEvent()**】**

代码语言:txt
复制
- **如果返回****`true`****,【事件被消费】** 

优先级:**onTouch()**-->**onTouchEvent()** -->**performClick()** --> onClick()以上是事件处理方法的优先级顺序,按照这个顺序, 只要**排在前面**的**事件方法**返回**true**,**消耗处理**了**点击事件**了, 点击事件**便就地结束,不再下发,** 排在后面**的**点击事件**也就不会再被调用和响应了;** 【文末有实例】另, onTouch()的实现需要实现onTouchListeneronTouchEvent()/performClick()直接在自定义View文件中重写即可; onClick()的实现需要实现onClick

  • 当一个点击事件产生后, 其传递过程顺序:**Activity -> Window -> 顶级View**(上述说的表示View层中的顺序);
  • 顶级View接收到事件后,就会按照事件分发机制去分发事件。
  • 如果一个View的**onTouchEvent**返回**false**, 那么它的父容器的**onTouchEvent**将会被调用, 依此类推。 【除非下往上回传到某个返回true的onTouchEvent(), 则在那里停止,否则——】
  • 如果所有的元素都不处理这个事件, 那么这个事件将会最终传递给**Activity**处理, 即**Activity**的**onTouchEvent**方法会被调用。

  • 形象地举个例子, 假如点击事件是一个难题, 这个难题最终被上级领导分给了一个程序员去处理(类似事件分发过程), 结果这个程序员搞不定(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**)!!!!!!!

除非它是**不可点击**的(**clickable** 和**longClickable**同时为**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**是可点击的,并且它收到了**down**和**up**的事件。

(11)【由外而内;以下犯上】

事件传递过程是**由外向内**的,

即事件总是先传递给**父元素**,然后再由**父元素**分发给**子View**,

通过**requestDisallowInterceptTouchEvent**方法可以在**子元素**中**干预父元素**的**事件分发**过程,但是**ACTION_DOWN**事件除外。

稍微复习一下: 事件方法的优先级:**onTouch()**-->**onTouchEvent()** -->**performClick()** --> onClick()以上是事件处理方法的优先级顺序,按照这个顺序, 只要**排在前面**的**事件方法**返回**true**,**消耗处理**了**点击事件**了, 点击事件**便就地结束,不再下发,** 排在后面**的**点击事件**也就不会再被调用和响应了;** 下面是关于**事件优先级**的一个实例:

代码语言:javascript
复制
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**; 这里可以看一下文末的两篇博客!
  1. 事件的处理则是分发的“回溯”,!!!!! 顺序与分发相反,是一个**从下到上**的过程, 从**最下层的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开始,源码:
代码语言:javascript
复制
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

如上,

首先事件开始交给**Activity**所附属的**Window**进行**分发**,如果返回**true**,

整个事件循环就结束了:

代码语言:javascript
复制
if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }

返回**false**意味着事件没有元素处理,

所有View的**onTouchEvent**都返回了false,

那么Activity的onTouchEvent就会被调用。

代码语言:javascript
复制
return onTouchEvent(ev);
  • 接下来看Window是如何将事件传递给ViewGroup的; Window**是个**抽象类**!!!** 而**Window**的**superDispatchTouchEvent**方法也是个**抽象方法**!!! 因此我们必须找到**Window的实现类**才行。源码: public abstract boolean superDispatchTouchEvent(MotionEvent event);
  • Window的实现类其实是**PhoneWindow**, 这一点从Window的源码中有这么一段话:
代码语言:javascript
复制
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.
  • 大概是说,
代码语言:txt
复制
- **`Window类`****可以控制****`顶级View`****的****`外观`****和****`行为策略`****!!!**
- **它的****`唯一实现`****位于****`android.policy.PhoneWindow`****中!!!**
- **当你要****`实例化`****这个****`Window类`****的时候,** 
代码语言:javascript
复制
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
  • 可以清楚看到, PhoneWindow**将事件直接传递给了**DecorView**!!!!!!!!!!**
  • DecorView**是什么:**
代码语言:javascript
复制
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)以后,** 会调用**ViewGroup**的**dispatchTouchEvent**方法, 然后, 如果**顶级ViewGroup**拦截事件即**onInterceptTouchEvent**返回true, 则事件由ViewGroup处理, 如果ViewGroup的mOnTouchListener被设置则**onTouch**会被调用, 否则**onTouchEvent**会被调用。 如果都提供的话,onTouch会屏蔽掉onTouchEvent。
  • 在onTouchEvent中,如果设置了mOnClickListener,则onClick会被调用!!!!!!!!! 如果顶级ViewGroup不拦截事件, 则事件会传递给它所在的点击事件链上的子View, 这时**子View**的**dispatchTouchEvent**会被调用。 到此,事件已经从顶级View传递给了下一层View,接下来的传递过程和顶级View是一致的,如此循环,完成整个事件的分发。

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

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

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

代码语言:javascript
复制
    // 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事件的处理。

...


参考:

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 原理解析
  • 分发与拦截
  • 事件处理
  • 小结
  • 源码分析
    • Activity对点击事件的分发过程
      • 顶级View对点击事件的分发过程
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档