Android中典型的滑动冲突

AndroidView树中,事件分发自上而下,每一层都可以拦截事件、处理事件,并把处理结果上报。常见的滑动操作其实就是View接收触控事件,再根据触控坐标的变化去移动指定View的位置。

ViewViewGroup嵌套组合构成完整的用户界面,其中有一种典型的滑动冲突,即父View子View都要接收触控事件,但是父View只上下滑动,子View只左右滑动,如下图所示:

滑动冲突

如果了解Android的事件分发、拦截机制,可以很清晰的实现这种类型的滑动操作。以本例来说,蓝色ViewGroup应该在触控事件MotionEvent到达时,判断相邻的2次事件是否是垂直的滑动事件,如果是,就拦截,调用自己的onTouchEvent()来移动本View。如果不是垂直滑动事件,就不拦截,将事件分发给红色子View,它会调用自身的onTouchEvent()来移动自己。

原理很简单,但具体的实现还有一些细节需要处理,尤其是下面的这些问题。

ACTION_DOWN事件的处理

ACTION_DOWN是一个触控事件序列的开始,也是最特殊的一个事件,它必须被处理,而它应该何时被处理,何时被拦截,则是解决滑动冲突的关键逻辑。

之前在关于事件分发、拦截机制的文章里提过,ViewGroup一旦拦截ACTION_DOWN,如果不处理则同序列后续事件会在上层被直接拦截,如果处理则同序列后续事件会直接调用该ViewGrouponTouchEvent()进行处理,而不会再向下分发。这样的话,只要ViewGroup拦截了ACTION_DOWN事件,无论它是否处理,其子View都不可能接收到同事件序列的后续事件,所以绝不可以在ViewGroup中拦截ACTION_DOWN事件。

既然父View不可以拦截ACTION_DOWN事件,而它又必须被处理,也就是说子View必须处理ACTION_DOWN事件。

非ACTION_DOWN事件的拦截

同样提到的,如果一个事件序列的某个非ACTION_DOWN事件被ViewGroup拦截,无论它是否被处理,这整个事件序列的后续事件都会跳过该ViewGrouponInterceptTouchEvent(),而直接调用其onTouchEvent()来处理。也就是说,一旦父View判断这是一个垂直滑动操作而开始拦截事件,则无论它有没有处理后续的事件,子View都不可能再接收到同序列事件了。在视图上表现为,手指首先垂直滑动,父View垂直移动,这是正常的,如果手指开始左右滑动,会发现子View没有左右移动,是因为这些触控事件并没有被传递到子View

另一种情况,在一次触控事件序列中,如果父View判断相邻的事件是左右滑动,它就不会拦截,并将这些事件发送给子View处理,子View会在onTouchEvent()中进行水平移动操作,目前都是正常的,但如果手指开始垂直滑动,会发现父View判断出这是垂直滑动,而开始拦截事件,引起相对应的垂直移动,但子View此时却收不到事件了。

我认为这种逻辑很奇怪,一次完整的触控事件序列应该只分配给一个View进行处理,当手指落下并开始滑动时就应该判断出用户的意图是滑动哪个具体的View,而在后续的滑动事件到达时,不应该再由另一个View来处理这些事件。所以,在确定此事件序列已经交给子View处理的情况下,要使用requestDisallowInterceptTouchEvent(true)禁止父View再拦截本事件序列的后续事件。

代码示例

首先是蓝色的ViewGroup

class EventViewGroup : RelativeLayout {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    /**
     * 用于判断是否拦截的触控坐标记录
     */
    private var xIntercept = 0f
    private var yIntercept = 0f

    /**
     * 用于判断滑动的触控坐标记录
     */
    private var xPoint = 0f
    private var yPoint = 0f

    // ViewGroup判断是否要拦截事件,否则所有事件均交给View处理
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        var result = when (ev?.action) {
            MotionEvent.ACTION_DOWN -> {
                LogUtil.e("$id onTouchEvent  ACTION_DOWN")
                // 记录ACTION_DOWN事件坐标
                xIntercept = ev.rawX
                yIntercept = ev.rawY

                xPoint = ev.rawX
                yPoint = ev.rawY
                // 不拦截ACTION_DOWN事件
                false
            }

            MotionEvent.ACTION_MOVE -> {
                LogUtil.d("$id onInterceptTouchEvent  ACTION_MOVE")
                // 只拦截竖向事件
                val moveY = ev.rawY - yIntercept
                val moveX = ev.rawX - xIntercept
                xIntercept = ev.rawX
                yIntercept = ev.rawY
                if (moveX == moveY && moveX == 0f) {
                    // 收到重复事件,不拦截
                    false
                } else if (Math.abs(moveY) >= Math.abs(moveX)) {
                    // 垂直距离大于水平距离,拦截
                    true
                } else {
                    // 其余情况不拦截
                    false
                }
            }

            MotionEvent.ACTION_UP -> {
                LogUtil.d("$id onInterceptTouchEvent  ACTION_UP")
                false
            }

            else -> false
        }
        LogUtil.d("$id onInterceptTouchEvent  $result")
        return result
    }

    /**
     * 处理事件
     */
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        // 处理所有收到的事件
        var result = true
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                LogUtil.d("$id onTouchEvent  ACTION_DOWN")
            }

            MotionEvent.ACTION_MOVE -> {
                var offsetY = event.rawY - yPoint
                var offsetX = event.rawX - xPoint
                LogUtil.d("$id onTouchEvent  ACTION_MOVE offsetX = $offsetX offsetY = $offsetY")
                // 垂直移动
                offsetTopAndBottom(offsetY.toInt())
                yPoint = event.rawY
                xPoint = event.rawX
            }

            MotionEvent.ACTION_UP -> {
                LogUtil.d("$id onTouchEvent  ACTION_UP")
            }
        }

        LogUtil.d("$id onTouchEvent  $result")
        return result
    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        LogUtil.d("$id event.rawX = ${ev?.rawX} event.rawY = ${ev?.rawY}")
        var result = super.dispatchTouchEvent(ev)
        LogUtil.d("$id dispatchTouchEvent  $result")
        return result
    }
}

然后是红色的View

class EventView : ImageView {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    /**
     * 用于判断滑动的触控坐标记录
     */
    private var xPoint = 0f
    private var yPoint = 0f

    /**
     * 处理事件
     */
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        // 处理所有收到的事件
        var result = true
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                LogUtil.e("$id onTouchEvent  ACTION_DOWN")
                // 记录ACTION_DOWN事件坐标
                xPoint = event.rawX
                yPoint = event.rawY
            }

            MotionEvent.ACTION_MOVE -> {
                var offsetX = event.rawX - xPoint
                LogUtil.e("$id onTouchEvent  ACTION_MOVE offsetX = $offsetX")
                // 水平移动
                if (offsetX != 0f) {
                    offsetLeftAndRight(offsetX.toInt())
                    // 不允许父View再拦截本事件序列的后续事件
                    parent.requestDisallowInterceptTouchEvent(true)
                }
                xPoint = event.rawX
                yPoint = event.rawY
            }

            MotionEvent.ACTION_UP -> {
                LogUtil.e("$id onTouchEvent  ACTION_UP")
            }
        }
        LogUtil.e("$id onTouchEvent  $result")
        return result
    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        var result = super.dispatchTouchEvent(ev)
        LogUtil.e("$id dispatchTouchEvent  $result")
        return result
    }
}
arrow_upward