ViewPager2 横向滑动冲突的探索和解决
2024-02-09 16:13:01
问题的根源与冲突产生的时机
对于滑动冲突问题的理解,需要明确的是,冲突的发生是触摸事件分发的过程中没有满足开发者需求的结果,最终处理触摸事件的 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 滑动冲突时,需要理解触摸事件分发的机制,并根据触摸事件分发机制来设计解决方案。