返回

ViewPager2 横向滑动冲突的探索和解决

Android

问题的根源与冲突产生的时机

对于滑动冲突问题的理解,需要明确的是,冲突的发生是触摸事件分发的过程中没有满足开发者需求的结果,最终处理触摸事件的 View 不是业务上想要的结果。

触摸事件的分发机制:

  • 事件分发机制是一个从 Activity 开始逐层向下分发的过程。
  • 分发的顺序遵循这样的规律:先父控件后子控件,同级控件则以深度优先遍历的顺序进行事件分发。
  • 当触摸事件分发到某个 View 时,会调用该 View 的 onTouchEvent 方法来处理该事件。
  • 事件被消费后不会再向下分发。

ViewPager2 横向滑动与 RecyclerView 冲突产生的时机:

ViewPager2 与 RecyclerView 同时出现在一个布局时,当用户手指在 RecyclerView 上进行横向滑动时,可能会出现滑动冲突。出现滑动冲突的原因是 RecyclerView 的 onInterceptTouchEvent 方法的默认返回值为 true,表示 RecyclerView 拦截了这次触摸事件,并且事件不会再向下分发给 ViewPager2。

解决 ViewPager2 与 RecyclerView 滑动冲突的办法,就是让 RecyclerView 不要拦截这个横向滑动事件,在 onInterceptTouchEvent 方法中返回 false,并且在 ViewPager2 中消费这个事件。以下列出一些解决方案:

ViewPager2 中解决冲突

禁用 RecyclerView 的滑动:

这种解决方案是最简单的,可以在 RecyclerView 的 onInterceptTouchEvent 方法中返回 false,表示 RecyclerView 不拦截这个横向滑动事件。这种方法的缺点是,RecyclerView 的滑动功能将被禁用。

override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
    return false
}

子类化 RecyclerView:

另一种方法是子类化 RecyclerView 并覆盖 onInterceptTouchEvent 方法。在子类的 onInterceptTouchEvent 方法中,可以根据特定的条件来决定是否拦截触摸事件。例如,如果触摸事件的水平位移大于垂直位移,则可以返回 false,表示不拦截这个触摸事件。这种方法的优点是,RecyclerView 的滑动功能不会被禁用。

class MyRecyclerView : RecyclerView {

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        if (Math.abs(e.x - e.rawX) > Math.abs(e.y - e.rawY)) {
            return false
        }
        return super.onInterceptTouchEvent(e)
    }
}

使用 CoordinatorLayout:

CoordinatorLayout 是一个可以协调多个 View 之间布局和行为的布局。可以在 CoordinatorLayout 中使用 Behavior 来控制 View 的行为。例如,可以使用 CoordinatorLayout.Behavior.BottomSheetBehavior 来控制 BottomSheet 的行为。这种方法的优点是,可以更灵活地控制 View 的行为。

<androidx.coordinatorlayout.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.viewpager2.widget.ViewPager2
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <androidx.recyclerview.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/app_bar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

使用 NestedScrollingChild:

NestedScrollingChild 是一个可以嵌套到另一个 View 中的 View。当嵌套的 View 发生滚动时,父 View 会收到通知。可以使用 NestedScrollingChild 来解决 ViewPager2 与 RecyclerView 滑动冲突的问题。这种方法的优点是,可以更灵活地控制 View 的滚动行为。

class MyNestedRecyclerView : RecyclerView, NestedScrollingChild {

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    override fun isNestedScrollingEnabled(): Boolean {
        return true
    }

    override fun startNestedScroll(axes: Int, type: Int): Boolean {
        return true
    }

    override fun stopNestedScroll(type: Int) {
        super.stopNestedScroll(type)
    }

    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
        val consumedX = dx
        val consumedY = dy
        scrollBy(consumedX, consumedY)
        consumed[0] += consumedX
        consumed[1] += consumedY
    }

    override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int) {
        super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type)
    }
}

结语

以上是几种解决 ViewPager2 与 RecyclerView 滑动冲突的办法。具体使用哪种办法,需要根据实际情况来决定。在解决 ViewPager2 与 RecyclerView 滑动冲突时,需要理解触摸事件分发的机制,并根据触摸事件分发机制来设计解决方案。