NestedScrollView“​​嵌套滚动协议​​”解决宫廷内斗

89 阅读4分钟

今天我们就用一个“​​滑动王国​​”的宫廷故事,揭开 NestedScrollView(简称 ​​NSV​​)这位“​​外交大臣​​”如何用精妙的“​​嵌套滚动协议​​”解决宫廷内斗(滑动冲突)的秘密!🧙‍♂️ 准备好爆米花,故事开始啦 🍿


🏰 ​​第一章:王国的纷争 - 滑动冲突的根源​

​场景:​​ 滑动王国里有两位贵族:

  • ​大贵族 ScrollView​:掌管​​垂直滚动​​大权,他的领地很大(整个屏幕)。
  • ​小贵族 RecyclerView​:也爱​​垂直滚动​​,但住在大贵族的城堡里(被嵌套)。

​冲突爆发:​
用户想滑动 RecyclerView 看更多商品,但手指一抖,ScrollView 就抢走事件自己滚动了!RecyclerView 气得跳脚:“​​这是我的地盘!​​” 😤
这就是经典的​​嵌套滑动冲突​​——父子 View 都想抢事件控制权!


🕊️ ​​第二章:外交大臣 NSV 的智慧 - 嵌套滚动协议​

国王 Android 5.0 看不下去了,钦点 ​NestedScrollView​ 为外交大臣,并颁布 ​NestedScrolling 协议​​(一套沟通规则)。
NSV 的厉害之处在于:​​它既是贵族(Parent),也是侍从(Child)​​!身兼两职,左右逢源。

🔮 ​​核心外交四步曲:​

想象 NSV 和子 View(如 RecyclerView)在分蛋糕(滑动事件):

  1. ​【请示父王】​​ child.dispatchNestedPreScroll(dx, dy, consumed, null)
    → 子 View 滑动前问父 View:“我要吃 dy 像素蛋糕,您先吃吗?”
    → 父 View(NSV)在 onNestedPreScroll() 回应:“我吃 20px(consumed[1]=20),剩下 80px 你吃吧!”

  2. ​【自己享用】​​ 子 View 吃剩余 80px,完成自己的滚动。

  3. ​【孝敬父王】​​ child.dispatchNestedScroll(0, 0, 0, 10, null)
    → 子 View 吃完报告:“我还剩 10px 没吃完,您要吗?”
    → 父 View 在 onNestedScroll() 笑纳:“好,我吃了!”

  4. ​【宴会结束】​​ child.stopNestedScroll()
    → 子 View 鞠躬:“父王,我滚完了!”
    → 父 View 在 onStopNestedScroll() 点头:“散会!”

💻 ​​代码中的外交协议(简化版)​

NSV 的源码中,关键角色是两个“​​外交助理​​”:

java
Copy
// 身份1:作为Child时,帮它和父View沟通
private final NestedScrollingChildHelper mChildHelper;
// 身份2:作为Parent时,帮它管理子View
private final NestedScrollingParentHelper mParentHelper;

当 NSV ​​自己需要滚动​​时,会这样处理:

java
Copy
@Override
public boolean onTouchEvent(MotionEvent ev) {
    switch (action) {
        case MotionEvent.ACTION_MOVE:
            // 1. 先问父View:“我要吃dy,您先吃吗?”
            if (dispatchNestedPreScroll(0, dy, mScrollConsumed, null)) {
                dy -= mScrollConsumed[1]; // 减去父View吃掉的
            }
            // 2. 自己吃剩余dy
            scrollBy(0, dy);
            // 3. 报告父View:“我还剩未吃的,您要吗?”
            dispatchNestedScroll(0, 0, 0, dyUnconsumed, null);
            break;
    }
}

🛡️ ​​第三章:宫廷安防 - 事件拦截机制​

NSV 的卫队长 onInterceptTouchEvent() 负责安保:

java
Copy
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            mLastMotionY = y; // 记录按下的位置
            startNestedScroll(SCROLL_AXIS_VERTICAL); // 通知父View:“我要滚了!”
            break;
        case MotionEvent.ACTION_MOVE:
            float dy = Math.abs(y - mLastMotionY);
            if (dy > mTouchSlop) { // 滑动距离超过阈值
                mIsBeingDragged = true; // 卫队长拔刀:“这事件归我管!”
                getParent().requestDisallowInterceptTouchEvent(true); // 通知父View别插手
            }
            break;
    }
    return mIsBeingDragged;
}

​安防逻辑:​

  • 手指按下(ACTION_DOWN):立刻向父 View ​​报备​​滚动计划 。

  • 手指移动(ACTION_MOVE):若滑动距离超过阈值 mTouchSlop,就​​拦截事件​​,并举起“​​金牌​​”(requestDisallowInterceptTouchEvent(true))禁止父 View 抢事件 。


🏗️ ​​第四章:城堡布局 - 只许一个孩子的倔强​

NSV 继承自 FrameLayout,但​​脾气古怪​​:​​只允许一个子 View​​!

java
Copy
// 源码中addView时会检查
@Override
public void addView(View child) {
    if (getChildCount() > 0) {
        throw new IllegalStateException("最多一个孩子!");
    }
    super.addView(child);
}

​布局技巧:​​ 若子 View 高度不足,NSV 的 fillViewport="true" 属性会强制​​拉伸孩子​​填满屏幕:

xml
Copy
<androidx.core.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true"> <!-- 关键! -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <!-- 子内容 -->
    </LinearLayout>
</androidx.core.widget.NestedScrollView>

🆚 ​​第五章:新旧贵族的对决 - 为何不用 ScrollView?​

​特性​​传统 ScrollView​​外交官 NestedScrollView​
嵌套滚动支持❌ 独断专行,抢子 View 事件✅ 用协议协商,父子和谐 2
嵌套 RecyclerView卡顿、冲突丝滑流畅 3
兼容 CoordinatorLayout❌ 不协作✅ 支持标题栏折叠/展开 7

​结论:​​ 在新项目中,​​永远用 NSV 替代 ScrollView​​!它是 Google 钦定的“冲突终结者”。


🧩 ​​终章:解决滑动冲突的三大法则​

​方法​​适用场景​​NSV 的优势​
​内部拦截法​​ (发金牌)子 View 需要优先滚动NSV 作为 Child 时,用 requestDisallow 抢权 6
​外部拦截法​​ (父裁决)父容器需控制滚动方向NSV 作为 Parent 时,在 onInterceptTouchEvent 决策 7
​嵌套协议​​ (官方推荐)所有 Android 5.0+ 项目✅ 原生支持!省心省力 45

🚀 ​​NSV 使用心法:​

  1. ​用 NSV 替代所有 ScrollView​​,尤其嵌套 RecyclerView/ListView 时 。

  2. ​设置 android:fillViewport="true"​ 避免内容不足时的布局异常。

  3. ​禁用子 View 的嵌套滚动​​:若不需要双层滚动,调用 recyclerView.setNestedScrollingEnabled(false)

  4. ​搭配 CoordinatorLayout​ 实现炫酷联动效果(如标题栏折叠) 。

“滑动王国的和平,建立在 ​NestedScrolling 协议​​的精密协作上,而 NSV 是协议最优雅的践行者。” —— 《Android 滑动外交史》📜

现在,用 NSV 去征服你的嵌套布局吧!遇到滑动冲突,记得召唤这位外交大臣~ 👑