当前位置: 首页 > news >正文

【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 等速度获取方法传入其编号即可获得速度信息

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 方法来监听各种回调方法

GestureDetector gestureDetector = new GestureDetector(context, new GestureDetector.OnGestureListener() {@Overridepublic 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 处理事件,伪代码逻辑是

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 对点击事件的分发过程

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 方法

// PhoneWindow
public boolean superDispatchTouchEvent(MotionEvent event) {return mDecor.superDispatchTouchEvent(event);
}// DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {return super.dispatchTouchEvent(event); // 即FrameLayout
}
// 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 的源码是

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 处理

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。

http://www.dtcms.com/a/532175.html

相关文章:

  • AIGC(生成式AI)试用 38 -- 程序(Python + OCR)-1
  • s001网站建设设计微信营销网络营销方式
  • #PCIE#《PCIE P2P 传输那点事儿》
  • HTTP | 跨域 - 知识点总结
  • 解决[PM2][ERROR] Script not found: D:\projects\xxx\start
  • 开发一款连接带有GEM/SECS协议软件的设备(一)
  • 大连微信网站开发app软件开发培训班
  • 同仁微网站建设工作室微信辅助网站制作
  • FFmpeg 基本数据结构 AVPacket分析
  • Linux at命令详解:轻松调度延迟任务
  • 线程停止、休眠、礼让、强制执行、观测线程状态
  • 复盘|嵌入式Linux驱动开发之I2C子系统
  • AI Agent常用的RAG有哪些种,分别适用于什么情况
  • 对中兴光猫zteOnu.exe项目的简单分析(提供下载地址)
  • 有没有专门做中考卷子的网站网站建设培训要多久
  • 做网站图片用什么格式最好个人网站备案能做宣传用么
  • JAVA1026 方法;类:抽象类、抽象类继承;接口、接口继承 Linux:Mysql
  • 密码学系列 - 零知识证明(ZKP) - NTT与MSM的总结
  • 《解决界面交互痛点:WaterFlow 瀑布流、双层嵌套滚动、键盘避让与跨 Ability 跳转实践》
  • 网页设计建立站点步骤做网站的广告词
  • Spring Boot3零基础教程,生命周期启动加载机制,笔记64
  • StarGantt在线甘特图最新版本评测
  • 43.渗透-Kali Linux-工具-Ettercap(dns欺骗)
  • 详解C++中的迭代器
  • 区块链论文速读 CCF A--USENIX Security 2025(2)
  • 基于区块链的新能源管理平台的设计与实现(源码+文档)
  • 2025年10月26日 AI大事件
  • 企业建站个人建站源码沧州专业网站建设公司
  • 基于springboot的电影评论网站系统设计与实现
  • 1.2.1.3 大数据方法论与实践指南-一种跨团队业务结算方式探索