View 的事件分发

事件分发可以说是 Android 中非常非常重要的知识了, 如果作为一个 Android 开发人员没有了解过事件分发, 那他就不能是一个合格的 Android 程序员, 作为一个
中高级 Android 开发工程师,是必须要深入了解的。学习事件分发主要解决的问题是多层 ViewGroup ,View 嵌套事件冲突, 这篇文章主要分析的是手指的事件, 而
对于其他例如鼠标, 触控笔, 等的事件, 是有略微不同的.

什么是事件分发

在用户界面中, 许多 View 和 ViewGroup 都有需要处理的事件, ViewGroup 中有 View 和 ViewGroup, 这样不断包裹形成层级, 互相重叠. 用户
在对屏幕进行某些特定的动作时, ViewGroup 按一定的顺序获取该动作, 并且可以选择向子 ViewGroup 传递, 或者拦截该动作事件, View 则可以响应, 消费该动作事件.

几个典型涉及事件分发例子, 比如点击一个菜单选项, 或滑动一个列表, 然而列表里可能还有其他列表, 列表外部可能是一个内容很多的容器,该容器也需要滚动,
如何处理滚动的优先级呢? 再比如滑动一个列表而不是点击滑动时手指触动的那个 Button. 这就涉及事件分发了.

事件分发的主角

事件分发的主角即是用户界面 UI, UI 由 View 和 ViewGroup ( ViewGroup 也是 View 的子类) 和 Activity.

View 和 ViewGroup:

  1. 继承于 View 并且实现了 ViewParent, ViewManager 这两个接口的 ViewGroup 及其子类. 例如常用的 LinearLayout, FrameLayout 等都是 ViewGroup 的子类.
  2. 直接或间接继承于 View 为普通控件, 例如 Button, ImageView, TextView.

另一个主角 Activity

在事件分发中, 还有一个主角, 即是 Activity, 首先接收到事件的是 Activity, 由 Activity 交给 Window 处理, 再将事件分发给 decorView, 最后交给我们
的布局了, decorView 为我们 setContentView 的容器, Activity 的根布局, 我们可以通过 Activity.getWindow().getDecorView() 获取.

几个基本概念

MotionEvent

事件分发中,分发的事件就是这个类的实例,这个类用于描述动作事件,保存着移动事件的数据,比如动作,坐标等.
用户每次从手指接触 View 或 ViewGroup 到手指离开这段时间称为一次动作事件,还有非正常情况的 ACANCEL,用户还没抬起手指这个事件被中断时出发,
例如,在一个 RecyclerView 中有一个 Button, 按下这个 Button 的同时往下滑动,Button 的 onTouchEvent 会触发 ACTION_CANCEL.

MotionEvent 的几种动作(Action)类型:

  • MotionEvent.ACTION_DOWN : 按下时触发
  • MotionEvent.ACTION_MOVE : 移动
  • MotionEvent.ACTION_UP : 离开屏幕
  • MotionEvent.ACTION_CANCEL: 非正常取消
  • MotionEvent.ACTION_OUTSIDE: 移动到 View 外

事件序列

用户手指按下屏幕到离开发生的所有事件为一个事件序列, 以 ACTION_DOWN 开始, 以 ACTION_UP 结束, 中间可能还有若干 ACTION_MOVE 事件.

dispatchTouchEvent(MotionEvent event)

Activity , View, ViewGroup 中都有这个方法, View 除 ViewGroup 及其子类外的子类没有这个方法, 这个方法负责派发事件, 返回结果为是否消费该事件,
如果事件能够传递给当前 ViewGroup, 则一定会被调用, 这个方法中会调用下级 View, 或 ViewGroup 的 onTouchEvent, dispatchTouchEvent,
并将下级返回的结果向上返回.

onInterceptTouchEvent

只有 ViewGroup 有这个方法,在 dispatchTouchEvent 中会调用这个方法, 返回 true 则表示拦截该事件, 不会向下传递事件.

onTouchEvent(MotionEvent event)

所有事件分发的参与者都有该方法, 用于处理动作事件, 这个方法会在 dispatchTouchEvent 中调用, 返回结果表示是否消耗当前事件, 如果不消耗, 则在同一个
事件序列中, 这个 View 不会再次收到事件.

分发顺序及规则

典型情况的顺序

我们假设所有 ViewGroup 的 onInterceptTouchEvent 返回都为 false, 即不拦截改事件, 使改事件继续往下分发.

dispatchTouchEvent 的顺序:

Activity  ->    Window(PhoneWindow) -> DecorView    ->  ViewGroup   ->  View

所有情况下, Activity 总是第一时间获取到事件, 然后往下分发, 在 View, ViewGroup 中即不拦截(即 onInterceptTouchEvent 返回 false), 也不消费( onTouchEvent 返回false)
的情况下, 事件将通过 onTouchEvent 往回传递, 直到 Activity.
这里省略了 Window 和 DecorView:

Activity ---dispatchTouchEvent--->  ViewGroup ---dispatchTouchEvent---onInterceptTouchEvent---> View    
                                                                                                  |
                                     Activity  <---onTouchEvent---  ViewGroup <---onTouchEvent---

ViewGroup 中的 dispatchTouchEvent:

拦截部分代码片段:

...
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}
// 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;
}
...

从上代码片段中可知, 首先会进行手势检测, 如果在一个事件序列发生的中途出现手势检测, 则在这之前的事件都会被重置和取消, 然后, 将会判断是否拦截该事件,
mFirstTouchTarget 为下一个需要派发事件的 View, 如果他为空, 表示事件不交给子 View 处理, 则 intercepted.

继续, 往下分发事件的代码 ...

if (!canceled && !intercepted) {
    ...
    if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        ...
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            // Child wants to receive touch within its bounds.
            mLastTouchDownTime = ev.getDownTime();
            if (preorderedList != null) {
                // childIndex points into presorted list, find original index
                for (int j = 0; j < childrenCount; j++) {
                    if (children[childIndex] == mChildren[j]) {
                        mLastTouchDownIndex = j;
                        break;
                    }
                }
            } else {
                mLastTouchDownIndex = childIndex;
            }
            mLastTouchDownX = ev.getX();
            mLastTouchDownY = ev.getY();
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }
        ...
    }
}
 // Dispatch to touch targets.
if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else {
    // 此处为遍历 View 分发事件的代码
}
...

当事件没有被取消, 也没有被拦截, 并且事件为 DOWN, 则继续往子 View 派发.

ViewGroup 中向子 View 分发事件主要是在 dispatchTransformedTouchEvent 中进行的, 在这个方法中会调用子 view 的 dispatchTouchEvent.

常见顺序概要

只要上层 ViewGroup 将事件分发下来的没有被拦截, 则 dispatchTouchEvent 一定会被调用, 在 dispatchTouchEvent 中, 首先会调用 onInterceptTouchEvent
判断是否拦截该事件, 如果拦截, 则不再往下分发, 这个事件就交给这个 ViewGroup处理, 执行 onTouchEvent, 那么它的下级将无法收到任何事件. 否则继续往下分发,
分发到最底层的 ViewGroup,和 View 时, onTouchEvent 将从最底层依次往上层被调用, 如果其中有某个 View 或 ViewGroup 的 onTouchEvent 方法返回了 true,
则该 View 将消费该事件, 更上层的 View 和 ViewGroup 将无法接收到该事件, 并且, 消费了该事件的 View 或 ViewGroup 的子 View 将无法收到该事件序列
ACTION_DOWN 后剩余的事件.

Activity 中的 dispatchTouchEvent

以下是 Activity 中的分发, getWindow().superDispatchTouchEvent(ev), 由此可见, Activity 将 事件分发给 Window.

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

设置了 onTouchListener 或 onClickListener

onTouchListener 将会在 onTouchEvent 前调用, 如果 onTouchListener 中的 onTouch 返回了 true, 则 onTouchEvent 不会调用.

在 View 的 dispatchTouchEvent 中, 我们可以找到以下片段, li.mOnTouchListener.onTouch(this, event) 返回 true, 则 result 赋值 true, onTouchEvent 将不会执行:

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;  
        }
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    ...
}

而 onClickListener 的 onClick 方法则会在 onTouchEvent 调用, 可见 onClickListener 的优先级低于 onTouchListener.

其他情况

多个 View 重叠, 比如在 FrameLayout 中, 则在布局文件最下面的 View 优先级比较高.

总结和其他

  • 只有在 clickable 和 longClickable 都为 false 的情况, View 才无法接收事件

https://djh.red/blog/article/24/
(完)

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

推荐阅读更多精彩内容