【Android】View 事件分发机制与源码解析

View 的基本知识

View 的位置参数

按坐标系的不同分别有

  1. top、bottom、left 和 right 是相对父布局坐标系的偏移量。
  2. translationX 和 translationY 是属性动画系统相对父布局坐标系平移的额外偏移量,默认值为 0。
  3. mScrollX 和 mScrollY 是内容相对控件边界滑动的偏移量,默认值也为 0。
  4. x = left + translationX,y = top + translationY,二者是 View 的实际显示位置,首项的 4 个值是 final 值,逻辑处理按照 x 和 y 的值为基准。
  5. getLocationOnScreen(int[])getLocationInWindow(int[]),通过上述方法可以获得 View 在屏幕显示区域的绝对坐标和相对当前 Window 的坐标。

MotionEvent 和 TouchSlop

MotionEvent 是用户手指接触屏幕后产生的事件,分别有

  1. ACTION_DOWN,手指接触屏幕的瞬间
  2. ACTION_MOVE,手指在屏幕上移动
  3. ACTION_UP,手指离开屏幕的瞬间

通过 MotionEvent 的 getX 和 getRawX 方法可以获得此次点击事件相对接受事件的当前 View 和在屏幕显示区域的横轴绝对坐标,纵轴同理。

TouchSlop 表示滑动事件的最小认定距离,可以通过 ViewConfiguration.get(getContext()).getScaledTouchSlop() 获得该常量,在滑动控件内细心判别 TouchSlop 常量值可以有更好的用户体验

VelocityTracker 和 GestureDetector

VelocityTracker 速度追踪,用来追踪手指在滑动过程中的速度,包括水平和竖直方向的速度,使用方法是获取 VelocityTracker 实例后通过在每次事件回调中 addMovement 形成完整的事件序列,实例内部会计算相应速度,以及 VelocityTracker 是支持多指触控的速度识别的,系统为同时在屏幕发生 MotionEvent 的手指分配了 pointerId 编号,在 getXVelocity 等速度获取方法传入其编号即可获得速度信息

java 复制代码
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);

velocityTracker.computeCurrentVelocityTracker(units);
int vx = (int) velocityTracker.getXVelocity();
int vy = (int) velocityTracker.getYVelocity();

velocityTracker.clear();
velocityTracker.recycle();

GestureDetector 手势检测,首先要创建 GestureDetector 对象并实现 OnGestureListener 接口,根据实际业务也可以实现 OnDoubleTapListener 从而监听双击行为,可以在 View 的 onTouchListener 等方法调用 GestureDetector 的 onTouchEvent 方法来监听各种回调方法

java 复制代码
GestureDetector gestureDetector = new GestureDetector(context, new GestureDetector.OnGestureListener() {
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        Log.d("Gesture", "Scroll dx=" + distanceX + " dy=" + distanceY);
        return true;
    }
}); // distanceX/Y 是事件序列两次移动的距离差

View 的事件分发机制

点击事件的传递规则

事件分发过程由 3 个重要的方法共同完成,分别是 dispatchTouchEvent 分发事件、onInterceptTouchEvent 拦截事件和 onTouchEvent 处理事件,伪代码逻辑是

java 复制代码
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if (onInterceptTouchEvent(ev)) { // 控件可以拦截事件则调用控件的 onTouchEvent 处理事件
        consume = onTouchEvent(ev);
    } else {
        consume = child.dispatchTouchEvent(ev); // 传递给子控件分发
    }
    
    return consume; // 表示事件是否被消费
}

当 View 回调 onTouchEvent 方法开始处理事件时,如果其实现 OnTouchListener,则其中的 onTouch 方法会回调,如果该方法最终返回 false,则会回调当前 View 的 OnTouchEvent,在 onTouchEvent 方法中如果设置有 OnClickListener,则会回调其 onClick 方法。

点击事件在 Java 层的传递过程是 Activity -> Window -> DecorView,其中 Window 是位于 Activity 和屏幕内容之间起到桥梁作用的层抽象层级,管理 1 块可绘制区域的容器,其唯一实现类是 PhoneWindow。每个 Window 都有 1 个根视图实例 DecorView,DecorView 继承自 FrameLayout,Activity 调用 setContentView 方法时,传入的布局实例会添加到 DecorView 中,是 DecorView 的子控件。

如果上述分发代码的 consume 实例始终 false 未得到消费,则事件会最终传递给 Activity 处理,结合 View 树来看类似于递归的自底而上收束。

Android 开发艺术探索书中,对事件分发机制有一些结论,其分别是

  1. 1 个事件序列是 1 根手指接触屏幕 MotionEvent.ACTION_DOWN 到离开屏幕 MotionEvent.ACTION_UP 或事件取消 MotionEvent.ACTION_CANCEL 的过程。
  2. 1 个事件序列只能被 1 个 ViewGroup 拦截并处理,ViewGroup 拦截事件后,该事件所在序列的所有事件都会直接交给该实例处理,且 ViewGroup 的 onInterceptTouchEvent 方法不会再次被调用。
  3. View 开始处理事件时如果 onTouchEvent 对 MotionEvent.ACTION_DOWN 事件返回 false,则同一序列的其他事件都不会再交给该 View 处理,且会重新交给其父元素处理。
  4. View 只消耗 MotionEvent.ACTION_DOWN 事件时,该点击事件会消失,父元素和 Activity 无法接受处理,因为 Android 的点击事件中,MotionEvent.ACTION_DOWN 决定该序列的事件归属,如果 DOWN 被 View 消费,则后续的 MOVE 和 UP 都只会发给该 View。
  5. ViewGroup 默认不拦截任何事件,且只有 ViewGroup 能够拦截事件。
  6. View 的 onTouchEvent 默认消耗事件,除非该 View 不可点击,点击属性分为 clickable 和 longClickable,后者属性在控件中几乎都默认为 false,前者则会视情况而定。

所以,分发链 Activity -> Window -> ViewTree 中,优先寻找愿意消费事件序列 DOWN 事件的子元素,如果子元素没有愿意消费 DOWN 的则会向上查找父元素,事件序列可以被父元素提前拦截。

事件分发的源码分析

首先看 Activity 对点击事件的分发过程

java 复制代码
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction(); // 用户进行了操作
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

Activity 的 dispatchTouchEvent 方法传递到 PhoneWindow 的 superDispatchTouchEvent 方法,紧接着直接传递给 DecorView,DecorView 也会直接传递给父类 FrameLayout,其是 ViewGroup,所以最后会进入 ViewGroup 的 dispatchTouchEvent 方法

java 复制代码
// PhoneWindow
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

// DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event); // 即FrameLayout
}
java 复制代码
// ViewGroup.dispatchTouchEvent

// 1. 清除残留状态
if (actionMasked == MotionEvent.ACTION_DOWN) {
    cancelAndClearTouchTarget(ev);
    resetTouchState();
}

// 2. 拦截决策
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCELT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action);
    } else {
        intercepted = false;
    }
} else {
    intercepted = true;
}

// 3. 事件分发
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
    final int childIndex = customOrder
            ? getChildDrawingOrder(childrenCount, i) : i;
    final View child = (preorderedList == null)
            ? children[childIndex] : preorderedList.get(childIndex);
    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        continue;
    }
    
    newTouchTarget = getTouchTarget(child);
    if (newTouchTarget != null) {
        newTouchTarger.pointerIdBits |= idBitsToAssign;
        break;
    }
    
    resetCancelNextUpFlag(child);
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        mLastTouchDownTime = ev.getDownTime();
        if (preorderedList != null) {
            for (int j = 0; j < childrenCount; j++) {
                if (children[childrenIndex] == mChildren[j]) {
                    mLastTouchDownIndex = j;
                    break;
                }
            }
        } else {
            mLastTouchDownIndex = childIndex;
        }
        mLastTouchDownX = ev.getX();
        mLastTouchDownY = ev.getY();
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
    }
}

// 4. 自己处理
if (mFirstTouchTarget == null) {
    handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
}

我们把 ViewGroup.dispatchTouchEvent 方法分成 4 个部分,注释 1 处的源码含义是,ViewGroup 会在 DOWN 事件到来时做重置状态的操作,清除上一个事件序列的残留状态,在 resetTouchState 方法会对标志位 FLAG_DISALLOW_INTERCEPT 进行重置(mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT),因此子 View 的 requestDisallowInterceptTouchEvent 方法无法影响 ViewGroup 对 DOWN 事件的拦截处理

注释 2 处的拦截决策的大致意思是,如果 MotionEvent 是 DOWN 起点事件或子 View 没有该事件的执行权,mFirstTouchTarget == null,则 ViewGroup 会进行拦截检测,也就是由子类重写的 onInterceptTouchEvent 方法所返回的 boolean 值,同时在内层 if 有前面提到的 requestDisallowInterceptTouchEvent 方法的实现,即 disallowIntercept 字段。

注意代码块 16 行的 setAction 方法,在事件分发过程中 MotionEvent 实例会复用避免重复 new 实例带来的开销,而 MotionEvent 实例本身会被子类重写的 onInterceptTouchEvent 方法持有,开发者可以修改指向,这样可能会导致复用错位问题,所以要 setAction 恢复成调用前的状态,是防御型编程。

注释 3 处即 ViewGroup 未拦截事件,将其分发到子 View 的阶段,要区分其中 mChildren 字段和 preorderedList 的区别,前者是 ViewGroup 内部默认的子视图数组,Android 控件的绘制过程是类似盖房自底而上的,我们在屏幕看到的 Activity 显示相当于房间的俯视图,浮在上层的 View 在 mChildren 处于末尾,事件分发又倾向先分给上层 View,所以外层 for 循环是逆序排序;后者 preorderedList 是开发者自定义排序的 View 序列,可以实现高度定制化的业务场景,所以如果 preorderedList != null,要进行 index 对齐以便于后续的事件分发。

dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign) 会调用子 View 的 dispatchTouchEvent 方法,若其返回 true,则会执行到 newTouchTarget = addTouchTarget(child, idBitsToAssign),addTouchEvent 的源码是

java 复制代码
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
    TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

注意到在 addTouchEvent 方法内部将 mFirstTouchTarget 字段赋值成 TouchTarget.obtain(child, pointerIdBits),TouchTarget 也就是完整事件序列的接收者 child,子 View 接收分发后 mFirstTouchTarget != null,回看前面注释 2 的拦截决策,假设子 View 没有设置标志位且 ViewGroup 没有重写 onInterceptTouchEvent (默认返回 false),序列后续每次触发的事件也会进入 if 块从而获取 onInterceptTouchEvent 的返回值,若 onInterceptTouchEvent 返回 true,则后续事件来到 else 块,intercepted == true 且不会再次调用 onInterceptTouchEvent 方法 ,这样也就体现了事件分发结论的 2 和 4 条。

最后看注释 4 的自己处理逻辑,dispatchTransformedTouchEvent 调用 super.dispatchTouchEvent 方法,事件接下来交由 View 处理

java 复制代码
public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    ...
    
    if (onFilterTouchEventForSecurity(event)) {
        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;
    }
    ...
    
    return result;
}

View.dispatchTouchEvent 方法先调用 onFilterTouchEventForSecurity 进行安全检查,再判断是否注册 mOnTouchListener 及 View 是否 ENABLED 可见,最后会调用注册 onTouchListener 的 onTouch 方法,判断返回值,如果前面的判断和调用均 false,则会进入 onTouchEvent,所以可以得到优先级 onTouchListener > onTouchEvent。

相关推荐
我是场3 小时前
Android Camera 从应用到硬件之- 枚举Camera - 1
android
咕噜签名分发冰淇淋3 小时前
苹果ios安卓apk应用APP文件怎么修改手机APP显示的名称
android·ios·智能手机
应用市场3 小时前
从零开始打造Android桌面Launcher应用:原理剖析与完整实现
android
叶羽西3 小时前
Android15增强型视觉系统(EVS)
android
沅霖3 小时前
android kotlin语言中的协程
android·开发语言·kotlin
齊家治國平天下3 小时前
Android 14 系统启动流程深度解析:rc文件的语法、解析及常见语法详解
android·init·rc·init.rc
刘一说3 小时前
Spring Boot 主程序入口与启动流程深度解析:从 `@SpringBootApplication` 到应用就绪
java·spring boot·后端
shaominjin1233 小时前
Android Studio 代码注释模板设置指南
android
eguid_13 小时前
【从零开始开发远程桌面连接控制工具】01-项目概述与架构设计
java·远程连接·远程控制·远程桌面·vnc·teamviewer