如何处理RecyclerView中OnTouchListener触摸事件冲突问题
RecyclerView 子项内ImageView拖动交互失效修复
问题场景
- 基于RecyclerView实现列表,列表项内包含可拖动的ImageView(黑色方块),需求为支持左右拖动方块,方块触碰容器边缘或抬手时自动回弹到容器中心位置
- 布局效果参考:

- 异常表现:ImageView要么完全无法移动,要么仅能移动极小距离,抬手后无法正常回到中心位置
原有问题代码
Adapter实现
class RecyclerViewAdapter( private val items: List<String>) : RecyclerView.Adapter<RecyclerViewAdapter.Holder>() { inner class Holder(val binding: ItemBinding) : RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { val binding = ItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) return Holder(binding) } @SuppressLint("ClickableViewAccessibility") override fun onBindViewHolder(holder: Holder, position: Int) { holder.binding.image.setOnTouchListener( MyTouchInterface( image = holder.binding.image, container = holder.binding.container ) ) } override fun getItemCount() = items.size }
触摸监听实现
class MyTouchInterface( private val image: AppCompatImageView, private val container: View ) : View.OnTouchListener { private var downPT = PointF() private var startPT = PointF() @SuppressLint("ClickableViewAccessibility") override fun onTouch(view: View?, event: MotionEvent?): Boolean { val defaultPosition = container.x + (container.width / 2 - image.width / 2) when (event!!.action) { MotionEvent.ACTION_MOVE -> { image.x = (startPT.x + event.x - downPT.x) startPT[image.x] = image.y container.doOnLayout { if ((startPT.x + event.x) - downPT.x < (defaultPosition - image.width)) { Log.e("POS left", "${(startPT.x + event.x) - downPT.x}") } if ((startPT.x + event.x) - downPT.x > (defaultPosition + image.width)) { Log.e("POS right", "${(startPT.x + event.x) - downPT.x}") } } } MotionEvent.ACTION_DOWN -> { downPT[event.x] = event.y startPT[image.x] = image.y } MotionEvent.ACTION_UP -> { image.x = defaultPosition } else -> {} } return true } }
问题根因
- 触摸事件拦截冲突:拖动时RecyclerView默认会拦截MOVE事件用于列表滚动,没有主动通知父布局放弃拦截,导致子View收不到完整的拖动事件序列
- 坐标参考系错误:计算默认居中位置时错误叠加了
container.x(容器相对于其父布局的X偏移),但ImageView的x坐标是相对于直接父布局(即container)的,两者参考系不统一,导致位置计算完全错误 - 拖动逻辑错误:ACTION_MOVE阶段每次都更新起始位置,导致偏移量计算累加异常,拖动距离被截断
修复方案
- 在ACTION_DOWN触发时调用
parent.requestDisallowInterceptTouchEvent(true),通知RecyclerView不要拦截当前触摸事件序列,拖动结束后恢复拦截状态 - 统一坐标参考系:使用
event.rawX获取屏幕绝对坐标计算拖动偏移,避免不同View坐标系的转换问题;居中位置直接基于container和image的宽高计算,无需叠加容器的父布局偏移 - 修正拖动逻辑:仅在ACTION_DOWN时记录ImageView初始位置和按下坐标,MOVE阶段基于初始值计算偏移,同时增加边界限制,不让ImageView拖出容器范围
- 回弹逻辑增加属性动画,优化交互体验
修复后的触摸监听代码
class MyTouchInterface( private val image: AppCompatImageView, private val container: View ) : View.OnTouchListener { private var downRawX = 0f private var startImageX = 0f // 相对于container的居中X坐标 private val centerX: Float get() = (container.width - image.width) / 2f // 拖动左右边界 private val minX = 0f private val maxX: Float get() = (container.width - image.width).toFloat() @SuppressLint("ClickableViewAccessibility") override fun onTouch(view: View, event: MotionEvent): Boolean { when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { // 禁止父布局拦截触摸事件 view.parent.requestDisallowInterceptTouchEvent(true) downRawX = event.rawX startImageX = image.x } MotionEvent.ACTION_MOVE -> { val dx = event.rawX - downRawX var targetX = startImageX + dx // 限制拖动范围不超出容器 targetX = targetX.coerceIn(minX, maxX) image.x = targetX } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { // 带动画回弹到中心 image.animate() .x(centerX) .setDuration(200) .start() // 恢复父布局事件拦截 view.parent.requestDisallowInterceptTouchEvent(false) } } return true } }
补充初始化配置
在onBindViewHolder中给container增加布局完成回调,初始化ImageView的初始位置,避免首次加载位置偏移:
override fun onBindViewHolder(holder: Holder, position: Int) { val touchListener = MyTouchInterface( image = holder.binding.image, container = holder.binding.container ) holder.binding.image.setOnTouchListener(touchListener) // 布局完成后初始化方块位置到中心 holder.binding.container.doOnLayout { holder.binding.image.x = (it.width - holder.binding.image.width) / 2f } }
内容的提问来源于stack exchange,提问作者Bbeni
相关产品推荐
相关产品推荐

