Android 中典型的滑动冲突

Android 触控事件在View树中自上而下分发传递,每一层都可以拦截、处理事件并把结果上报。“滑动”操作的本质是View接收触控事件,根据触控坐标的变化移动内容的位置。

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

Android 滑动冲突

理解 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处理,由子ViewonTouchEvent()中进行水平移动操作。目前都是正常的,但如果手指开始垂直滑动,会发现父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("ViewGroup 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("ViewGroup 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("ViewGroup onInterceptTouchEvent ACTION_UP")
                false
            }

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

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

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

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

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

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        LogUtil.d("ViewGroup event.rawX = ${ev?.rawX} event.rawY = ${ev?.rawY}")
        var result = super.dispatchTouchEvent(ev)
        LogUtil.d("ViewGroup 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("View onTouchEvent  ACTION_DOWN")
                // 记录 ACTION_DOWN 事件坐标
                xPoint = event.rawX
                yPoint = event.rawY
            }

            MotionEvent.ACTION_MOVE -> {
                var offsetX = event.rawX - xPoint
                LogUtil.e("View 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("View onTouchEvent  ACTION_UP")
            }
        }
        LogUtil.e("View onTouchEvent  $result")
        return result
    }

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