Android ViewPager2嵌套滑动冲突

当你使用两个ViewPager2嵌套时,确实是会有恶心的滑动冲突。默认是优先响应父布局的ViewPager2的。

而如果你希望它优先响应子布局,当子布局滑动到最后一格的时候才响应父布局,下面是我的解决方案:

以下内容,我们假设父布局的ViewPager2存在A,B,C,D四个页面,A页面存在子ViewPager2


首先我是用的广播来触发的:

  1. 默认关闭父布局滑动
  2. 子布局滑动到最后一格,发送广播通知父布局打开滑动
  3. 父布局切换A,关闭父布局滑动

但是问题很明显,我从B,C,D重新滑到A页面,父布局是已经被通知关闭滑动的,我没办法再次滑动返回B,C,D页面。所以我采用了下面的Google提供的方案。

首先新建一个工具 NestedScrollableHost


class NestedScrollableHost @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {

    private var touchSlop = ViewConfiguration.get(context).scaledTouchSlop
    private var initialX = 0f
    private var initialY = 0f

    // 找到直接子节点中的 ViewPager2
    private val parentViewPager: ViewPager2?
        get() {
            var v: View? = parent as? View
            while (v != null && v !is ViewPager2) {
                v = v.parent as? View
            }
            return v as? ViewPager2
        }

    private val child: View? get() = if (childCount > 0) getChildAt(0) else null

    private fun canChildScroll(orientation: Int, delta: Float): Boolean {
        val direction = -delta.sign.toInt()
        return when (orientation) {
            ViewPager2.ORIENTATION_HORIZONTAL -> child?.canScrollHorizontally(direction) ?: false
            ViewPager2.ORIENTATION_VERTICAL -> child?.canScrollVertically(direction) ?: false
            else -> throw IllegalArgumentException()
        }
    }

    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        handleInterceptTouchEvent(e)
        return super.onInterceptTouchEvent(e)
    }

    private fun handleInterceptTouchEvent(e: MotionEvent) {
        val orientation = parentViewPager?.orientation ?: return

        // 只有当子 View 能够滑动时,才处理拦截逻辑
        if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
            return
        }

        if (e.action == MotionEvent.ACTION_DOWN) {
            initialX = e.x
            initialY = e.y
            parent.requestDisallowInterceptTouchEvent(true)
        } else if (e.action == MotionEvent.ACTION_MOVE) {
            val dx = e.x - initialX
            val dy = e.y - initialY
            val isVpHorizontal = orientation == ViewPager2.ORIENTATION_HORIZONTAL

            val scaledDx = dx.absoluteValue * if (isVpHorizontal) 1f else .5f
            val scaledDy = dy.absoluteValue * if (isVpHorizontal) .5f else 1f

            if (scaledDx > touchSlop || scaledDy > touchSlop) {
                if (isVpHorizontal == (scaledDy > scaledDx)) {
                    // 滑动方向与 ViewPager2 方向不一致,允许父级拦截
                    parent.requestDisallowInterceptTouchEvent(false)
                } else {
                    // 滑动方向一致,判断子 View 是否能继续滑动
                    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
                        parent.requestDisallowInterceptTouchEvent(true)
                    } else {
                        parent.requestDisallowInterceptTouchEvent(false)
                    }
                }
            }
        }
    }
}

在子布局外嵌套这个View

在默认情况下,你嵌套之后发现父布局的滑动不会被触发,子布局正常了。这个问题的原因很简单,解决方案↓

解决滑动监听的问题

ViewPager2实际上是一个RecyclerView,因此它默认打开了越界回弹(OverScroll)。就是那种滑动到边缘的拉伸效果(早期Android版本可能是一个奇怪的发光效果)。当这个效果触发时,系统会认为内层 View 已经处理了这次滑动,从而不会把剩余的位移传递给父布局。

所以你需要禁用这个属性:

// 在代码中找到内层的 ViewPager2
val innerViewPager = findViewById<ViewPager2>(R.id.inner_view_pager)

// ViewPager2 内部是一个 RecyclerView,需要找到并设置它
(innerViewPager.getChildAt(0) as? RecyclerView)?.overScrollMode = View.OVER_SCROLL_NEVER

当然也有其它的可能,比如你的NestedScrollableHost嵌套层级太深。你需要保证你的NestedScrollableHost下一层就是你的Viewpager2的子视图!

其他思路:

这里有一个更粗暴,更清晰的方案,但是可能会有奇怪的问题,不过我认为可以记录下来(与我之前的想法比较接近,可能会有奇怪的问题):

innerViewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
        val isAtEnd = position == (innerViewPager.adapter?.itemCount ?: 0) - 1
        val isAtStart = position == 0
        
        if ((isAtEnd && positionOffsetPixels == 0) || (isAtStart && positionOffsetPixels == 0)) {
            // 到达边界,允许父类拦截
            innerViewPager.parent.requestDisallowInterceptTouchEvent(false)
        } else {
            // 未到边界,禁止父类拦截
            innerViewPager.parent.requestDisallowInterceptTouchEvent(true)
        }
    }
})

Comments

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注