之前『TabLayout & ViewPager2 快速打造选项卡切换效果]』一文介绍了如何实现一个简单的页面滑动切换效果,随着开发的深入,你可能会遇到 ViewPager2 嵌套滑动的问题,也就是当 ViewPager2 内有一个可以同方向滚动的 View,比如 RecyclerView,它并不能够按照我们的预期工作。

嵌套滚动异常

嵌套?之前我们用 NestedScrollView 解决 Android 嵌套滚动问题,这次能不能依葫芦画瓢?

很显然,并不适合。

既然如此,我们能否通过修改 ViewPager2 的事件分发逻辑,使其根据我们的需要进行分发?

很遗憾,ViewPager2 被标记为 final,我们无法继承修改。

public final class ViewPager2 extends ViewGroup {
    ...
}

RecyclerView 总该不会被标记为 final 吧,我们重写 RecyclerView 是否行得通?

我们换个角度思考这个问题,这次我们重写 RecyclerView,假如说下次我们需要往 ViewPager2 里嵌套一个 ScrollView 或者其他任意可滚动的 View,岂不是每一个都要重写?有没有一劳永逸的方法?

其实可以单独自定义一个 ViewGroup 用于处理 ViewPager2 内的事件分发,当添加了需要处理嵌套滑动问题的 View 时,只需要在该 View 外面包一层我们自定义的 ViewGroup,就可以实现对嵌套滑动事件的处理。

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

    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
        }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        when (ev?.action) {
            MotionEvent.ACTION_DOWN -> {
                parentViewPager?.isUserInputEnabled = false
            }

            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                parentViewPager?.isUserInputEnabled = true
            }
        }
        return super.dispatchTouchEvent(ev)
    }
}

我们继承 FrameLayout 来实现,一般情况下该 ViewGroup 仅会包裹我们需要嵌套滚动的 View,所以 FrameLayout 对原有代码的入侵性最小,同时性能也最优。

这其中一个关键点是如何获取 ViewPager2 的实例,当然我们可以通过外部传入,但这样每次使用时都需要手动设置一遍,非常麻烦。于是这里采用另一种方式,通过 ViewgetParent() 方法获取其父 View,然后判断其是否为 ViewPager2,如果是则返回,否则继续向上查找。

拿到 ViewPager2 的实例就好办了,当接收到 ACTION_DOWN 事件时,我们调用 setUserInputEnabled() 设置为 false,禁止 ViewPager2 滚动,当接收到 ACTION_UPACTION_CANCEL 事件时,我们设置为 true,允许 ViewPager2 滚动。

使用方法正如我们前面所述,包裹在需要嵌套滑动的 View 外即可:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".NestedFragment">
    ...
    <com.example.nested.ViewPager2NestedFrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
    </com.example.nested.ViewPager2NestedFrameLayout>
</FrameLayout>

效果如下:

嵌套滚动正常

正如我们所期望的那样工作,由于范围仅限制在该 ViewGroup 范围内,所以触摸该 ViewGroup 以外的区域仍能够使 ViewPager2 正常滑动。