You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

RecyclerView子项用MotionLayout侧滑的冲突与崩溃问题

我之前也遇到过一模一样的问题,这本质是RecyclerView的滚动触摸事件和MotionLayout侧滑事件的分发优先级冲突,再加上触摸处理逻辑的小漏洞导致了空指针崩溃。下面是经过验证的解决方案:

解决RecyclerView子项MotionLayout侧滑与滚动的手势冲突及崩溃问题

1. 优化MotionScene的侧滑配置

先调整你的MotionScene,给OnSwipe添加更精确的触摸约束,减少不必要的手势误触发:

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:motion="http://schemas.android.com/apk/res-auto">
    <Transition motion:constraintSetStart="@layout/chat_item_start" 
                motion:constraintSetEnd="@layout/chat_item_end" 
                motion:duration="500">
        <OnSwipe motion:targetId="@+id/background" 
                 motion:touchAnchorSide="right" 
                 motion:dragDirection="dragLeft"
                 motion:touchRegionId="@+id/background" <!-- 限定触摸触发区域为背景 -->
                 motion:maxVelocity="1000" <!-- 限制滑动速度,降低和滚动的冲突概率 -->
                 motion:threshold="0.2" /> <!-- 降低触发阈值,更容易激活侧滑 -->
    </Transition>
</MotionScene>

2. 修复触摸事件监听器的核心逻辑

你原来的触摸监听返回false会导致事件继续向上传递,让RecyclerView提前拦截滚动;而且空指针大概率是因为holder.motion为空或者触摸变量未正确初始化。修改后的逻辑如下:

private var initialX = 0f
private var initialY = 0f

holder.itemView.setOnTouchListener { view, motionEvent ->
    val motionLayout = holder.motion ?: return@setOnTouchListener false
    val progress = motionLayout.progress

    when(motionEvent.actionMasked) { // 用actionMasked处理多点触摸更安全
        MotionEvent.ACTION_DOWN -> {
            initialX = motionEvent.x
            initialY = motionEvent.y
            // 按下时立即让MotionLayout接管事件,防止RecyclerView抢先拦截
            view.parent.requestDisallowInterceptTouchEvent(true)
        }
        MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
            // 根据当前动画进度决定最终状态
            if (progress > 0.5f) {
                motionLayout.transitionToEnd()
            } else {
                motionLayout.transitionToStart()
            }
            // 动画结束后释放拦截,允许RecyclerView正常滚动
            view.parent.requestDisallowInterceptTouchEvent(false)
        }
        MotionEvent.ACTION_MOVE -> {
            val deltaX = abs(motionEvent.x - initialX)
            val deltaY = abs(motionEvent.y - initialY)
            
            // 动态判断手势类型:横向偏移大于纵向时保持拦截,否则释放让RecyclerView滚动
            if (deltaX > deltaY && deltaX > 10) { // 增加最小偏移阈值,避免微小滑动误判
                view.parent.requestDisallowInterceptTouchEvent(true)
            } else if (deltaY > deltaX && deltaY > 10) {
                view.parent.requestDisallowInterceptTouchEvent(false)
            }
        }
    }
    // 让MotionLayout自己处理侧滑逻辑,不要手动模拟
    motionLayout.onTouchEvent(motionEvent)
}

3. 完善TransitionListener的空安全处理

原来的Listener里没有做空安全判断,这是空指针异常的潜在来源之一,修改后:

holder.motion?.setTransitionListener(object : MotionLayout.TransitionListener {
    override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) {}
    
    override fun onTransitionStarted(p0: MotionLayout?, startId: Int, endId: Int) {
        p0?.let {
            // 动画开始时强制拦截事件,防止滚动干扰动画执行
            it.parent.requestDisallowInterceptTouchEvent(true)
        }
    }
    
    override fun onTransitionChange(p0: MotionLayout?, startId: Int, endId: Int, progress: Float) {}
    
    override fun onTransitionCompleted(p0: MotionLayout?, currentId: Int) {
        p0?.let {
            // 动画完成后释放拦截,恢复RecyclerView的滚动功能
            it.parent.requestDisallowInterceptTouchEvent(false)
        }
    }
})

4. 额外的崩溃预防小贴士

针对你提到的VelocityTracker空指针,还要注意:

  • 确保MotionLayout是子项布局的根布局,保证触摸事件能完整传递到它
  • 在Adapter的onBindViewHolder中,务必确认holder.motion已经正确初始化,避免为null
  • 避免在动画过程中快速切换手势(比如侧滑到一半突然滚动),这会导致MotionLayout内部状态混乱

通过以上调整,侧滑时MotionLayout会优先接管事件,滚动时RecyclerView正常响应,同时彻底避免了空指针崩溃的风险。

内容的提问来源于stack exchange,提问作者Borombo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:53:57