带有 RecyclerView 滚动问题的 Vertical ViewPager2

Vertical ViewPager2 with RecyclerView Scrolling Issue

我在垂直方向上使用带有两个片段的 ViewPager2。当用户向下滑动到第二个片段时,有一个 RecyclerView 在相同的垂直方向滚动内容。

问题是当我滚动 RecyclerView 的内容时,有时 ViewPager2 捕获滚动事件,有时 RecyclerView 捕获滚动事件。

我想要这样当用户滚动到 RecyclerView 的顶部时,ViewPager 只会在用户到达顶部时向上滑动回到第一个片段RecyclerView 中的内容。

我试过使用 recyclerView.isNestedScrollingEnabled = false,但运气不佳。我还尝试将 RecyclerView 放入 NestedScrollView,但不推荐这样做,因为 RecyclerView 然后会创建数据集所需的每个 ViewHolder,这显然效率不高。

所以...我只是阅读了一些 documentation 就能弄清楚。我将 post 答案放在这里,以帮助遇到类似问题的其他人:

由于 ViewPager2 不能很好地支持嵌套滚动视图,与 NestedScrollView 不同,我们需要在布局中使用自定义包装器包装嵌套滚动视图,以便能够处理被嵌套拦截的触摸和滑动事件滚动视图父级。在我们的例子中,子项是 RecyclerView,父项是 ViewPager2。

您可以找到包装器 class here。只需将它添加到您的项目中,然后将可滚动视图包裹在其中,类似于以下内容:

    <NestedScrollableHost
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/my_recycler_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical" />

    </NestedScrollableHost>

这里有几件事需要注意:documentation 表示此解决方案不适用于 ViewPager 中其他可滚动视图内的可滚动视图。此解决方案仅适用于 ViewPager 的即时滚动视图。

另一个注意事项是包装器 class 使用 requestDisallowInterceptTouchEvent() 来确保如果子 需要滚动,子可滚动视图会告诉父级不要滚动。

我得到的最佳解决方案是在 recyclerView.addOnItemTouchListener(this) 上使用 gestureDetector.SimpleOnGestureListener。

第 1 步:在 OnCreate() 方法中

gestureDetector = new GestureDetector(getActivity(), new GestureListener());

第二步:实现recyclerView addonitemtouchlistenr方法-

recyclerView.addOnItemTouchListener(this);

第 3 步:创建扩展 GestureDetector.SimpleOnGestureListener.

的 class GestureListener
public class GestureListener extends GestureDetector.SimpleOnGestureListener {
    private final int Y_BUFFER = 10;

    @Override
    public boolean onDown(MotionEvent e) {
        // Prevent ViewPager from intercepting touch events as soon as a DOWN is detected.
        // If we don't do this the next MOVE event may trigger the ViewPager to switch
        // tabs before this view can intercept the event.
        Log.d("vp", "true1");
        recyclerView.getParent().requestDisallowInterceptTouchEvent(true);
        return super.onDown(e);
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        if (Math.abs(distanceX) > Math.abs(distanceY)) {
            Log.d("vp2", "true");
            // Detected a horizontal scroll, allow the viewpager from switching tabs
            recyclerView.getParent().requestDisallowInterceptTouchEvent(false);
        } else if (Math.abs(distanceY) > Y_BUFFER) {
            // Detected a vertical scroll prevent the viewpager from switching tabs
            Log.d("vp3", "false");
            recyclerView.getParent().requestDisallowInterceptTouchEvent(true);
        }
        return super.onScroll(e1, e2, distanceX, distanceY);
    }
}

第 4 步:从 onInterceptTouchEvent() 调用 gestureDetector.onTouchEvent(e)。

@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
    gestureDetector.onTouchEvent(e);
    return false;
}

class NestedScrollableHost : FrameLayout {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    private var touchSlop = 0
    private var initialX = 0f
    private var initialY = 0f
    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

    init {
        touchSlop = ViewConfiguration.get(context).scaledTouchSlop
    }

    private fun canChildScroll(orientation: Int, delta: Float): Boolean {
        val direction = -delta.sign.toInt()
        return when (orientation) {
            0 -> child?.canScrollHorizontally(direction) ?: false
            1 -> 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

        // Early return if child can't scroll in same direction as parent
        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 == ORIENTATION_HORIZONTAL

            // assuming ViewPager2 touch-slop is 2x touch-slop of child
            val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
            val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f

            if (scaledDx > touchSlop || scaledDy > touchSlop) {
                if (isVpHorizontal == (scaledDy > scaledDx)) {
                    // Gesture is perpendicular, allow all parents to intercept
                    parent.requestDisallowInterceptTouchEvent(false)
                } else {
                    // Gesture is parallel, query child if movement in that direction is possible
                    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
                        // Child can scroll, disallow all parents to intercept
                        parent.requestDisallowInterceptTouchEvent(true)
                    } else {
                        // Child cannot scroll, allow all parents to intercept
                        parent.requestDisallowInterceptTouchEvent(false)
                    }
                }
            }
        }
    }
}