NestedScrollView的滑动冲突解决方案:嵌套滑动的"接力赛"故事

218 阅读3分钟

故事背景:滑动的接力棒

想象Android世界正在举办一场滑动接力赛

  • 第一棒选手:RecyclerView(负责内容区域的精细滑动)
  • 第二棒选手:NestedScrollView(负责整体页面的宏观滑动)

当用户滑动时,就像传递接力棒:

plaintext

用户手指滑动 → RecyclerView先接棒 → 滑到边界 → 传递给NestedScrollView → 继续滑动

但旧版ScrollView像独裁的裁判,只会抢走接力棒自己跑,导致RecyclerView永远没机会滑动!


第一章:NestedScrollView的智慧——嵌套滑动协议

NestedScrollView通过实现嵌套滑动协议(NestedScrolling)解决冲突,就像制定了接力赛规则:

java

// NestedScrollView的类声明
public class NestedScrollView extends FrameLayout implements
    NestedScrollingParent3,  // 我是"父级选手"
    NestedScrollingChild3 {  // 我也是"子级选手"
}

核心角色:

  1. NestedScrollingChild:接力发起者(如RecyclerView)
  2. 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();
}

第三章:滑动冲突解决全景图

deepseek_mermaid_20250702_f89b90.png


第四章: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);
        }
    }
}

嵌套滑动黄金法则:

  1. 起跑协商:Child通过startNestedScroll()发起滑动
  2. 预跑机制:Parent通过onNestedPreScroll()优先消费
  3. 正式开跑:Child用剩余距离自己滑动
  4. 补跑机会:Child通过onNestedScroll()报告未消费距离
  5. 冲刺接力:Fling事件通过onNestedPreFling()传递

🏁 滑动接力口诀
起跑先举手(startNestedScroll)
预跑看父走(onNestedPreScroll)
自己跑一段(Child滑动)
剩距交父手(onNestedScroll)
冲刺别忘吼(onNestedPreFling)

NestedScrollView就像智慧的接力赛教练,让RecyclerView和自身完美配合,实现了丝滑的嵌套滑动体验!