1) 它们各自是什么
-
传统手势机制
指 dispatchTouchEvent → onInterceptTouchEvent → onTouchEvent 这一套 MotionEvent 分发/拦截模型。一个手势周期内,只能有一个最终消费方(通常是最内层的可滚控件;父通过 拦截 把事件抢回)。数据粒度是 原始触点事件。
-
NestedScrolling(嵌套滑动协议)
是一套在触摸系统之上工作的 滚动协作协议(Child ↔ Parent)。它关注的是 滚动位移/速度(dx, dy, vx, vy) ,而不是原始 MotionEvent。流程分 PreScroll(父先吃)→ 子消费 → PostScroll(把剩余交给父) ,并区分 TYPE_TOUCH / TYPE_NON_TOUCH(拖动/非触摸,如 fling)。v3 还支持 父在 Post 阶段回填“我又吃了一点” 。
2) 一眼看懂的对照表
| 维度 | 传统手势(事件分发/拦截) | NestedScrolling(嵌套滑动) |
|---|---|---|
| 数据粒度 | MotionEvent(原始触点) | dx/dy 位移、vx/vy 速度 |
| 消费模型 | 单一消费者:最终只有一个 View 处理整段手势;父可在开始阶段拦截 | 协同消费:父/子 同一帧可共同消费 同一个滚动量(Pre→子→Post) |
| 目标 | 决定“谁 处理这次手势” | 决定“如何按比例分配 这次滚动” |
| 时序钩子 | 拦截前/分发后(拦截一次定生死) | dispatchNestedPreScroll / dispatchNestedScroll / (Pre)Fling / stopNestedScroll(每帧协商) |
| 多级父 | 没有“向上反馈”机制;一旦确定消费者,父不再参与 | 支持多级父链逐级消费,v3 支持父 回填 consumed 累加 |
| Fling 协作 | 各玩各的(通常子自己 fling,父无感) | dispatchNestedPreFling / dispatchNestedFling 可 先问父 / 通知父 跟随或拦截 |
| 手势/动画区分 | 无内建区分 | v2 起有 TYPE_TOUCH / TYPE_NON_TOUCH(拖动 vs fling/平滑滚动) |
| 坐标修正 | 自己处理父移动导致的坐标跳变 | offsetInWindow 提供位移修正 |
| 典型用途 | 单层滚动、点击/拖拽、简单拦截冲突 | 同向嵌套滚动协作:AppBar 收起与列表滚动、折叠头图跟随、底部面板与内容联动 |
| 学习/实现成本 | 低:重写 2~3 个回调即可 | 中:Child/Parent 双方都要实现(或用内置控件)并正确处理 pre/post/fling/type |
3) 为什么需要 NestedScrolling(传统手势做不到/做不好)
- 同向嵌套共享滚动量:例如“上滑先收起 AppBar,再让 RecyclerView 滚”。传统拦截只能二选一:要么父全吃、要么子全吃;无法一帧按比例分配。
- Fling 的协作:手指离开后列表继续惯性滚动,父(如 AppBar/头图)也应跟随;传统手势对 fling 无协作渠道。
- 多级父的逐级消费:子吃不完的 dy 交给父1,父1 吃不完再交父2……传统手势没有“post”阶段的回退通道。
- 触摸 vs 非触摸链路隔离:v2 的类型维度使“拖动链路”和“惯性/动画链路”互不干扰,避免奇怪的冲突。
4) 什么时候用谁(决策表)
- 仅单层可滚(如一个 RecyclerView / ScrollView) → 传统手势足够。
- 不同方向嵌套(外层竖、内层横) → 传统拦截 + 方向判定即可。
- 同方向嵌套且要“先父再子/先子再父/按规则分配” → 上 NestedScrolling(v2/v3) 。
- 手指放开后父也要跟随子惯性(AppBar/头图/底部面板) → 必须 NestedScrolling(含 Pre/后续 Fling) 。
- Compose 项目 → 优先用 nestedScroll/NestedScrollConnection(它就是此协议在 Compose 的落地)。
5) 典型场景对比
-
AppBarLayout ↔ RecyclerView
- 传统:要么 AppBar 抢走事件,要么 RV 独吃 → 体验割裂。
- 嵌套:onNestedPreScroll 先吃 dy 收起 AppBar;子吃剩余;到顶/到底把 dyUnconsumed 交给另一侧继续;fling 用 (Pre)Fling 协同,跟手/跟惯性都顺畅。
-
NestedScrollView 包 RecyclerView(同向)
- 传统:强拦截常引发布局抖动或滚动黏连。
- 嵌套:通常让其中一层不滚(禁用 nested)或使用 父/子明确分工的 NestedScrolling,视交互决定谁先吃。
6) 性能与工程取舍
- NestedScrolling 有更多回调(每帧 pre/post),但数据量只是几个整型,开销很小;真正的成本在“你做了哪些动画/测量/布局”。
- 按需开始/结束:只在 ACTION_DOWN 调 startNestedScroll,在 UP/CANCEL stopNestedScroll;复用 consumed/offset 数组避免分配。
- 区分 type:拖动走 TYPE_TOUCH,fling/平滑滚动走 TYPE_NON_TOUCH,避免“链路串线”。
- 优先用现成组件:RecyclerView(Child3)和 CoordinatorLayout/AppBarLayout/NestedScrollView 已实现好大部分协议,自己只需实现缺的 Parent/行为。
7) 常见坑 & 速治
- 只实现 v1,fling 不生效 → 升到 v2(含 type),更稳是 v3(父可回填 consumed)。
- 两层都能竖向滚,抖动 → 明确分工:要么禁一层滚动,要么用 NestedScrolling 分配滚动量。
- 父移动导致触点跳变 → 用 offsetInWindow 修正。
- 忘记 stop → 下个手势被污染;务必成对调用 start/stopNestedScroll。
- 以为 NestedScrolling 会替代触摸系统 → 它是“在触摸系统之上”的滚动协作层,两者并不互斥。
8) 一句话总结
- 传统手势解决“这次手势给谁处理”。
- NestedScrolling解决“这次滚动怎么在父/子之间协作分配(含 fling)”。
- 同向嵌套、联动折叠、惯性协作 → 用 NestedScrolling(v2/v3) ;其他简单场景,传统手势即可。