Android MotionEvent之ACTION_CANCEL

前言

對于Android MotionEvent,我們平時大多關(guān)注的是ACTION_DOWN、ACTION_UP、ACTION_MOVE,本篇將重點分析ACTION_CANCEL 產(chǎn)生的原因及其滑動事件的處理。
通過本篇文章,你將了解到:

1、ACTION_CANCEL 產(chǎn)生的原因
2、手指離開當前View時事件處理
3、手指離開屏幕時事件處理

1、ACTION_CANCEL 產(chǎn)生的原因

從ViewGroup 入手分析

事件分發(fā)是從ViewGroup-->View,因此想要知道View是否收到ACTION_CANCEL,需要從ViewGroup入手,而ViewGroup 分發(fā)的重點即在dispatchTouchEvent(xx)里。


image.png

先看看dispatchTouchEvent(xx) 代碼,之前的文章有詳細分析過,此次重點關(guān)注
ACTION_CANCEL的處理邏輯:

#ViewGroup.java
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                //首次Down事件處理------------>(1)
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
            //ViewGroup是否攔截事件
            ...
            //是否發(fā)送取消事件
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            //尋找接收了Down事件的子View(子布局)
            ...
            if (mFirstTouchTarget == null) {
                //沒有子View(子布局)消費事件,于是事件流轉(zhuǎn)到ViewGroup onTouchEvent
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                //有子View(子布局)消費事件
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    //遍歷消費鏈
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        //已經(jīng)消費過,直接返回
                        handled = true;
                    } else {
                        //判斷是否需要發(fā)送取消事件------------>(2)
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        //分發(fā)事件------------>(3)
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            //如果是取消事件,則子View(子布局)沒必要消費了,因此從消費鏈里摘除
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
            ...
        }
        ...
        return handled;
    }

上面列出了三個重點,將一一分析:
(1)
主要是cancelAndClearTouchTargets(xx)方法:

#ViewGroup.java
    private void cancelAndClearTouchTargets(MotionEvent event) {
        //子View(子布局) 消費了Down事件
        if (mFirstTouchTarget != null) {
            boolean syntheticEvent = false;
            ...
            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                //分發(fā)cancel 事件
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            ...
        }
    }

可以看出,最終調(diào)用了dispatchTransformedTouchEvent(xx)發(fā)送cancel事件。
按照正常流程來說,因為收到Down事件時,mFirstTouchTarget==null,因此此處通常不會執(zhí)行發(fā)送cancel事件。
(2)
resetCancelNextUpFlag(xx)方法,顧名思義:重置取消標記。

#ViewGroup.java
    private static boolean resetCancelNextUpFlag(@NonNull View view) {
        if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) {
            //之前設(shè)置過取消標記,此處重置
            view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
            //返回true
            return true;
        }
        return false;
    }

PFLAG_CANCEL_NEXT_UP_EVENT 標記的作用是記錄View是否被臨時移出Window。比如View.performButtonActionOnTouchDown(xx)處理鼠標相關(guān)的問題,這個值平時也很少用到。

既要判斷resetCancelNextUpFlag(xx)返回值,也要判斷intercepted值:ViewGroup是否攔截并消費了事件。
若是兩者之一有一者滿足,則認為需要發(fā)送cancel事件。

(3)
(2)點僅僅是判斷是否需要發(fā)送cancel事件,真正發(fā)送事件的方法是dispatchTransformedTouchEvent(xx):

#ViewGroup.java
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        final int oldAction = event.getAction();
        //判斷參數(shù)cancel 是否為true
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            //將event 事件設(shè)置為cancel
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                //ViewGroup自己消費
                handled = super.dispatchTouchEvent(event);
            } else {
                //子View(子布局) 消費
                handled = child.dispatchTouchEvent(event);
            }
            //處理后,將action重置
            event.setAction(oldAction);
            return handled;
        }
        //正常事件流程
    }

結(jié)合上述3點可知,在ViewGroup.dispatchTouchEvent(xx)里發(fā)送cancel事件,常用的判斷即是:

1、ViewGroup是否攔截消費了事件,若是則給子View(子布局)發(fā)送cancel事件。
2、發(fā)送給子View cancel事件后,后續(xù)的事件將不會發(fā)給子View。

那么除了ViewGroup 攔截消費事件,還有哪些地方觸發(fā)發(fā)送cancel 事件呢?

ViewGroup 移除View

想象一種場景:

手指在View 上滑動,在此過程中,View 被移出ViewGroup。

來看ViewGroup.remove(xx)的實現(xiàn):

removeView(view)-->removeViewInternal(view)-->removeViewInternal(index, view)

核心功能在removeViewInternal(index, view) 實現(xiàn):

#ViewGroup.java
    private void removeViewInternal(int index, View view) {
        ...

        view.clearAccessibilityFocus();

        //發(fā)送cancel 事件
        cancelTouchTarget(view);
        cancelHoverTarget(view);

        if (view.getAnimation() != null ||
                (mTransitioningViews != null && mTransitioningViews.contains(view))) {
            addDisappearingView(view);
        } else if (view.mAttachInfo != null) {
           //調(diào)用detached
           view.dispatchDetachedFromWindow();
        }
        ...
    }

可以看出,當View 從ViewGroup 移除后,若是它已經(jīng)消費了事件,那么將會收到cancel 事件。
使用如下代碼測試:

        viewGroup.postDelayed(new Runnable() {
            @Override
            public void run() {
                viewGroup.removeAllViews();
            }
        }, 3000);

手指按在View 上,3s后將View 從ViewGroup里移除。

Window 移除View

當調(diào)用WindowManager.removew(view)方法時,最終會調(diào)用到ViewGroup.dispatchDetachedFromWindow(xx)方法:

##ViewGroup.java
    void dispatchDetachedFromWindow() {
        //發(fā)送 cancel 事件
        cancelAndClearTouchTargets(null);
        ...
    }

使用如下代碼測試:

        viewGroup.postDelayed(new Runnable() {
            @Override
            public void run() {
                getWindowManager().removeView(getWindow().getDecorView());
            }
        }, 3000);

手指按在View 上,3s后將View 從Window里移除。

可以看出,觸發(fā)發(fā)送cancel 事件常見的有三種場景:


場景

2、手指離開當前View時事件處理

手指圖

如上圖,手指在View 上按下,View消費了Down事件,此時手指滑出View,并在ViewGroup上滑動,那么整個事件分發(fā)是怎么樣的呢?
通常來說,單指滑動的時候事件分為三個過程:

1、按下事件--->Down 事件。
2、滑動事件--->Move 事件。
3、抬起事件--->Up 事件。

從事件分發(fā)流程可知,當手指按下的時候:

1、若View 消費了Down事件,那么之后的Move、Up事件都會傳遞給View(不考慮ViewGroup攔截情況)。
2、手指在滑動的過程中,滑出了View,在ViewGroup上滑動,此時Move、Up事件依然會傳遞給View。

而View 本身是支持響應(yīng)長按與點擊動作的,若是當前手指還在View 上,那么很好理解,若是當前手指已經(jīng)滑動到ViewGroup上,長按與點擊動作如何響應(yīng)呢?
從直覺上來說,應(yīng)該不響應(yīng)的。
如何做到不響應(yīng)的?理論上來說:因為可以知道每個MotionEvent的觸摸坐標,通過該坐標就可以得知當前MotionEvent是否落在目標View 上,若坐標不在目標View(消費Down事件的View),那么就不響應(yīng)長按與點擊動作。
來看看源碼里是如何處理的:

#View.java
    public boolean onTouchEvent(MotionEvent event) {
        //點擊的坐標
        final float x = event.getX();
        final float y = event.getY();
        ...

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        //處在按下狀態(tài)
                        ...

                        //沒有發(fā)生長按動作
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            //移除長按動作
                            removeLongPressCallback();

                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                //響應(yīng)點擊動作
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }
                        ...
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    ...
                    break;

                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        //重置按下狀態(tài)
                        setPressed(false);
                    }
                    //移除延時單擊動作
                    removeTapCallback();
                    //移除長按動作
                    removeLongPressCallback();
                    ...
                    break;

                case MotionEvent.ACTION_MOVE:
                    ...

                    if (!pointInView(x, y, touchSlop)) {
                        //移除延時單擊動作
                        removeTapCallback();
                        //移除長按動作
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            ////重置按下狀態(tài)
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    ...

                    break;
            }

            return true;
        }

        return false;
    }

A、正常響應(yīng)點擊/長按動作

對于正常情況來說,比如上面的對ACTION_UP 事件的處理。
先說單擊動作:

1、首先需要當前View 處在按下狀態(tài)。
2、其次長按動作沒有發(fā)生。

再說長按動作:

長按動作是個延時執(zhí)行任務(wù),若是沒被取消,則終將會被執(zhí)行。

B、移動時處理點擊/長按動作

由A點可知,若是手指沒有滑出當前View,那么點擊與長按按照正常流程走。若是滑動出了View,在ACTION_MOVE事件處理邏輯如下:

1、一直在監(jiān)測當前的事件點坐標是否還在目標View里,若不在,則進行第2步處理。
2、發(fā)現(xiàn)事件點坐標已經(jīng)不在目標View里,于是移除延時單擊動作、移除長按動作、重置按下狀態(tài)。
3、當目標View收到ACTION_UP事件后,發(fā)現(xiàn)按下狀態(tài)為false,于是不響應(yīng)單擊動作,而延時長按動作已經(jīng)被取消了,也不會響應(yīng)長按動作。

C、總結(jié)

1、一旦View消費了Down事件,那么后續(xù)的Move、Up、Cancel等事件都會交給它處理(ViewGroup沒有攔截消費的前提下)
2、即使滑動超出了當前View的范圍,它依然能夠收到上述事件。
3、若是最終的Up事件不是發(fā)生在當前View之上,那么該View不響應(yīng)單擊與長按動作。
4、若是收到Cancel事件,那么View就不會再響應(yīng)單擊與長按動作,并且后續(xù)的事件將不會再收到。

3、手指離開屏幕時事件處理

當手指滑動離開屏幕時,如下圖:


image

其處理邏輯與手指離開當前View時事件處理是一致的。

本文基于Android 10。
各種事件Demo請移步:相關(guān)代碼演示

您若喜歡,請點贊、關(guān)注,您的鼓勵是我前進的動力

持續(xù)更新中,和我一起步步為營系統(tǒng)、深入學(xué)習Android/Java

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容