Android中ACTION_CANCEL的觸發時機

轉載請以連接形式標明出處: 本文出自:103style的博客java

code base on android-28android


目錄

  • 結論
  • 測試代碼
  • 源碼分析

結論

  • 當子控件處理了 ACTION_DOWN 事件, 可是父控件攔截了 ACTION_MOVE 事件,子控件不會收到ACTION_CANCEL
  • 當子控件處理了 ACTION_DOWN 事件, 可是父控件攔截了 ACTION_UP 事件,當再次點擊父控件,子控件會收到 ACTION_CANCEL

即 子控件 在處理了 ACTION_DOWN 以後,若是沒有收到 ACTION_UP 事件,則在下次點擊父控件的時候會收到 ACTION_CANCEL 事件。bash


測試代碼

新建一個項目, 修改activity_main.xml以下:ide

<?xml version="1.0" encoding="utf-8"?>
<包名.TestLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <包名.TestTextView
        android:id="@+id/test"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_gravity="center"
        android:background="@color/colorPrimary" />
</包名.TestLayout>
複製代碼

添加 TestLayout.javaTestTextView.java源碼分析

public class TestLayout extends FrameLayout {
    public TestLayout(@NonNull Context context) {
        super(context);
    }
    public TestLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    public TestLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (
                ev.getAction() == MotionEvent.ACTION_MOVE
                        || ev.getAction() == MotionEvent.ACTION_UP
        ) {
            return true;
        }
        return super.dispatchTouchEvent(ev);
    }
}
複製代碼
public class TestTextView extends TextView {
    private static final String TAG = "TestTextView";
    public TestTextView(Context context) {
        super(context);
    }
    public TestTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    public TestTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, System.currentTimeMillis() + " onTouchEvent: action = " + event.getAction());
        return true;
    }
}
複製代碼
  • 首先咱們註釋 TestLayout|| ev.getAction() == MotionEvent.ACTION_UP 這句,運行點擊中間的子控件,而後移動一下,再擡起,而後重複一次,打印日誌以下:測試

    TestTextView: 1571728312400  onTouchEvent: action = 0
    TestTextView: 1571728314213  onTouchEvent: action = 1
    TestTextView: 1571728316088  onTouchEvent: action = 0
    TestTextView: 1571728318113  onTouchEvent: action = 1
    複製代碼

    發現只收到 ACTION_DOWNACTION_UP 事件。ui

  • 而後咱們把 TestLayout|| ev.getAction() == MotionEvent.ACTION_UP 這句恢復,重複操做,發現日誌圖下:spa

    TestTextView: 1571728458713  onTouchEvent: action = 0
    TestTextView: 1571728460379  onTouchEvent: action = 3
    TestTextView: 1571728460379  onTouchEvent: action = 0
    複製代碼

    根據時間戳咱們知道在第二次點擊時,纔會先收到ACTION_CANCEL,再收到 ACTION_DOWN日誌


源碼分析

  • 第一種只攔截 ACTION_MOVE 咱們很好分析,由於事件是從上面一層一層傳下來的,當上層攔截了 消耗了 ACTION_MOVE 事件以後,就不會往下傳遞了,咱們的操做是由一個 ACTION_DOWN 多個 ACTION_MOVE 和 一個 ACTION_UP 組成的, 因此子控件只能收到 ACTION_DOWNACTION_UPcode

  • 第二種由於攔截了ACTION_MOVEACTION_UP 事件,因此子控件只能收到 ACTION_DOWN

而後在每次收到 ACTION_DOWN 的時候,ViewGroup 都會去清除以前的狀態。 即下面的 cancelAndClearTouchTargets(ev)dispatchTransformedTouchEvent(...)child.dispatchTouchEvent(event),即將 MotionEvent.ACTION_CANCEL 傳給子控件 。

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);//清除以前的狀態
        }
    }
}
private void cancelAndClearTouchTargets(MotionEvent event) {
    if (mFirstTouchTarget != null) {
        boolean syntheticEvent = false;
        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            //傳遞ACTION_CANCEL給子View
            dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
        }
    }
}
private boolean dispatchTransformedTouchEvent(...) {
    final boolean handled;
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        // cancel 爲 true
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            //傳遞ACTION_CANCEL給子View
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
}
複製代碼

那這個 mFirstTouchTarget 是什麼,何時被賦值的呢?

private TouchTarget mFirstTouchTarget;
private static final class TouchTarget {
    public View child;
    public TouchTarget next;
}
複製代碼

mFirstTouchTarget 是一個保存處理了事件的 子View鏈表 結構。

賦值也在 dispatchTouchEvent 中,在 dispatchTransformedTouchEvent(...) 中 判斷子View是否處理了事件,處理了則添加到 mFirstTouchTarget 鏈表的頭部。

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (onFilterTouchEventForSecurity(ev)) {
        if (!canceled && !intercepted) {
            if (...) {
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final View[] children = mChildren;
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(...);
                        final View child = getAndVerifyPreorderedView(...);
                        //判斷子View是否處理了事件
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            //把處理了事件的子View 添加到鏈表頭部
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
    ...
}
//判斷子View是否處理了事件
private boolean dispatchTransformedTouchEvent(...) {
    final boolean handled;
    final int oldAction = event.getAction();
    final MotionEvent transformedEvent;  
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    transformedEvent.recycle();
    return handled;
}
//添加到mFirstTouchTarget鏈表的頭部
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
複製代碼

因此流程大體就是, 當第一次點擊 TestLayout, 因爲 TestTextView 消耗了 ACTION_DOWN 事件,而後被保存在 TestLayoutmFirstTouchTarget 鏈表中, 在第二次點擊的時候,經過 cancelAndClearTouchTargets 方法,遍歷 mFirstTouchTarget 鏈表,將 ACTION_CANCEL 事件傳遞給子view。


若是以爲不錯的話,請幫忙點個讚唄。

以上


掃描下面的二維碼,關注個人公衆號 Android1024, 點關注,不迷路。

Android1024
相關文章
相關標籤/搜索