Denua 博客

View 的事件分发

发布时间: 2019-02-25 14:22   分类 : Android    标签: Android View 浏览: 2290   

事件分发可以说是 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 才无法接收事件

(完)

评论    

Copyright denua denua.cn