Android事件分发之ACTION_CANCEL机制及作用

如果要查看ACTION_MOVE与ACTION_UP的事件传递机制,查看Android事件分发之ACTION_MOVE与ACTION_UP的传递机制

ACTION_CANCEL产生场景

在阅读ViewGroup事件分发相关源码过程中,有时候会见到ACTION_CANCEL这一事件。那么这一事件是如何产生的呢?按照网上的说法,当手指从当前view移出后,当前view就会收到ACTION_CANCEL这一事件,这一定是正确的吗?下面我们来看两个例子:

例子1:


import android.content.Context
import android.support.constraint.ConstraintLayout
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent

class CustomViewGroup @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {

    var hasInterceptMoveEvent = false
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        // Log.d("TAG", "${ev?.action}:CustomViewGroup onInterceptTouchEvent")
        if (!hasInterceptMoveEvent && ev?.action == MotionEvent.ACTION_MOVE) {
            hasInterceptMoveEvent = true
            return true
        }
        return false
    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        Log.d("TAG", "${getAction(ev?.action)}:CustomViewGroup dispatchTouchEvent")
        return super.dispatchTouchEvent(ev)
        
    }


}

我们自定义一个ViewGroup,覆盖它的onInterceptTouchEvent方法和dispatchTouchEvent方法。在onInterceptTouchEvent方法中我们只拦截了一次MotionEvent.ACTION_MOVE事件。hasInterceptMoveEvent用于控制只拦截一次。而在dispatchTouchEvent方法中,我们打印出当前ViewGroup处理的事件。看下getAction方法定义:

fun getAction(action: Int?): String {
    return when (action) {
        MotionEvent.ACTION_DOWN -> "MotionEvent.ACTION_DOWN"
        MotionEvent.ACTION_MOVE -> "MotionEvent.ACTION_MOVE"
        MotionEvent.ACTION_UP -> "MotionEvent.ACTION_UP"
        MotionEvent.ACTION_CANCEL -> "MotionEvent.ACTION_CANCEL"
        else -> "OTHER"
    }
}

其实就是根据Action对应的Int值转化为字符串,让我们的Log更加直观。

而在ViewGroup内部,我们放置了一个自定义Button。代码如下:


class CusButton @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : Button(context, attrs, defStyleAttr) {
    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
        Log.d("TAG", "${getAction(event?.action)}:CusButton dispatchTouchEvent")
        return true
    }
}

这个自定义Button只是将当前dispatchTouchEvent方法收到的事件打印出来。

在这里插入图片描述
现在我们进行如下操作:

手指按住button,然后移动,移出button外,然后松开。我们看下打印出的Log:

04-23 15:13:48.553 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_DOWN:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.553 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_DOWN:CusButton dispatchTouchEvent
04-23 15:13:48.574 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.574 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_CANCEL:CusButton dispatchTouchEvent
04-23 15:13:48.591 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.607 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.624 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.642 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.658 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.674 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.691 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.708 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.724 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.741 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.758 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.775 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.791 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.808 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.825 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.841 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.859 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.866 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:13:48.867 22928-22928/com.lee.myapplication D/TAG: MotionEvent.ACTION_UP:CustomViewGroup dispatchTouchEvent

我们看到CusButton确实收到了一个ACTION_CANCEL事件,并且在这个事件之后,CusButton并没有收到任何事件。所以我们大概能够猜到:如果ViewGroup拦截了Move事件,那么这个Move事件将会转化为Cancel事件传递给子view。

例子2:

如果我们的ViewGroup不拦截Move事件,那么是否也会产生Cancel事件呢?
我们修改一下CustomViewGroup源码:


class CustomViewGroup @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {

    // var hasInterceptMoveEvent = false
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        // Log.d("TAG", "${ev?.action}:CustomViewGroup onInterceptTouchEvent")
//        if (!hasInterceptMoveEvent && ev?.action == MotionEvent.ACTION_MOVE) {
//            hasInterceptMoveEvent = true
//            return true
//        }
        return false
    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        Log.d("TAG", "${getAction(ev?.action)}:CustomViewGroup dispatchTouchEvent")
        return super.dispatchTouchEvent(ev)

    }


}

onInterceptTouchEvent方法不拦截事件。同样的操作我们再来看下Log:

04-23 15:29:24.089 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_DOWN:CustomViewGroup dispatchTouchEvent
04-23 15:29:24.089 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_DOWN:CusButton dispatchTouchEvent
04-23 15:29:24.104 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:29:24.105 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CusButton dispatchTouchEvent
04-23 15:29:24.121 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:29:24.121 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CusButton dispatchTouchEvent
04-23 15:29:24.139 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:29:24.139 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CusButton dispatchTouchEvent
04-23 15:29:24.154 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:29:24.155 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CusButton dispatchTouchEvent
04-23 15:29:24.171 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:29:24.171 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CusButton dispatchTouchEvent
04-23 15:29:24.188 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:29:24.188 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CusButton dispatchTouchEvent
04-23 15:29:24.205 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:29:24.205 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CusButton dispatchTouchEvent
04-23 15:29:24.221 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:29:24.221 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CusButton dispatchTouchEvent
04-23 15:29:24.233 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:29:24.233 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CusButton dispatchTouchEvent
04-23 15:29:24.250 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:29:24.250 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CusButton dispatchTouchEvent
04-23 15:29:24.268 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:29:24.268 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CusButton dispatchTouchEvent
04-23 15:29:24.275 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CustomViewGroup dispatchTouchEvent
04-23 15:29:24.276 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_MOVE:CusButton dispatchTouchEvent
04-23 15:29:24.276 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_UP:CustomViewGroup dispatchTouchEvent
04-23 15:29:24.276 24484-24484/com.lee.myapplication D/TAG: MotionEvent.ACTION_UP:CusButton dispatchTouchEvent

我们可以看到Button不会收到Cancel事件。即时手指滑出了button,仍然可以收到Move和Up事件。

ACTION_CANCEL作用

我们知道如果某一个子View处理了Down事件,那么随之而来的Move和Up事件也会交给它处理。但是交给它处理之前,父View还是可以拦截事件的,如果拦截了事件,那么子View就会收到一个Cancel事件,并且不会收到后续的Move和Up事件。
下面我们通过源码验证上面这段话:


@Override
public boolean dispatchTouchEvent(MotionEvent ev) {


    boolean handled = false;
    final int action = ev.getAction();
    final int actionMasked = action & MotionEvent.ACTION_MASK;

    // Check for interception.
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null
    ) {
       //----------源码1-----------
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {
        // There are no touch targets and this action is not an initial down
        // so this view group continues to intercept touches.
        intercepted = true;
    }
    

    // Check for cancelation.
    final boolean canceled = resetCancelNextUpFlag(this)
            || actionMasked == MotionEvent.ACTION_CANCEL;

    // Update list of touch targets for pointer down, if needed.
    TouchTarget newTouchTarget = null;
    boolean alreadyDispatchedToNewTouchTarget = false;
    if (!canceled && !intercepted) {
        // ...
    }

    // Dispatch to touch targets.
    if (mFirstTouchTarget == null) {
        // ...
    } else {
        // ---------------源码2----------------
        // Dispatch to touch targets, excluding the new touch target if we already
        // dispatched to it.  Cancel touch targets if necessary.
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
                final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
                //-------------- 源码3---------
                if (dispatchTransformedTouchEvent(
                        ev, cancelChild,
                        target.child, target.pointerIdBits
                    )
                ) {
                    handled = true;
                }

                //-------------- 源码4---------
                if (cancelChild) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                    } else {
                        predecessor.next = next;
                    }
                    target.recycle();
                    target = next;
                    continue;
                }
            }
            predecessor = target;
            target = next;
        }
    }

    return handled;
}



这段代码进行了一定的精简,但是大致流程还是很清楚的。当子view处理完Down事件之后,mFirstTouchTarget不为null,那么是可以走到源码1处的,如果子view没有对ViewGroup进行不拦截的设置,那么disallowIntercept为false,此时会走到onInterceptTouchEvent方法,如果我们拦截了Move事件,那么onInterceptTouchEvent返回true。也就是说intercepted为true;
此时会走到源码2处,遍历每一个将要处理事件的view,alreadyDispatchedToNewTouchTarget表示事件是否已经交给view处理,此时当然是false。到源码3处时,由于intercepted为true,所以cancelChild为true,我们看下dispatchTransformedTouchEvent方法:

    /**
     * Transforms a motion event into the coordinate space of a particular child view,
     * filters out irrelevant pointer ids, and overrides its action if necessary.
     * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
     */
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
         // 。。。
    }

注意,走到dispatchTransformedTouchEvent时,cancel这个参数为true。通过event.setAction(MotionEvent.ACTION_CANCEL);这句话将MotionEvent的Action设置为了ACTION_CANCEL,并且交给子view处理。

通过这里我源码我们知道了ACTION_CANCEL的产生过程。那么为什么产生ACTION_CANCEL后,子view无法收到后续事件了呢?

在源码4处,有这样一段代码:


                if (cancelChild) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                    } else {
                        predecessor.next = next;
                    }
                    target.recycle();
                    target = next;
                    continue;
                }

cancelChild为true,会将当前target节点从链表中删除。那么后续事件到来时,view在mFirstTouchTarget链表中不存在,自然就不会交给它处理了。

FLAG_DISALLOW_INTERCEPT的作用

我们通过上面的代码可以看出,即时是MOVE和UP事件,在传递给子View之前也是可以通过ViewGroup的onInterceptTouchEvent方法拦截的,如果拦截了,那么该事件就会变成Cancel事件传递给子view。
那么是否有办法,子view不让ViewGroup拦截时间呢?

        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }

可以看到disallowIntercept变量为true的时候,会跳过onInterceptTouchEvent方法。换句话如果设置了FLAG_DISALLOW_INTERCEPT这个flag,那么ViewGoup则不会拦截Move和Up事件。

我们再找到设置FLAG_DISALLOW_INTERCEPT这个flag的地方。


    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

这段代码,充分展示了位运算的强大即高效。子View可以通过设置requestDisallowInterceptTouchEvent(true)来达到禁止父ViewGroup拦截事件的目的。

但是需要注意的是,FLAG_DISALLOW_INTERCEPT这个flag无法对Down事件生效。因为在Down时,会清空FLAG_DISALLOW_INTERCEPT

            // Handle an initial down.
      if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

    /**
     * Resets all touch state in preparation for a new cycle.
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }            

  • 28
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
Android 事件分发机制是指在用户与Android设备进行交互时,Android系统如何接收并分发这些事件的过程。事件分发机制包括三个阶段:分发、拦截和处理。 1. 分发阶段:事件Android设备的底层硬件驱动程序开始,通过InputEvent分发给View层。在View层中,事件分为两类:MotionEvent和KeyEvent。MotionEvent表示触摸事件,包括按下、移动、抬起等操作;KeyEvent表示按键事件,包括按下和抬起。 2. 拦截阶段:在事件分发到View层后,会从最上层的View开始进行事件分发,直到有View对事件进行拦截。如果有View对事件进行了拦截,则事件不会继续向下分发,而是由该View进行处理。View是否拦截事件的判断由onInterceptTouchEvent方法完成,如果该方法返回true则表示拦截事件。 3. 处理阶段:如果事件没有被拦截,则会被传递到最底层的View进行处理。在View中,事件处理由onTouchEvent方法完成。如果该方法返回true,则表示事件已经被处理,不再需要继续向下分发;如果返回false,则会继续向上分发直到有View对事件进行拦截。 Android事件分发机制的流程如下: ![image.png](attachment:image.png) 需要注意的是,事件分发机制是一个逆向分发的过程,即从底层向上分发,而不是从顶层向下分发。这是因为底层的View需要先处理事件,如果底层的View不拦截事件事件才能向上分发

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值