当你使用两个ViewPager2嵌套时,确实是会有恶心的滑动冲突。默认是优先响应父布局的ViewPager2的。
而如果你希望它优先响应子布局,当子布局滑动到最后一格的时候才响应父布局,下面是我的解决方案:
以下内容,我们假设父布局的ViewPager2存在A,B,C,D四个页面,A页面存在子ViewPager2
首先我是用的广播来触发的:
- 默认关闭父布局滑动
- 子布局滑动到最后一格,发送广播通知父布局打开滑动
- 父布局切换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)
}
}
})

发表回复