Motion Layout内水平RecyclerView斜向快速滑动时阻止垂直滚动
解决Motion Layout中水平RecyclerView斜向滑动触发垂直滚动的问题
我之前也踩过这个坑,nestedScrollEnabled确实没法直接搞定这种斜向快速滑动的场景。要实现Google Play那种"优先响应水平滑动,忽略轻微垂直分量"的效果,核心是让RecyclerView能判断滑动的主导方向——当用户明显是要横向滑动时,就把轻微的垂直滑动给"屏蔽"掉,不让父布局(Motion Layout)触发垂直滚动。下面给你两种经过验证的可行方案:
方案一:自定义RecyclerView,拦截并过滤垂直滑动事件
这种方法通过重写RecyclerView的触摸事件和Fling逻辑,直接在控件层面判断滑动优先级,逻辑简单直观:
class HorizontalScrollResistantRecyclerView(context: Context, attrs: AttributeSet?) : RecyclerView(context, attrs) { private var initialX = 0f private var initialY = 0f // 可调整:当水平滑动距离超过这个值时,判定为水平滑动主导 private val scrollDistanceThreshold = 80f // 可调整:速度倍数阈值,水平速度超过垂直的2倍时,只保留水平Fling private val velocityRatioThreshold = 2f override fun onInterceptTouchEvent(e: MotionEvent): Boolean { when (e.action) { MotionEvent.ACTION_DOWN -> { // 记录触摸起始点 initialX = e.x initialY = e.y } MotionEvent.ACTION_MOVE -> { val dx = abs(e.x - initialX) val dy = abs(e.y - initialY) // 水平滑动占主导时,禁止父布局拦截触摸事件(阻止垂直滚动) if (dx > scrollDistanceThreshold && dx > dy * velocityRatioThreshold) { parent.requestDisallowInterceptTouchEvent(true) } else if (dy > scrollDistanceThreshold && dy > dx * velocityRatioThreshold) { // 垂直滑动占主导时,允许父布局处理 parent.requestDisallowInterceptTouchEvent(false) } } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { // 触摸结束后,恢复父布局的拦截权限 parent.requestDisallowInterceptTouchEvent(false) } } return super.onInterceptTouchEvent(e) } // 过滤Fling事件中的垂直速度 override fun fling(velocityX: Int, velocityY: Int): Boolean { return if (abs(velocityX) > abs(velocityY) * velocityRatioThreshold) { // 只保留水平方向的Fling super.fling(velocityX, 0) } else { // 垂直Fling占主导时,交给父布局处理 false } } }
之后把布局里的普通RecyclerView替换成这个自定义控件,同时确保android:nestedScrollingEnabled="true"即可。
方案二:自定义NestedScrollingBehavior(适配Motion Layout嵌套滚动)
如果需要更精细地控制嵌套滚动的交互逻辑,可以给RecyclerView绑定一个自定义的Behavior,在嵌套滚动回调里处理方向判断:
class HorizontalRecyclerViewBehavior : NestedScrollingBehavior() { private var isHorizontalScrolling = false // 可调整:速度阈值,超过这个值才判定为有效Fling private val minVelocityThreshold = 1200f // 可调整:方向判断的速度倍数 private val velocityRatio = 1.5f override fun onStartNestedScroll( coordinatorLayout: CoordinatorLayout, child: View, directTargetChild: View, target: View, axes: Int, type: Int ): Boolean { // 只监听水平+垂直方向的滚动事件 return axes and (ViewCompat.SCROLL_AXIS_HORIZONTAL or ViewCompat.SCROLL_AXIS_VERTICAL) != 0 } override fun onNestedPreScroll( coordinatorLayout: CoordinatorLayout, child: View, target: View, dx: Int, dy: Int, consumed: IntArray, type: Int ) { if (target is RecyclerView && (target.layoutManager as? LinearLayoutManager)?.orientation == LinearLayoutManager.HORIZONTAL) { val dxAbs = abs(dx) val dyAbs = abs(dy) isHorizontalScrolling = dxAbs > dyAbs * velocityRatio if (isHorizontalScrolling) { // 水平滚动主导,消耗掉垂直方向的滚动位移,不让父布局响应 consumed[1] = dy } } super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type) } override fun onNestedPreFling( coordinatorLayout: CoordinatorLayout, child: View, target: View, velocityX: Float, velocityY: Float ): Boolean { if (isHorizontalScrolling && abs(velocityX) > abs(velocityY) * velocityRatio && abs(velocityX) > minVelocityThreshold) { // 拦截垂直Fling,只触发水平方向的快速滚动 target.fling(velocityX.toInt(), 0) return true } return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY) } }
然后在布局中给RecyclerView设置这个Behavior:
<com.yourpackage.HorizontalScrollResistantRecyclerView android:id="@+id/horizontal_rv" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_behavior="com.yourpackage.HorizontalRecyclerViewBehavior" android:nestedScrollingEnabled="true"/>
小提示
上面的阈值(距离、速度倍数、最小速度)都可以根据你的产品手感需求调整,多测试几次就能找到最贴近Google Play的交互体验。
内容的提问来源于stack exchange,提问作者Bali
相关产品推荐
相关产品推荐

