天天看點

千裡馬 android framework之MotionEvent.ACTION_CANCEL怎麼産生-讨厭的android觸摸面試題

hi,粉絲朋友!

大家對于MotionEvent.ACTION_CANCEL這個cancel事件是不是感覺又熟悉又陌生,熟悉是因為經常在onTouch識别觸摸事件時候會把它和ACTION_UP放在一塊處理,基本停留在字面意思了解為 “”取消“”

新課程優惠擷取請加入qq群:422901085

Android手機大廠Framework系統-Input系統專題實戰課

[入門課,實戰課,跨程序專題

ps需要學習深入framework課程和課程優惠

ACTION_CANCEL觸發場景和原因:

customTextView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        Log.i("test2"," onTouch motionEvent = " + motionEvent);
        if (motionEvent.getAction() == MotionEvent.ACTION_CANCEL || motionEvent.getAction() == MotionEvent.ACTION_UP ) {
            Log.i("test2"," onTouch ACTION_CANCEL = " ,new Exception());
        }
        return true;
    }
});
           

是以這裡本節課就來帶大家深入了解這裡ACTION_CANCEL

首先我們來分析應用觸摸最常見的ACTION_CANCEL情況:

千裡馬 android framework之MotionEvent.ACTION_CANCEL怎麼産生-讨厭的android觸摸面試題

這個就是我們常見的一個父布局MyLayout,它裝載了2個子控件textview1和textview2.假設一個觸摸事件到來,我們知道觸摸事件的傳遞順序就是:父布局 – 》 子布局

這樣的一個順序,也就是其實事件父布局是具有完全的決策權利來是否給子布局,可以通過方法onInterceptTouchEvent。

接下來我們又來看看MotionEvent,一個正常完整觸摸應該是怎麼個順序呢?

ACTION_DOWN – > ACTION_MOVE --> ACTION_UP

是以說我們控件正常事件處理就是最少要有DOWN --》UP兩個事件,才代表事件介紹,那麼你會問如果隻有DOWN,和MOVE是否可以呢?答案:當然是不可以的

為啥呢?

大家可以想一下這樣一個場景,你是一個按鈕,來了觸摸事件DOWN了後,你把按鈕變成selected狀态了,你本來一直等值來個UP事件來變回正常狀态,如果系統發生依次你沒有收到UP會怎麼樣呢?那就是你的應用按鈕就永遠處于選中狀态無法取消,一直到程序關閉(當然說是正常你沒有特别處理情況)。

是以有了以上基礎後,大家就知道有了DOWN事件一般都要有UP事件才算完整,但是有一些場景他就是可能收到了DOWN,後面收不到UP了怎麼辦?

比如看如下例子:

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("test2","onInterceptTouchEvent ev = " + ev);
        if (ev.getAction() == MotionEvent.ACTION_MOVE ) {//我們不攔截DOWN事件,但是攔截MOVE事件,父布局一攔截,子控件就會收到ACTION_CANCEL
            return true;
        }
        return super.onInterceptTouchEvent(ev);
    }
           

這種情況我們父布局隻并沒有攔截DOWN事件,是以DOWN傳遞給你textview1,但是因為在MOVE時候我們父布局攔截了,即說明事件不再傳遞給子控件了textview1,那麼這個是不是就是textview1收到了DOWN,但是收不到UP情況,不完整了,那該怎麼辦?

這個時候其實就是今天重點介紹的ACTION_CANCEL出廠,他就是來幫忙解決上面的因為父布局中間進行了觸摸事件攔截,但是子布局又要一個完整觸摸過程,那麼就需要傳遞一個觸摸事件給子控件,那麼傳遞什麼合适?

大家肯定會想UP最合适,UP不就可以了?但是你要想想使用者還沒有UP啊?如果傳遞了UP是不是也可能會有問題,是以這時候就是傳遞ACTION_CANCEL,它代表是取消,即代表這個觸摸事件到此被取消結束了。控件收到需要自己做對應的掃尾工作,保證事件完整。

上面我們已經對ACTION_CANCEL觸發場景和原因已經清楚,接下來看看源碼是怎麼處理的:

ACTION_CANCEL出現源碼分析

app代碼:

layout.xml

android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:paddingTop="100dp"
        >
        <TextView
            android:id="@+id/custom_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello CustomLayout !"
            ></TextView>

    </com.example.anrdemo.CustomLayout>

           

代碼端對onTouch的事件進行監聽:

TextView customTextView = (TextView)findViewById(R.id.custom_text);
        customTextView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                Log.i("test2"," onTouch motionEvent = " + motionEvent);
                if (motionEvent.getAction() == MotionEvent.ACTION_CANCEL || motionEvent.getAction() == MotionEvent.ACTION_UP ) {
                //變成了CANCEL後列印一下堆棧,追蹤架構哪裡進行了改變
                    Log.i("test2"," onTouch ACTION_CANCEL = " ,new Exception());
                }
                return true;
            }
        });
           
2021-11-27 13:50:06.911 7010-7010/com.example.anrdemo I/test2:  onTouch ACTION_CANCEL = 
    java.lang.Exception
        at com.example.anrdemo.MainActivity$2.onTouch(MainActivity.java:46)
        at android.view.View.dispatchTouchEvent(View.java:13949)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
        at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:465)
        at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1849)
        at android.app.Activity.dispatchTouchEvent(Activity.java:4011)
        at com.example.anrdemo.MainActivity.dispatchTouchEvent(MainActivity.java:72)
        at androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:69)
        at androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:69)
        at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:423)
        at android.view.View.dispatchPointerEvent(View.java:14212)
        at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:5652)
        at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:5455)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4958)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5011)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4977)
        at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:5117)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4985)
        at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:5174)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4958)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5011)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4977)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4985)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4958)
        at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7675)
        at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:7644)
        at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:7605)
        at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:7800)
        at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:188)
        at android.view.InputEventReceiver.nativeConsumeBatchedInputEvents(Native Method)
        at android.view.InputEventReceiver.consumeBatchedInputEvents(InputEventReceiver.java:178)
        at android.view.ViewRootImpl.doConsumeBatchedInput(ViewRootImpl.java:7751)
        at android.view.ViewRootImpl$ConsumeBatchedInputRunnable.run(ViewRootImpl.java:7824)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:967)
        at android.view.Choreographer.doCallbacks(Choreographer.java:791)
        at android.view.Choreographer.doFrame(Choreographer.java:719)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:952)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
           

這裡就列印出來了架構調用到onTouch中MotionEvent變成CANCEL的流程,友善我們進行源碼分析。

具體大家可以自己取追蹤,這裡我這邊列出結果:

base/core/java/android/view/ViewGroup.java

// 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;
                }
            } 
           

這裡onInterceptTouchEvent會調用到我們CustomLayout的onInterceptTouchEvent,我們識别到了如果MOVE就return true,是以intercepted這個時候也是true。

final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits))
           

這裡cancelChild就變成了true,是以

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

        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {//根據前面cancelChild變成了CANCEL
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
//---省略部分
}
        
           

從以上可以看出,結合我們Input專題學習知識,我們知道觸摸事件InputDispatcher傳遞給App實際還是ACTION_MOVE,但是app程序ViewGroup的政策把事件變成了ACTION_CANCEL來保證事件的完整性。

總結流程圖:

千裡馬 android framework之MotionEvent.ACTION_CANCEL怎麼産生-讨厭的android觸摸面試題