在不拦截触摸事件的情况下检测 ViewGroup 中的长按

问题描述

我希望在我的 ViewGroup 中有以下行为:

  • 根本不拦截事件
  • 检测到长按后立即拦截
  • 仅在没有孩子检测到长按时才拦截

我认为,如果一个子节点在其 OnLongClickListener 上返回 true,这将导致 requestDisallowInterceptTouchEvent 向上传递视图层次结构,但这并没有发生...

有什么想法可以正确实施吗?

我目前的解决方案

class InterceptTouchFrameLayout @JvmOverloads constructor(
    context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0,defStyleRes: Int = 0
) : FrameLayout(context,attrs,defStyleAttr,defStyleRes) {

    var onEventListener: ((event: Event,location: Point) -> Unit)? = null
    var longPressDown: DownEvent? = null
    var longPressEventHandled: Boolean = false
    var longPressRunnable = Runnable {
        longPressDown?.let { onEventListener?.invoke(Event.LongPress,it.getPoint()) }
        longPressEventHandled = true
        L.d { "Long Press Event" }
    }

    // -----------------
    // Touch Events
    // -----------------

    /*
    * This method JUST determines whether we want to intercept the motion.
    * If we return true,onTouchEvent will be called and we do the actual
    * event handling there.
    */
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        val intercept: Boolean // true if we want to intercept the touch event

        if (longPressEventHandled) {
            // the long press event has already been send out so we intercept - no child view needs to handle this event anymore!
            intercept = true
        } else {

            when (ev.actionMasked) {
                // Always handle the case of the touch gesture being complete.
                MotionEvent.ACTION_CANCEL,MotionEvent.ACTION_UP -> {
                    cancelLongPressDelayed()
                    intercept = false
                }
                MotionEvent.ACTION_DOWN -> {
                    startLongPressDelayed(ev)
                    intercept = false
                }
                MotionEvent.ACTION_MOVE -> {
                    val inValidDistance = longPressDown?.inClickDistance(ev,touchSlop) ?: false
                    // if we are not in a valid long press distance we cancel the long press
                    if (!inValidDistance) {
                        cancelLongPressDelayed()
                    }
                    intercept = false
                }

                else -> {
                    // In general,we don't want to intercept touch events. They should be
                    // handled by the child view.
                    intercept = false
                }
            }
        }
        L.d { "Intercept: ${ev.actionMasked} | $intercept | $longPressEventHandled" }
        return intercept
    }

    /* Here we actually handle the touch event
     * This method will only be called if the touch event was intercepted in
     * onInterceptTouchEvent
     */
    override fun onTouchEvent(ev: MotionEvent): Boolean {
        // we only intercept the touch event if we have detected a long press - otherwise we always forward the touch events to the children
        // => this function should NEVER be called when longPressEventHandled == false!
        L.d { "ev = ${ev.actionMasked} | longPressEventHandled = $longPressEventHandled" }

        val handled: Boolean
        when (ev.actionMasked) {
            // Always handle the case of the touch gesture being complete.
            MotionEvent.ACTION_CANCEL,MotionEvent.ACTION_UP -> {
                cancelLongPressDelayed()
                handled = true
            }
            else -> {
                handled = false
            }
        }
        return handled
    }

    override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
        super.requestDisallowInterceptTouchEvent(disallowIntercept)
        L.d { "disallowIntercept = $disallowIntercept" }
        cancelLongPressDelayed()
    }

    // -----------------
    // functions
    // -----------------

    private fun startLongPressDelayed(ev: MotionEvent) {
        longPressEventHandled = false
        longPressDown = DownEvent(ev.x.toInt(),ev.y.toInt())
        handler.postDelayed(
            longPressRunnable,(longPressTimeout.toFloat() * 1.1f).toLong() // we add a little bit to the timeout so that childrens long press handlers fire before us
        )
    }

    private fun cancelLongPressDelayed() {
        handler.removeCallbacks(longPressRunnable)
        longPressEventHandled = false
        longPressDown = null
    }

    // -----------------
    // classes / constants
    // -----------------

    private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
    private val longPressTimeout = ViewConfiguration.getLongPressTimeout().toLong()

    class DownEvent(val x: Int,val y: Int) {
        fun inClickDistance(ev: MotionEvent,touchSlop: Int): Boolean {
            val distX = abs(ev.x.toInt() - x)
            val distY = abs(ev.y.toInt() - y)
            return distX <= touchSlop && distY <= touchSlop
        }

        fun getPoint() = Point(x,y)
    }

    enum class Event {
        LongPress
    }
}

我的解决方案有问题

我的 LongClickListeners 中确实有一个带有 ViewGroup 的项目 - 这些项目确实在其侦听器中返回 true 但这无济于事。我不知道如何检测孩子是否也在处理长按 - 在这种情况下,我的视图组应该立即取消它的长按处理...

似乎我需要在 ALL LongClickListeners 中手动执行以下操作:

view.parent.requestDisallowInterceptTouchEvent()

但这意味着视图必须始终知道它在我的自定义视图组中,并且必须手动调用 requestDisallowInterceptTouchEvent - 但这似乎不是很干净。这个问题有更好的解决方案吗?我不能在每个 OnLongClickListener 中手动执行某些操作?我可以在 ViewGroup 中执行什么操作,例如 doesAnyChildViewUnderMotionEventHandleThisLongPress() 之类的函数?有一个 View.hasOnLongClickListeners() 函数,但仅在 API >= 30 中,这对我的用例来说没问题,但似乎没有针对旧 API 的解决方案...

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...