故事:Android小镇的滑板比赛(NestedScrollingChildHelper如何解决滑动冲突)

220 阅读3分钟

在Android小镇上,每年都会举办滑板比赛。今年有个特殊挑战:父子双人滑板赛(Parent滑板和Child滑板嵌套)。但每次比赛时,父子滑板总撞在一起(滑动冲突)!直到天才工程师NestedScrollingChildHelper出现...


📖 第一章:冲突的根源

// 传统滑动事件处理(冲突现场)
override fun onTouchEvent(e: MotionEvent): Boolean {
    when(e.action) {
        ACTION_MOVE -> {
            val dy = e.y - lastY // 计算滑动距离
            parentView.scrollBy(0, dy)  // 父滑板想动
            childView.scrollBy(0, dy)   // 子滑板也想动
            return true
        }
    }
}

问题:父子滑板同时响应滑动,结果抖动、卡顿甚至反向滑动!


🦸 第二章:Helper的解决方案 - "协商机制"

Helper引入三级协商协议(源码核心思想):

  1. 预滑动阶段:Child先问Parent:"我要滑了,您要先滑吗?"
  2. 主滑动阶段:Parent不滑的部分Child自己滑
  3. 后滑动阶段:Child滑完后问Parent:"我还有剩余,您要接着滑吗?"

🔧 第三章:Helper的工具箱(关键源码解析)

Helper的武器库(简化版核心方法):

方法名作用故事类比
startNestedScroll()发起滑动协商孩子举手:"比赛开始!"
dispatchNestedPreScroll()询问父布局是否先消耗滑动"爸爸您要先滑这段距离吗?"
dispatchNestedScroll()将剩余滑动交给父布局"我滑完了,剩下的交给您!"
stopNestedScroll()结束滑动协作"比赛结束!"

🛠 第四章:实战代码演示

// Child滑板装备Helper(初始化)
val childHelper = NestedScrollingChildHelper(this)
childHelper.isNestedScrollingEnabled = true // 开启协作模式

override fun onTouchEvent(e: MotionEvent): Boolean {
    when(e.action) {
        ACTION_DOWN -> {
            // 阶段1:发起协商
            childHelper.startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
        }
        ACTION_MOVE -> {
            val dy = (lastY - e.y).toInt() // 计算滑动距离
            
            // 阶段2:预滑动询问
            val consumed = IntArray(2)
            if (childHelper.dispatchNestedPreScroll(0, dy, consumed, null)) {
                dy -= consumed[1] // 减去父布局消耗的部分
            }
            
            // 自己消费剩余滑动
            scrollBy(0, dy)
            
            // 阶段3:传递剩余量(此处为0,实际可传递overscroll)
            childHelper.dispatchNestedScroll(0, 0, 0, 0, null)
        }
        ACTION_UP -> {
            // 结束协作
            childHelper.stopNestedScroll()
        }
    }
    return true
}

🎯 第五章:协作流程图解

      [Child滑动开始]
          │
          ▼
   ┌───────────────┐
   │startNestedScroll│  // 发起协商
   └───────┬───────┘
           │
           ▼
 ┌───────────────────┐
 │dispatchNestedPreScroll│ // 问:"父布局要先滑吗?"
 └─────────┬─────────┘
           │
    ┌──────┴──────┐
    │父布局消耗部分距离│
    └──────┬──────┘
           │
           ▼
┌─────────────────────┐
│Child滑动剩余距离      │ // "那我滑剩下的!"
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│dispatchNestedScroll │ // "我滑完了,父布局要收尾吗?"
└──────────┬──────────┘
           │
           ▼
      [滑动结束]

💡 第六章:设计精髓揭秘

  1. 事件分层机制

    // 源码核心逻辑(NestedScrollingChildHelper.java)
    public boolean dispatchNestedPreScroll(int dx, int dy, 
            @Nullable int[] consumed, @Nullable int[] offsetInWindow) {
        if (hasNestedScrollingParent()) {
            // 关键调用:向上传递预滑动事件
            mNestedScrollingParent.onNestedPreScroll(mView, dx, dy, consumed);
        }
    }
    
  2. 责任链模式:事件沿父子层级逐级传递

  3. 滑动信用体系:通过consumed[]数组精确分配滑动距离


🏆 第七章:比赛结果

使用Helper后的效果:

  1. 父布局(RecyclerView)先滑动:消耗90%距离
  2. 子布局(ScrollView)滑动剩余10%
  3. 当子布局滑到底部:剩余距离交还父布局
  4. 完美平滑过渡!零冲突!

✨ 终极总结

NestedScrollingChildHelper本质是:

一套滑动事件仲裁协议
通过预滑动 → 自身滑动 → 后滑动的三段式协作,
用"询问-响应-再分配"机制取代暴力抢夺事件,
最终实现嵌套滑动的和谐统一!

就像Android小镇的滑板比赛:
孩子先问父亲:"您要先滑吗?" → 父亲滑不动了孩子再滑 → 孩子滑到底把剩余距离还给父亲
一家人整整齐齐向前冲! 🚀