面試常問的ACTION_CANCEL到底何時觸發(fā)率挣,滑出子View范圍會發(fā)生什么?

看完本文你將了解:

  • ACTION_CANCEL的觸發(fā)時機
  • 滑出子View區(qū)域會發(fā)生什么椒功?為什么不響應(yīng)onClick()事件

首先看一下官方的解釋:

/**
 * Constant for {@link #getActionMasked}: The current gesture has been aborted.
 * You will not receive any more points in it.  You should treat this as
 * an up event, but not perform any action that you normally would.
 */
public static final int ACTION_CANCEL           = 3;

說人話就是:當前的手勢被中止了,你不會再收到任何事件了讼呢,你可以把它當做一個ACTION_UP事件,但是不要執(zhí)行正常情況下的邏輯节沦。

ACTION_CANCEL的觸發(fā)時機

有四種情況會觸發(fā)ACTION_CANCEL:

  • 在子View處理事件的過程中甫贯,父View對事件攔截
  • ACTION_DOWN初始化操作
  • 在子View處理事件的過程中被從父View中移除時
  • 子View被設(shè)置了PFLAG_CANCEL_NEXT_UP_EVENT標記時
1叫搁,父view攔截事件

首先要了解ViewGroup什么情況下會攔截事件,Look the Fuck Resource Code:

/**
 * {@inheritDoc}
 */
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        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) {
            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;
        }
        ...
    }
    ...
}

有兩個條件

  • MotionEvent.ACTION_DOWN事件或者mFirstTouchTarget非空,也就是有子view在處理事件
  • 子view沒有做攔截惨奕,也就是沒有調(diào)用ViewParent#requestDisallowInterceptTouchEvent(true)

如果滿足上面的兩個條件才會執(zhí)行onInterceptTouchEvent(ev)梨撞。

如果ViewGroup攔截了事件,則intercepted變量為true时肿,接著往下看:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        ...

        // Check for interception.
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // 當mFirstTouchTarget != null螃成,也就是子view處理了事件
                // 此時如果父ViewGroup攔截了事件锈颗,intercepted==true
                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;
        }

        ...

        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            ...
        } else {
            // 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) {
                    ...
                } else {
                    // 判斷一:此時cancelChild == true
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;

                    // 判斷二:給child發(fā)送cancel事件
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    ...
                }
                ...
            }
        }
        ...
    }
    ...
    return handled;
}

以上判斷一處cancelChild為true淋淀,然后進入判斷二中一看究竟:

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設(shè)置成ACTION_CANCEL
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            ...
        } else {
            // 分發(fā)給child
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    ...
}

當參數(shù)cancel為ture時會將event設(shè)置為MotionEvent.ACTION_CANCEL朵纷,然后分發(fā)給child袍辞。

2搅吁,ACTION_DOWN初始化操作
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) {
            // 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.
            // 取消并清除所有的Touch目標
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }
        ...
    }
    ...
}

系統(tǒng)可能會由于App切換谎懦、ANR等原因丟失了up界拦,cancel事件梗劫。

因此需要在ACTION_DOWN時丟棄掉所有前面的狀態(tài),具體代碼如下:

private void cancelAndClearTouchTargets(MotionEvent event) {
    if (mFirstTouchTarget != null) {
        boolean syntheticEvent = false;
        if (event == null) {
            final long now = SystemClock.uptimeMillis();
            event = MotionEvent.obtain(now, now,
                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
            event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
            syntheticEvent = true;
        }

        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            resetCancelNextUpFlag(target.child);
            // 分發(fā)事件同情況一
            dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
        }
        ...
    }
}

PS:在dispatchDetachedFromWindow()中也會調(diào)用cancelAndClearTouchTargets()

3,在子View處理事件的過程中被從父View中移除時
public void removeView(View view) {
    if (removeViewInternal(view)) {
        requestLayout();
        invalidate(true);
    }
}

private boolean removeViewInternal(View view) {
    final int index = indexOfChild(view);
    if (index >= 0) {
        removeViewInternal(index, view);
        return true;
    }
    return false;
}

private void removeViewInternal(int index, View view) {

    ...
    cancelTouchTarget(view);
    ...
}

private void cancelTouchTarget(View view) {
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        if (target.child == view) {
            ...
            // 創(chuàng)建ACTION_CANCEL事件
            MotionEvent event = MotionEvent.obtain(now, now,
                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
            event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
            分發(fā)給目標view
            view.dispatchTouchEvent(event);
            event.recycle();
            return;
        }
        predecessor = target;
        target = next;
    }
}
4瓷翻,子View被設(shè)置了PFLAG_CANCEL_NEXT_UP_EVENT標記時

在情況一種的兩個判斷處:

// 判斷一:此時cancelChild == true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;

// 判斷二:給child發(fā)送cancel事件
if (dispatchTransformedTouchEvent(ev, cancelChild,
    target.child, target.pointerIdBits)) {
    handled = true;
}

resetCancelNextUpFlag(target.child)為true時同樣也會導(dǎo)致cancel,查看代碼:

/**
 * Indicates whether the view is temporarily detached.
 *
 * @hide
 */
static final int PFLAG_CANCEL_NEXT_UP_EVENT        = 0x04000000;

private static boolean resetCancelNextUpFlag(View view) {
    if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) {
        view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
        return true;
    }
    return false;
}

根據(jù)注釋大概意思是妒牙,該view暫時detached湘今,detached是什么意思摩瞎?就是和attached相反的那個旗们,具體什么時候打了這個標記构灸,我覺得沒必要深究。

以上四種情況最重要的就是第一種稠氮,后面的只需了解即可隔披。

滑出子View區(qū)域會發(fā)生什么寂拆?

了解了什么情況下會觸發(fā)ACTION_CANCEL纠永,那么針對問題:滑出子View區(qū)域會觸發(fā)ACTION_CANCEL嗎渺蒿?這個問題就很明確了:不會茂装。

實踐是檢驗真理的唯一標準少态,代碼擼起來:

public class MyButton extends androidx.appcompat.widget.AppCompatButton {

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                LogUtil.d("ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                LogUtil.d("ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                LogUtil.d("ACTION_UP");
                break;
            case MotionEvent.ACTION_CANCEL:
                LogUtil.d("ACTION_CANCEL");
                break;
        }
        return super.onTouchEvent(event);
    }
}

一波操作以后日志如下:

(MyButton.java:32) -->ACTION_DOWN
(MyButton.java:36) -->ACTION_MOVE
(MyButton.java:36) -->ACTION_MOVE
(MyButton.java:36) -->ACTION_MOVE
(MyButton.java:36) -->ACTION_MOVE
(MyButton.java:36) -->ACTION_MOVE
(MyButton.java:39) -->ACTION_UP

滑出view后依然可以收到ACTION_MOVEACTION_UP事件。

為什么有人會認為滑出view后會收到ACTION_CANCEL呢?

我想是因為滑出view后屋摇,view的onClick()不會觸發(fā)了炮温,所以有人就以為是觸發(fā)了ACTION_CANCEL牵舵。

那么為什么滑出view后不會觸發(fā)onClick呢畸颅?再來看看View的源碼:

在view的onTouchEvent()中:

case MotionEvent.ACTION_MOVE:
    // Be lenient about moving outside of buttons
    // 判斷是否超出view的邊界
    if (!pointInView(x, y, mTouchSlop)) {
        // Outside button
        if ((mPrivateFlags & PRESSED) != 0) {
            // 這里改變狀態(tài)為 not PRESSED
            // Need to switch from pressed to not pressed
            mPrivateFlags &= ~PRESSED;
        }
    }
    break;
    
case MotionEvent.ACTION_UP:
    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
    // 可以看到當move出view范圍后没炒,這里走不進去了
    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
        ...
        performClick();
        ...
    }
    mIgnoreNextUpEvent = false;
    break;

1,在ACTION_MOVE中會判斷事件的位置是否超出view的邊界祖很,如果超出邊界則將mPrivateFlags置為not PRESSED狀態(tài)漾脂。

2骨稿,在ACTION_UP中判斷只有當mPrivateFlags包含PRESSED狀態(tài)時才會執(zhí)行performClick()等。

因此滑出view后不會執(zhí)行onClick()形耗。

結(jié)論:
  • 滑出view范圍后激涤,如果父view沒有攔截事件倦踢,則會繼續(xù)受到ACTION_MOVEACTION_UP等事件辱挥。
  • 一旦滑出view范圍晤碘,view會被移除PRESSED標記,這個是不可逆的宠蚂,然后在ACTION_UP中不會執(zhí)行performClick()等邏輯肥矢。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市灭抑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌腾节,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件案腺,死亡現(xiàn)場離奇詭異庆冕,居然都是意外死亡,警方通過查閱死者的電腦和手機劈榨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門访递,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人同辣,你說我怎么就攤上這事拷姿。” “怎么了旱函?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵响巢,是天一觀的道長。 經(jīng)常有香客問我棒妨,道長券腔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任商叹,我火速辦了婚禮卵洗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己脆诉,他們只是感情好,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布啰挪。 她就那樣靜靜地躺著,像睡著了一般锰什。 火紅的嫁衣襯著肌膚如雪霜幼。 梳的紋絲不亂的頭發(fā)上铡恕,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天烘挫,我揣著相機與錄音,去河邊找鬼捉捅。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播七蜘,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起驹溃,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后俱诸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體笼平,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年余黎,在試婚紗的時候發(fā)現(xiàn)自己被綠了可缚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片描姚。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡绊寻,死狀恐怖村缸,靈堂內(nèi)的尸體忽然破棺而出梯皿,到底是詐尸還是另有隱情工碾,我是刑警寧澤渊额,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響奔垦,放射性物質(zhì)發(fā)生泄漏寿弱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一辞居、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧桨踪,春花似錦老翘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至汽纠,卻和暖如春卫键,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背虱朵。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工莉炉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人碴犬。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓絮宁,卻偏偏與公主長得像,于是被迫代替她去往敵國和親服协。 傳聞我的和親對象是個殘疾皇子羞福,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容