故事背景:滑动的接力棒
想象Android世界正在举办一场滑动接力赛:
- 第一棒选手:RecyclerView(负责内容区域的精细滑动)
- 第二棒选手:NestedScrollView(负责整体页面的宏观滑动)
当用户滑动时,就像传递接力棒:
plaintext
用户手指滑动 → RecyclerView先接棒 → 滑到边界 → 传递给NestedScrollView → 继续滑动
但旧版ScrollView像独裁的裁判,只会抢走接力棒自己跑,导致RecyclerView永远没机会滑动!
第一章:NestedScrollView的智慧——嵌套滑动协议
NestedScrollView通过实现嵌套滑动协议(NestedScrolling)解决冲突,就像制定了接力赛规则:
java
// NestedScrollView的类声明
public class NestedScrollView extends FrameLayout implements
NestedScrollingParent3, // 我是"父级选手"
NestedScrollingChild3 { // 我也是"子级选手"
}
核心角色:
NestedScrollingChild:接力发起者(如RecyclerView)NestedScrollingParent:接力接收者(如NestedScrollView)
第二章:接力赛的四个关键步骤
步骤1:起跑信号(开始滑动)
java
// RecyclerView(Child)发起滑动
@Override
public void startNestedScroll(int axes) {
// 通知Parent:"我要开始滑动了!"
if (hasNestedScrollingParent()) {
mParent.startNestedScroll(axes);
}
}
步骤2:预传递(Parent先跑)
java
// NestedScrollView(Parent)的预滑动
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
// 计算自己还能滑多少
int remainY = getScrollY() - getScrollRange();
// 先消费部分滑动距离
if (remainY > 0 && dy > 0) {
int consumeY = Math.min(remainY, dy);
scrollBy(0, consumeY);
consumed[1] = consumeY; // 告诉Child:"我消费了这么多"
}
}
步骤3:正式传递(Child滑动)
java
// RecyclerView(Child)自己滑动
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
// 使用Parent未消费的距离自己滑动
if (dyUnconsumed != 0) {
scrollBy(0, dyUnconsumed);
}
}
步骤4:终点冲刺(边界处理)
java
// NestedScrollView处理边界
private boolean canChildScrollUp() {
// 检查RecyclerView是否还能上滑
return mChildRecyclerView.canScrollVertically(-1);
}
@Override
public boolean onStartNestedScroll(View child, View target, int axes) {
// 只当Child无法滑动时才接棒
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
&& !canChildScrollUp();
}
第三章:滑动冲突解决全景图
第四章:Google的巧妙设计揭秘
设计1:滑动优先级策略
java
// 实际源码:NestedScrollView#onNestedPreScroll
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
// 先尝试自己滑动
int[] parentConsumed = new int[2];
if (dispatchNestedPreScroll(dx, dy, parentConsumed, null)) {
// 如果祖父Parent消费了部分距离
consumed[0] += parentConsumed[0];
consumed[1] += parentConsumed[1];
}
// 再自己消费剩余部分
int left = dy - consumed[1];
if (left != 0) {
int scrollY = getScrollY();
int oldScrollY = scrollY;
scrollY = clamp(scrollY - left, getScrollRange(), 0);
consumed[1] += oldScrollY - scrollY;
scrollTo(getScrollX(), scrollY);
}
}
设计2:Fling接力处理
java
@Override
public void onNestedPreFling(View target, float velocityX, float velocityY) {
// 如果自己还能滑动,就消费fling
if (getScrollY() > 0) {
fling((int) velocityY);
}
// 否则传递给父级
else {
super.onNestedPreFling(target, velocityX, velocityY);
}
}
第五章:与传统方案的对比
| 方案 | 接力赛比喻 | 缺点 |
|---|---|---|
| 外部拦截法 | 裁判强制分配接力棒 | 不够灵活 |
| 内部拦截法 | 选手自己协商 | 嵌套层级多时复杂 |
| NestedScroll | 标准化的接力规则 | 需要双方实现协议 |
终极解决方案:自定义嵌套滑动
如果你想自定义滑动逻辑,只需实现协议接口:
java
public class CustomNestedLayout extends ViewGroup
implements NestedScrollingParent3 {
// 1. 声明支持嵌套滑动
@Override
public boolean onStartNestedScroll(View child, View target, int axes) {
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
// 2. 预处理滑动
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
int halfDy = dy / 2;
if (canScrollVertically(halfDy)) {
scrollBy(0, halfDy);
consumed[1] = halfDy; // 告诉Child我消费了一半
}
}
// 3. 处理剩余滑动
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
if (dyUnconsumed < 0) { // 还有向下滑动未消费
scrollBy(0, dyUnconsumed / 2);
}
}
}
嵌套滑动黄金法则:
- 起跑协商:Child通过
startNestedScroll()发起滑动 - 预跑机制:Parent通过
onNestedPreScroll()优先消费 - 正式开跑:Child用剩余距离自己滑动
- 补跑机会:Child通过
onNestedScroll()报告未消费距离 - 冲刺接力:Fling事件通过
onNestedPreFling()传递
🏁 滑动接力口诀:
起跑先举手(startNestedScroll)
预跑看父走(onNestedPreScroll)
自己跑一段(Child滑动)
剩距交父手(onNestedScroll)
冲刺别忘吼(onNestedPreFling)
NestedScrollView就像智慧的接力赛教练,让RecyclerView和自身完美配合,实现了丝滑的嵌套滑动体验!