title: 手势识别算法与性能优化 chapter: '3.4' section: '3.4' status: finalized task2b_state: fixed task6_state: reviewed task9_state: reviewed pipeline_stage: ready-to-publish applicable_versions: Android 10 (API 29) - Android 17 (API 37) version_notes: DEFAULT_STRATEGY_BY_AXIS 仅 Android 14+ (API 34+) 可用 confidence: medium last_verified: '2026-09-06' last_verified_against: AOSP android-17.0.0_r1 View/GestureDetector/VelocityTracker paths sources:
- type: aosp path: frameworks/base/core/java/android/view/VelocityTracker.java
- type: aosp path: frameworks/native/libs/input/VelocityTracker.cpp
- type: aosp path: frameworks/base/core/java/android/view/GestureDetector.java
- type: aosp path: frameworks/base/core/java/android/view/ViewConfiguration.java
- type: aosp path: frameworks/base/core/java/android/view/ViewGroup.java
- type: aosp path: frameworks/base/core/res/res/values/config.xml
- type: androidx path: frameworks/support/core/core/src/main/java/androidx/core/widget/NestedScrollView.java tags:
- android
- performance
- input
- gesture
- velocitytracker
- gesturedetector
- nestedscroll related_chapters:
- '3.1'
- '3.2'
- '3.3'
- '2.3'
手势识别算法与性能优化
手势识别既是状态机问题,也是事件所有权问题。优化前要先确认事件序列是否完整、由哪个 View 或识别器持有,再分析速度估计、阈值判断和自定义算法的 CPU 成本。
为什么要了解手势识别算法
“滑动不跟手”和“松手后没有惯性滚动”不一定伴随掉帧。触摸序列可能已经按时到达主线程,但在手势判定阶段被错误解释:
- 位移尚未超过触摸抖动容差阈值(TouchSlop),控件继续把它当作点击候选;
- 父容器过早拦截,子控件收到
ACTION_CANCEL; - 速度样本或指针 ID 使用错误,惯性滑动(Fling)速度偏小;
ACTION_MOVE回调中有分配、日志或业务计算,主线程没有及时处理下一批输入。
排查时要分开确认“事件何时到达”“谁拿到事件”“事件被识别成什么”“识别后如何驱动滚动”。平台源码基线是 Android 17、API 37、android-17.0.0_r1;涉及触控驱动边界时,内核基线是 android17-6.18-2026-06_r6。
VelocityTracker:从采样点得到速度
VelocityTracker 收集 MotionEvent 的时间、坐标和指针 ID,按需计算各指针在指定轴上的速度。GestureDetector、RecyclerView 以及许多自定义拖拽控件都依赖它判断是否启动 Fling。
生命周期与多指语义
典型用法是从 ACTION_DOWN 开始收集,在需要速度时计算,并在手势结束后回收:
private VelocityTracker velocityTracker;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
return true;
case MotionEvent.ACTION_MOVE:
velocityTracker.addMovement(event);
updateDrag(event);
return true;
case MotionEvent.ACTION_UP:
velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(
1000,
ViewConfiguration.get(getContext()).getScaledMaximumFlingVelocity());
int pointerId = event.getPointerId(event.getActionIndex());
float velocityX = velocityTracker.getXVelocity(pointerId);
float velocityY = velocityTracker.getYVelocity(pointerId);
finishGesture(velocityX, velocityY);
velocityTracker.recycle();
velocityTracker = null;
return true;
case MotionEvent.ACTION_CANCEL:
velocityTracker.recycle();
velocityTracker = null;
return true;
}
return false;
}
这段代码有三个约束:
addMovement()负责加入样本,computeCurrentVelocity()才生成供查询方法读取的速度;加入一个ACTION_MOVE不等于每次都要重新计算速度。getXVelocity(id)、getYVelocity(id)的参数是稳定的指针 ID,不是MotionEvent中会随指针增减而变化的位置索引(index)。- 一个
VelocityTracker可以同时记录多个指针 ID。源码中的对象池容量为 2,只表示最多缓存两个默认策略实例,不能据此推导“多指需要多个VelocityTracker”。
Android 17 的 Java 封装层将默认策略实例放进 Pools.SynchronizedPool<VelocityTracker>(2)。只有使用默认策略创建的实例会在 recycle() 时清空并回到池中;显式策略实例不会进入这个池。业务代码应成对调用 obtain() 和 recycle(),但不必围绕“池是否命中”设计手势算法。
Java、JNI 与原生策略
拟合计算位于 frameworks/native/libs/input/VelocityTracker.cpp。Android 17 的默认轴级策略如下:
static const std::map<int32_t, VelocityTracker::Strategy>
DEFAULT_STRATEGY_BY_AXIS = {
{AMOTION_EVENT_AXIS_X, VelocityTracker::Strategy::LSQ2},
{AMOTION_EVENT_AXIS_Y, VelocityTracker::Strategy::LSQ2},
{AMOTION_EVENT_AXIS_SCROLL, VelocityTracker::Strategy::IMPULSE},
};
触摸屏的 X、Y 是位置轴,默认使用二阶最小二乘拟合(LSQ2),即用近期的位置与时间样本拟合二次曲线并求速度;AXIS_SCROLL 是只报告变化量的差分轴,默认使用 IMPULSE,按冲量模型从连续差分样本估算速度。configureStrategy() 还明确禁止差分轴采用调用方覆盖的策略。Java 层虽然保留 obtain(int)、obtain(String) 等隐藏入口,但它们用于系统调试、测试和算法比较,普通 SDK 应用使用公开的 obtain()。
原生层的收样逻辑还包含几项影响诊断的细节:
ACTION_DOWN会先清空旧状态,再加入 X、Y 样本;ACTION_MOVE会遍历历史批次和当前批次,并为每个指针 ID 加入 X、Y;- 标记为重采样(resampled)的样本会跳过,避免根据预测坐标再次拟合速度;
ACTION_UP和普通ACTION_POINTER_UP不重复加入抬手位置,以保留末次有效移动的速度;- 同一指针超过 40 ms 没有新移动样本时,下一次采样会按“指针已经停下”处理并重建策略状态;
getComputedVelocity()最终按units / 1000缩放,并限制在[-maxVelocity, maxVelocity]。
Android 14(API 34)起,公开 API 增加了 isAxisSupported() 与 getAxisVelocity(),AXIS_SCROLL 也进入公开可跟踪范围。版本迭代可以概括为:
| 平台版本 | 默认策略模型 |
|---|---|
| Android 10–11 | 全局默认策略为 lsq2 |
| Android 12–13 | Strategy::DEFAULT 仍映射到全局 LSQ2 |
| Android 14–17 | 轴级策略表:X、Y 使用 LSQ2,SCROLL 使用 IMPULSE |
这张表用于解释版本差异,当前行为仍以 android-17.0.0_r1 为准。排查前应先确认输入源和轴,鼠标滚轮或旋钮的 AXIS_SCROLL 不能直接套用触摸屏 X、Y 的结论。
怎样测量 VelocityTracker 的开销
AOSP 没有为 addMovement() 或 computeCurrentVelocity() 承诺固定耗时,也不能从对象池推导“整条路径零分配”。处理器、编译状态、样本数、策略和调试代码都会改变结果。
Perfetto 通常只会显示包含它的上层主线程调用。若要判断自定义识别器是否算得过勤,可以在受控构建中围绕待测代码加 Trace.beginSection()、Trace.endSection(),并同时采集:
- 主线程调度与
android.view轨迹; FrameTimeline和应用帧;- ART 对象分配分析与运行时、GC 轨道,用于确认 Java、Kotlin 分配路径和停顿;若怀疑 JNI 一侧,再单独采集原生内存分配数据;
- 输入事件或自定义埋点中的
eventTime、处理开始时间。
computeCurrentVelocity() 的源码注释明确称其相对昂贵,应在需要读取速度时调用。只在 ACTION_UP 计算适合简单 Fling;如果产品需要在拖动过程中根据速度切换状态,也可以在 ACTION_MOVE 计算,但应限定触发条件,并用轨迹确认它是否构成高频耗时点。
GestureDetector:由事件序列驱动的状态机
GestureDetector 在内部持有 VelocityTracker、Handler 消息和一组状态位。单个事件只会推进状态;识别器会持续判断当前触摸序列还符合哪些候选条件。下面的状态图用于定位点击、滚动、长按、双击和 Fling 的主要分支:
flowchart LR
A["ACTION_DOWN"] --> B["Tap 候选<br/>调度 SHOW_PRESS、LONG_PRESS、TAP"]
B -->|"位移超过 TouchSlop"| C["Scroll"]
B -->|"长按超时且未取消"| D["Long press"]
B -->|"ACTION_UP 仍在 Tap 区域"| E["SingleTapUp"]
E -->|"双击等待窗结束"| F["SingleTapConfirmed"]
E -->|"第二次 DOWN 满足时间与距离"| G["Double tap"]
C -->|"ACTION_UP 且速度超过阈值"| H["Fling"]
A -->|"ACTION_CANCEL"| I["清理状态"]
这张图是阅读源码的索引,不代表每个回调互斥。例如双击监听器启用后,第一次抬手仍可能先触发 onSingleTapUp(),之后才得到 onDoubleTap() 或 onSingleTapConfirmed()。
ACTION_DOWN、ACTION_MOVE、ACTION_UP 分别做什么
Android 17 的 GestureDetector.onTouchEvent() 在进入 switch 前就把事件加入内部 VelocityTracker。各阶段的关键行为是:
ACTION_DOWN:保存按下事件和焦点坐标,进入点击候选区域,安排SHOW_PRESS、LONG_PRESS和双击确认相关消息,并调用onDown()。ACTION_MOVE:以多指焦点的位移与 TouchSlop 比较。越界后取消点击和长按候选并调用onScroll();后续移动只要焦点变化至少 1 像素即可继续产生onScroll()回调。ACTION_POINTER_UP:计算各指针的速度。如果离开的指针与剩余指针速度向量点积为负,会清空VelocityTracker,避免相反方向的多指运动生成错误 Fling。ACTION_UP:按双击、长按、单击候选、Fling 的优先顺序收尾;Fling 分支会计算速度并与最小速度阈值比较。ACTION_CANCEL:取消消息、回收事件与VelocityTracker,并清理所有状态位。
onDown() 的返回值很重要。控件应从 DOWN 起接受整个触摸序列;等到 MOVE 才开始返回 true,常会导致后续事件没有按预期交给识别器。
双击:立即抬手与延迟确认
启用 OnDoubleTapListener 后,需要区分三组回调:
onSingleTapUp():第一次点击在点击候选区域内抬手时立即调用,适合即时视觉反馈;onSingleTapConfirmed():等待双击时间窗后仍没有合格的第二次点击,才确认单击;onDoubleTap()、onDoubleTapEvent():第二次ACTION_DOWN被识别为双击后调用,后者还会收到这次点击的后续事件。
isConsideredDoubleTap() 同时检查:
- 第一次点击是否一直处于范围更大的双击触摸容差区;
- 第二次
DOWN与第一次UP的间隔是否落在doubleTapMinTime到doubleTapTimeout之间; - 两次
DOWN的距离平方是否小于doubleTapSlopSquare。
Android 17 框架后备的 doubleTapMinTime 是 40 ms,doubleTapTimeout 是 300 ms,但运行时阈值来自资源与系统配置,业务代码不应自行复制这些数字,应交给 GestureDetector 或从对应配置 API 获取。
长按:默认 400 ms,运行时可调整
Android 17 的 ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT 是 400 ms。下面的调用说明长按消息按“本次按下时间加运行时长按阈值”调度:
mHandler.sendMessageAtTime(
longPressMessage,
mCurrentDownEvent.getDownTime() + getLongPressTimeoutMillis());
ViewConfiguration.getLongPressTimeout() 会读取 Settings.Secure.LONG_PRESS_TIMEOUT;启用需要 Context 的新 API 时,则由实例取得设置值。因此,“所有 Android 设备固定 400 ms”并不准确,400 ms 只是当前平台默认值。
移动越过 TouchSlop、进入滚动、收到额外的 ACTION_POINTER_DOWN 或 ACTION_CANCEL 都可能取消长按。Android 17 还会处理 MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE:在仍有长按候选时按配置倍率放大 TouchSlop,并延后长按;CLASSIFICATION_DEEP_PRESS 则可立即触发长按。这些分类来自输入路径,应用不应根据压力值再造一套互相冲突的规则。
长按等待本身不会占用主线程。性能问题通常出现在 onLongPress() 回调,例如同步解码资源、访问磁盘或构建复杂弹窗。给回调添加应用侧轨迹区段,可以直接观察其执行时间。
View 手势冲突:先确认事件流属于谁
ACTION_DOWN 命中目标,后续事件沿目标链分发
ViewGroup.dispatchTouchEvent() 在 ACTION_DOWN 时清理上一个手势的状态并为本次触摸寻找子 View。命中的子 View 会保存为 TouchTarget。后续 MOVE、UP 通常沿已建立的目标链分发,不会每次从头对整棵 View 树做命中测试。
父 ViewGroup 仍有机会在后续事件调用 onInterceptTouchEvent()。一旦从“不拦截”变为“拦截”,原子目标会收到 ACTION_CANCEL,之后的事件交给父容器。诊断冲突时,应把同一序列的 DOWN → MOVE → CANCEL/UP 连起来看,只看某一个 MOVE 很容易误判。
横向父容器与纵向子容器
Android 没有通用的“谁先超过 TouchSlop 谁获胜”规则,胜负取决于父容器的 onInterceptTouchEvent()、子控件是否消费以及嵌套滑动协议。对于横向父容器,可以从 DOWN 保存初始坐标,等位移超过阈值且水平方向占优后再拦截:
private float initialX;
private float initialY;
private boolean draggingHorizontally;
private final int touchSlop =
ViewConfiguration.get(getContext()).getScaledTouchSlop();
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
initialX = event.getX();
initialY = event.getY();
draggingHorizontally = false;
return false;
case MotionEvent.ACTION_MOVE:
float dx = Math.abs(event.getX() - initialX);
float dy = Math.abs(event.getY() - initialY);
if (dx > touchSlop && dx > dy) {
draggingHorizontally = true;
return true;
}
return false;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
draggingHorizontally = false;
return false;
}
return draggingHorizontally;
}
这是仲裁骨架,不是可直接替换所有容器的控件实现。多指切换、从右到左布局(RTL)、避免在临界角度来回切换的角度滞回,以及子控件能否继续沿该方向滚动,都要按产品语义补充。父容器决定拦截后,必须能处理随后到来的事件;子控件也必须正确清理 ACTION_CANCEL。
requestDisallowInterceptTouchEvent 的边界
子 View 可以调用:
parent.requestDisallowInterceptTouchEvent(true);
Android 17 的 ViewGroup 会设置 FLAG_DISALLOW_INTERCEPT 并把请求逐级传给祖先。在该标志有效时,祖先跳过 onInterceptTouchEvent();新的 ACTION_DOWN、手势结束或取消会在 resetTouchState() 中清除标志。
它有三个边界:
- 这是对祖先 View 拦截的请求,不会绕过系统导航手势、窗口级输入策略或应用外的输入消费者;
- 它不会缩短已有的事件分发路径,也不会自动解决横纵方向判断;
- 不要在每个
MOVE无条件重复调用。状态没有变化时ViewGroup会提前返回,但更清楚的做法是在手势所有权变化时调用一次,并在需要交还父容器时传false。
Nested Scrolling:按消费距离协作
嵌套滑动让子容器与祖先按滚动距离协作,典型顺序是:
- 手势开始时,子 View 通过
startNestedScroll(axes, type)寻找愿意参与的父级; - 子 View 消费前调用
dispatchNestedPreScroll(),父级先取走一部分距离; - 子 View 消费剩余距离;
- 子 View 用
dispatchNestedScroll()报告已消费和未消费距离,父级还可以处理余量; - 手势或惯性滚动结束时调用
stopNestedScroll(type)。
下面的代码展示父容器预消费后,子容器如何消费余量并继续上报未消费距离。
int[] parentConsumed = new int[2];
if (dispatchNestedPreScroll(dx, dy, parentConsumed, null, type)) {
dx -= parentConsumed[0];
dy -= parentConsumed[1];
}
int consumedY = scrollSelfBy(dy);
int unconsumedY = dy - consumedY;
dispatchNestedScroll(
0, consumedY,
0, unconsumedY,
null, type);
示例只说明距离如何分配;生产代码通常复用数组,还要处理窗口偏移量、轴、触摸与非触摸类型,以及对应的父级接口。嵌套回调并不天然昂贵。只有性能跟踪显示某个父级回调或反复布局、绘制占用主线程时,才应把它列为性能问题。
TouchSlop 与 Fling 阈值
TouchSlop 是配置值,不应作为业务常量
Android 17 的 ViewConfiguration 保留 TOUCH_SLOP = 8,但注释明确说明它只为没有合适 Context 的旧代码兜底。正常应用通过下面的调用取得运行时阈值:
int touchSlop =
ViewConfiguration.get(context).getScaledTouchSlop();
取得像素值。构造 ViewConfiguration 时,框架从 config_viewConfigurationTouchSlop 资源读取像素尺寸;设备可用资源覆盖(overlay)校准它。
| 层次 | Android 17 中的含义 |
|---|---|
TOUCH_SLOP = 8 | 已弃用静态 API 使用的 dp 后备值 |
config_viewConfigurationTouchSlop | 框架默认资源,可被设备资源覆盖 |
getScaledTouchSlop() | 按当前 Context 和配置得到的像素值 |
TouchSlop 过大,会让拖动启动显得迟钝;过小,会把手指抖动误识别为拖动。调整自定义控件时还要区分“从 DOWN 的总位移”和“相邻两个 MOVE 的增量”:判定是否开始拖动通常使用前者,否则许多小增量永远无法越过阈值。
最小速度决定是否 Fling,最大速度用于限幅
Android 17 仍保留 50 dp/s 和 8000 dp/s 两个后备常量。带 Context 的运行时阈值来自 config_viewMinFlingVelocity 与 config_viewMaxFlingVelocity,getScaledMinimumFlingVelocity()、getScaledMaximumFlingVelocity() 返回像素每秒。
调用时要让单位一致:
ViewConfiguration configuration = ViewConfiguration.get(context);
velocityTracker.computeCurrentVelocity(
1000,
configuration.getScaledMaximumFlingVelocity());
float velocityY = velocityTracker.getYVelocity(pointerId);
if (Math.abs(velocityY)
>= configuration.getScaledMinimumFlingVelocity()) {
startFling(velocityY);
}
Android 14(API 34)增加了带 inputDeviceId、axis、source 的最小和最大 Fling 速度 API。输入设备或轴组合无效时,最小值返回 Integer.MAX_VALUE,最大值返回 Integer.MIN_VALUE,表示该组合不支持 Fling。跟踪旋钮的 AXIS_SCROLL 时,还需把轴值转换成像素每秒,再与对应阈值比较。触摸屏 X、Y 代码不要机械复用到滚轮和旋钮。
自定义识别器的性能与正确性陷阱
1. 在 ACTION_MOVE 中制造短命对象
下面的写法每个 MOVE 都创建 PointF:
case MotionEvent.ACTION_MOVE:
PointF delta = new PointF(
event.getX() - lastX,
event.getY() - lastY);
updateDrag(delta);
break;
高频路径可以直接使用局部 float,或复用确有必要的容器:
case MotionEvent.ACTION_MOVE:
float dx = event.getX() - lastX;
float dy = event.getY() - lastY;
updateDrag(dx, dy);
lastX = event.getX();
lastY = event.getY();
break;
不要只凭代码形态断言它一定触发 GC。应根据对象分配分析或运行时轨迹确认对象数量和停顿,再决定是否修改。
2. 把所有识别工作塞进每个 MOVE
常见热点包括同步日志、复杂几何计算、遍历业务列表、频繁 computeCurrentVelocity(),以及在回调里触发布局。处理策略是先减少无效工作:
- 未超过 TouchSlop 前,只维护识别所需的少量状态;
- 手势方向确定后,避免每帧重新执行同一套昂贵判定;
- 速度只在决策需要时计算;
- 可合并的视觉状态交给下一帧更新,不在输入回调里反复请求布局;
- 慢操作移出主线程,但触摸状态本身仍要按顺序在 UI 线程更新。
设备可能把多个采样点批量放进一个 MotionEvent 的历史记录(history)。需要高保真轨迹时应读取历史样本;只读当前坐标会丢掉中间点,但也不应为了“补点”自行插值后再送进 VelocityTracker。
3. 误解 View 层级的成本
层级和节点数量会影响 ACTION_DOWN 的命中测试,也会增加父子分发、拦截和回调的机会;后续事件通常复用 TouchTarget。因此,不能用固定层数判断输入一定慢,也不能假设每个 MOVE 都重新遍历整棵树。
优化前应在性能跟踪中找到具体的慢函数:
onInterceptTouchEvent()或onTouchEvent()中的业务代码;- 手势回调触发的
requestLayout()、同步加载布局(inflate)或数据绑定; - 嵌套滑动父级的预消费和余量处理;
ACTION_CANCEL后双方重复启动、停止动画。
减少无意义的容器仍有价值,但这属于 UI 结构优化,不能作为手势卡顿的通用处方。
厂商触控增强:先定位发生在哪一层
边缘抑制、手掌识别、湿手处理、游戏触控参数等能力可能位于触控 IC 固件、内核驱动、InputReader 配置、系统服务或应用内。AOSP 没有规定统一的边缘宽度、游戏 TouchSlop 或报点频率,不能把某个设备的测量结果写成 Android 平台行为。
可以按以下证据缩小范围:
- 同一设备上,所有应用都出现相同的原始坐标缺口,优先检查固件、驱动和设备配置;
getevent已出现样本,但框架或应用没有收到,检查 InputReader、窗口路由和输入消费者;- 应用收到连续
MotionEvent,只有某个控件改变了轨迹或阈值,检查控件识别器; - 只在系统导航边缘发生冲突,结合第 3.3、3.4 节检查系统手势与监控通道。
内核基线 android17-6.18-2026-06_r6 只能说明通用输入子系统的版本边界。没有具体设备的驱动、固件和资源覆盖源码时,不对厂商算法细节下结论。
Compose:命中链、三阶段传播与消费
Compose 在 Android 上仍从 MotionEvent 接收平台输入,但手势 API 使用 PointerEvent、PointerInputChange,并通过 Modifier.pointerInput() 中的挂起函数组织识别过程:
Modifier.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume()
updateOffset(dragAmount)
}
}
优先使用 clickable、scrollable、draggable、transformable 等高层组件或修饰符(modifier)。它们同时处理语义、焦点、可访问性、视觉反馈和事件消费。只有现有识别器无法表达所需交互时,再下沉到 awaitEachGesture、awaitFirstDown、awaitTouchSlopOrCancellation 等原始 API。
Compose 对新指针的第一个事件做命中测试,形成可接收指针输入的节点链;同一指针的后续事件沿这条链传播。每个事件经过三个传播阶段(pass):
| 传播阶段 | 方向 | 常见用途 |
|---|---|---|
Initial | 父到子 | 父级预先观察或拦截式处理 |
Main | 子到父 | 默认阶段,子节点通常先消费 |
Final | 父到子 | 根据前面阶段的消费结果收尾 |
consume() 会把当前 PointerInputChange 标记为已消费,但不会停止事件继续传播;其他处理器必须检查 isConsumed 并决定是否退出。这个语义不同于 View 中父容器拦截后给子 View 发 ACTION_CANCEL,迁移手势代码时不能机械照搬。
平台事件会适配成 Compose 内部结构,但这项转换不一定是性能瓶颈。Compose 手势卡顿仍应结合指针处理器的执行时间、状态写入引起的重组、布局与绘制,以及帧时间线判断。
排查清单
遇到滑动、双击或 Fling 异常时,按以下顺序收集证据:
- 记录完整的动作类型、指针 ID 与索引、
downTime、eventTime、坐标、事件来源和轴; - 确认序列最终是
UP还是CANCEL,以及哪个父容器改变了拦截决定; - 打印运行时
scaledTouchSlop、最小和最大 Fling 速度,不用源码后备值替代设备值; - 核对
VelocityTracker是否从ACTION_DOWN开始收样、是否在查询速度前调用computeCurrentVelocity()、是否按指针 ID 取值; - 用自定义轨迹区段标出回调,确认耗时来自识别、业务处理还是识别后的布局与绘制;
- 跨设备差异要同时保存设备资源覆盖、输入设备信息和复现轨迹,再讨论厂商调校。
源码与文档
- AOSP
android-17.0.0_r1:VelocityTracker.java - AOSP
android-17.0.0_r1:VelocityTracker.cpp - AOSP
android-17.0.0_r1:GestureDetector.java - AOSP
android-17.0.0_r1:ViewConfiguration.java - AOSP
android-17.0.0_r1:ViewGroup.java - Android common kernel
android17-6.18-2026-06_r6:tag tree - Android Developers:ViewConfiguration API reference
- Android Developers:Understand gestures in Compose
- Perfetto:Profiling memory usage and allocations