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

如何处理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
    }
}

问题根因

  1. 触摸事件拦截冲突:拖动时RecyclerView默认会拦截MOVE事件用于列表滚动,没有主动通知父布局放弃拦截,导致子View收不到完整的拖动事件序列
  2. 坐标参考系错误:计算默认居中位置时错误叠加了container.x(容器相对于其父布局的X偏移),但ImageView的x坐标是相对于直接父布局(即container)的,两者参考系不统一,导致位置计算完全错误
  3. 拖动逻辑错误:ACTION_MOVE阶段每次都更新起始位置,导致偏移量计算累加异常,拖动距离被截断

修复方案

  1. 在ACTION_DOWN触发时调用parent.requestDisallowInterceptTouchEvent(true),通知RecyclerView不要拦截当前触摸事件序列,拖动结束后恢复拦截状态
  2. 统一坐标参考系:使用event.rawX获取屏幕绝对坐标计算拖动偏移,避免不同View坐标系的转换问题;居中位置直接基于container和image的宽高计算,无需叠加容器的父布局偏移
  3. 修正拖动逻辑:仅在ACTION_DOWN时记录ImageView初始位置和按下坐标,MOVE阶段基于初始值计算偏移,同时增加边界限制,不让ImageView拖出容器范围
  4. 回弹逻辑增加属性动画,优化交互体验

修复后的触摸监听代码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 15:15:33