Android View 事件分發:處理滑動衝突 | 內部、外部事件攔截

Android View 事件分發:處理滑動衝突 | 內部、外部事件攔截

Overview of Content

以下的 Android 源碼會採用 android-10.0.0_r1 的源碼

本篇文章將深入解析 Android 點擊事件的傳遞與處理機制,幫助您全面了解 View 和 ViewGroup 在事件分發與攔截中的行為,並掌握事件衝突解決的核心技術。

透過實例講解,我們會探討從點擊事件的初始分發,到多層級視圖中事件處理的邏輯,包括內部與外部攔截策略。無論您是 Android 開發的初學者,還是希望精進的開發者,都能透過本篇內容提升對事件機制的理解,為構建流暢且高效的交互體驗奠定堅實基礎。


Android 點擊事件傳入

點擊事件是從 Activity(起點) 透過一層層傳遞至 View(終點) 中,下圖是一個示意圖,至於若對建構 View 有興趣可以參考 LayoutInflater 分析

最終傳入 View 中讓其處理點擊事件 (從最外部 Acitivty -> Window -> View)

Android View 事件分發概念

● 由於 View 的分發實作細節比較多,但我們要關注的 "分發",主要是由 3 個方法來完成

分發重點方法功能返回意義
dispatchTouchEvent : boolean由上層 View 被觸發,傳遞至目標 View返回結果會由 onTouchEvent、子 View 的 dispatchTouchEvent 影響 (true: 被處理)
onInterceptTouchEvent : boolean在當前 View 中,用來判斷是否攔截某個事件返回結果表示該事件是否被攔截 (true: 被攔截)
onTouchEvent : boolean當前 View 已經攔截,開始處理是建返回結果代表該事件是否被消耗 (true: 被處理)

● 以下是 View 事件分發的偽程式,可以很好的描述出 View 事件分發概念


// View 偽程式

public boolean dispatchTouchEvent(MotionEvent e) {
    bool isEventConsume = false;

    if(onInterceptTouchEvent(e)) {        // 是否攔截分發 
        isEventConsume = onTouchEvent(e);
    } else {
        isEventConsume = child.dispatchTouchEvent(e);
    }

    return isEventConsume;
}

A. 可以看出分發順序 dispatchTouchEvent -> onInterceptTouchEvent,在依照是否消耗來決定之後的走向

B. dispatchTouchEvent 的遞迴調用(若事件沒有被消耗,就會往子成員 child 繼續呼叫),直到找到消耗事件的 View

Android View 是一個 組件模式裝飾模式 的混合設計模式,它擁有加強或是弱化抽象,還有遞歸抽象… 等等程式設計特性

● 若該 View 已經攔截點擊事件,則會觸發 onTouch

A. onTouch 若沒消費該事件,才會傳遞給 onTouchEvent 方法

B. onTouchEvent 內才會有 onClick 事件

Activity view 接收點擊事件

● Activity (AppCompatActivity)、Windows (PhoneWindow) 之間的關係可以參考另一篇 Activity 布局,下圖表示了 AppCompatActivity 如何與 PhoneWindow 產生關係

點擊事件從 Activity#dispatchTouchEvent 開始分析,一路會分析進入 PhoneWindowDecorWindow,最後到達 DecorView#ViewGroup (最頂層 ViewGroup) 中的 dispatchTouchEvent 方法

/**
 *  Activity.java
 */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        // 這裡的 getWindow 就是 PhoneWindow 類
        // @ 分析 superDispatchTouchEvent
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

// -------------------------------------------------------------
/**
 * PhoneWindow.java
 */    
    private DecorView mDecor;

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }


// -------------------------------------------------------------
/**
 * DecorView.java
 * 
 * DecorView 繼承於 FrameLayout,而 FrameLayout 並沒有 Overidde 
 * dispatchTouchEvent 方法,所以必須往它的父類尋找
 */ 
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }


// -------------------------------------------------------------
/**
 * ViewGroup.java
 * 
 * ViewGroup 繼承於 View
 * ViewGroup 有重寫 dispatchTouchEvent,所以不用繼續往 View 去
 */ 
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ... 省略
    }

● 從這裡可以看出 事件分發會由 PhoneWindow 中的 DecorView 開始

Activity & View 關係圖


Android View 事件處理

透過上面分析,我們就知道點擊 事件是如何傳遞至 DecorView#ViewGroup 中,這裡我們會再分 ViewGroup 點擊事件來分析


  • 先了解幾個 MotionEvent

MotionEvent 事件動作其他
ACTION_DOWN手指下壓又份為攔截、不攔截
ACTION_UP手指抬起事件結束
ACTION_MOVE在螢幕滑動會被 多次觸發
ACTION_CANCEL事件取消事件被上層攔截時候觸發

ViewGroup 處理 ACTION_DOWN:ChildView 攔截

Down 事件只會觸發一次 (單點觸控,多點觸控就不只一次),攔截就是該 ViewGroup 自己處理事件,不會對 ChildView 分發

● 每個點擊事件都是以 Action Down 開始,細節請注意以下的註解,而它主要做的事情有 (這裡會先列出主要的處理項目)

A. 清除先前的事件:注意 resetTouchState 方法,它會在 ViewGroup#dispatchTouchEvent 事件是 ACTION_DOWN 時執行

ViewGroup 中有一個 member 是 TouchTarget:它用來串接該點擊事件

// ViewGroup.java
    
// 從該 ViewGroup 開始,往下串接點擊的目標
private TouchTarget mFirstTouchTarget;

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ... 省略部分

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {

        // 取得目前行為
        final int action = ev.getAction();
        // 與 Mask 進行 and 操作,取得真正的 Action
        final int actionMasked = action & MotionEvent.ACTION_MASK;


        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 在 Action Down 時才執行 reset

            // @ 追蹤 cancelAndClearTouchTargets 方法
            cancelAndClearTouchTargets(ev);

            // @ 追蹤 resetTouchState 方法
            resetTouchState();
        }

    }

    ... 省略部分

    return handled;
}

private void resetTouchState() {
    // @ 查看 clearTouchTargets 方法
    clearTouchTargets();
    resetCancelNextUpFlag(this);

    // 清除 FLAG_DISALLOW_INTERCEPT、允許 ViewGroup 攔截
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

// 清除第一個點擊 View 串列的所有事件
private void clearTouchTargets() {
    // TouchTarget 是單向鏈表
    TouchTarget target = mFirstTouchTarget;
    if (target != null) {
        do {
            TouchTarget next = target.next;
            // 回收 TouchTarget 方便覆用
            target.recycle();
            target = next;
        } while (target != null);

        // @ 將成員 mFirstTouchTarget 至為 null 
        mFirstTouchTarget = null;
    }
}

● 我們來試運算,使用上面的 ~& 公式,是否能清除 FLAG_DISALLOW_INTERCEPT 這個 Flag 值


## 假設 mGroupFlags 為 0x8A (0b1000 1010)
## FLAG_DISALLOW_INTERCEPT 為 0x80 (這是假設值 0b1000 0000) 
## 首先反向
~FLAG_DISALLOW_INTERCEPT
## Ans: 0b0111 1111

## 做 And 運算
mGroupFlags & (0b0111 1111)
## (0b1000 1010) & (0b0111 1111)
Ans: 0b0000 1010

B. 是否會呼叫 ViewGroup#onInterceptTouchEvent 方法:會有兩個條件,再加上一個 FLAG 判斷:

A. 目前是 ACTION_DOWN 事件

B. 已經有 ChildView 處理這個點擊事件 (如果有子 View 處理事件就會給 mFirstTouchTarget 賦值)

C. 目前 ViewGroup 是否被禁止攔截 (一般 ViewGroup 接收到 ACTION_DOWN 時,如果沒有禁止攔截的話,就會執行 onInterceptTouchEvent 方法)

FLAG_DISALLOW_INTERCEPT 判斷

可以透過 ViewGroup#requestDisallowInterceptTouchEvent 方法控制,通常是 ChildView 在要求 Parent 不要攔截使用

同時如果 ChildView 如果使用 requestDisallowInterceptTouchEvent 時也要同時禁止 ViewGroup 對 ACTION_DOWN 的攔截 (請看第一點)


// ViewGroup.java
    
// 從該 ViewGroup 開始,往下串接點擊的目標
private TouchTarget mFirstTouchTarget;

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ... 省略部分

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {

        // 取得目前行為
        final int action = ev.getAction();
        // 與 Mask 進行 and 操作,取得真正的 Action
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // Action down 時 ...初始化 FLAG_DISALLOW_INTERCEPT

        final boolean intercepted;
        // 這裡有兩個判斷 決定是否呼叫 ViewGroup 自己的 onInterceptTouchEvent 方法
        // 1. 目前是 ACTION_DOWN 
        // 2. 已經有子 View 處理這個點擊事件 
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {

            // 判斷是否禁止 攔截
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {

                // 呼叫自身的 onInterceptTouchEvent 方法
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }

    }

    ... 省略部分

    return handled;
}

C. 若 ViewGroup 自身沒有攔截,就會 遞迴 ChildView:並一一執行 ViewGroup#dispatchTransformedTouchEvent 分發給每個 ChildView 處理

  • 若 ChildView 消耗事件則返回 true,則跳出迴圈

  • 否則往下一個 ChildView 詢問


// ViewGroup
    
// 從該 ViewGroup 開始,往下串接點擊的目標
private TouchTarget mFirstTouchTarget;

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ... 省略部分

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {

        // 取得目前行為
        final int action = ev.getAction();
        // 與 Mask 進行 and 操作,取得真正的 Action
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // Action down 時 ...初始化 FLAG_DISALLOW_INTERCEPT

        final boolean intercepted;
        // 判斷自身 ViewGroup 是否可以攔截事件

        // 目前假設 ViewGroup 不攔截
        if (!canceled && !intercepted) {
            ...

            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                ...

                if (newTouchTarget == null && childrenCount != 0) {
                    ...
                    // 重新排列在該 ViewGroup 中的 ChildView 們的順序
                    final ArrayList<View> preorderedList = 
                                    buildTouchDispatchChildList();
                    ...

                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);

                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);

                        ...

                        // 是否動畫中 canReceivePointerEvents
                        // 點擊是否在 child 元素內 isTransformedTouchPointInView 
                        if (!child.canReceivePointerEvents()
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        // 獲取點擊中的 View 
                        // 新的 View 事件才會返回  非 null
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // Give it the new pointer in addition to the ones it is handling.
                            // Child View 已經接收到點擊
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }


                        // @ 重點在 dispatchTransformedTouchEvent 方法
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // ChildView 處理事件成功

                            ... 省略部分

                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();

                            // @ addTouchTarget 方法
                            // 內會賦予 mFirstTouchTarget 值
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            // 跳出迴圈
                            break;
                        }


                    }
                }

            }
        }

    }

    ... 省略部分

    return handled;
}

● 從這裡可以看出來 View 是一個多元樹,並且在這裡使用 責任鏈 OOP 設計

按照螢幕上的 Z 軸,重新排列 ViewGroup 中 ChildView 的順序


// ViewGroup

public ArrayList<View> buildTouchDispatchChildList() {
    return buildOrderedChildList();
}

ArrayList<View> buildOrderedChildList() {
    // 全部 ChildView 的數量
    final int childrenCount = mChildrenCount;

    ...

    final boolean customOrder = isChildrenDrawingOrderEnabled();
    for (int i = 0; i < childrenCount; i++) {
        // add next child (in child order) to end of list
        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View nextChild = mChildren[childIndex];
        final float currentZ = nextChild.getZ();

        // insert ahead of any Views with greater Z
        int insertIndex = i;
        while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
            insertIndex--;
        }
        // Z 軸越大則擺放至越前面
        mPreSortedChildren.add(insertIndex, nextChild);
    }
    return mPreSortedChildren;
}

ViewGroup#dispatchTransformedTouchEvent 方法:ViewGroup 會透過該方法把事件傳遞給 ChildView

內部其實就會針對 ChildView 執行 dispatchTouchEvent 方法

A. 若是傳入的 child 是 null 就呼叫 ViewGroup 父類的 dispatchTouchEvent

B. 不是 null 則呼叫指定 View 的 dispatchTouchEvent (當前情況就是有傳入 View 物件,所以會轉跳到指定 View#dispatchTouchEvent 方法)


// ViewGroup.java

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

    final boolean handled;

    ... 省略部分

    // 重點在個判斷
    if (child == null) {
        // 如果 Child view 為 null 則回傳給 ViewGroup 的 Parent
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        ... 目前走這裡

        handled = child.dispatchTouchEvent(transformedEvent);
    }

    return handled;
}

● ViewGroup#addTouchTarget:賦予 mFirstTouchTarget 成員


// ViewGroup.java

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        // 取覆用的 TouchTarget 物件

        final TouchTarget target = TouchTarget
                    .obtain(child, pointerIdBits);
        
        // 串接上一個事件的 View
        target.next = mFirstTouchTarget;
        
        // 第一個 處理點擊事件 的 View
        mFirstTouchTarget = target;
        return target;
    }

一開始 target.next 為 null,通過 TouchTarget#obtain 獲得對象 (相當於 new 的功能),並將 next 只給前一個 View

最後再將剛剛獲得的對象賦予 mFirstTouchTarget 並返回

D. 在 ChildView 接收並攔截事件後 (透過 dispatchTouchEvent 方法),會賦予該 ViewGroup#mFirstTouchTarget 值、並跳出迴圈

● ChildView 處理完事件後,返回到 ViewGroup 繼續處理


// ViewGroup.java

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

    ... 省略部分

    if (onFilterTouchEventForSecurity(ev)) {

        ... 省略上面分析 (ChildView 分發,跳出 for 迴圈)

        // 目前情況, ChildView 已經處理事件,所以 mFirstTouchTarget 不為 null
        if (mFirstTouchTarget == null) {
            // 下一小節在分析
        } else {
            ... 目前判斷走這

            TouchTarget predecessor = null;

            // 尋找目前的點擊事件相對的 ChildView

            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;
                    
                    // 分發事件
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }

        }

    }

    ...

    return handled;
}

到這裡就分析完 ViewGroup 不攔截事件,ChildView 透過 View#dispatchTouchEvent 收到事件

ViewGroup 處理 ACTION_DOWN:ChildView 不攔截

ChildView 不攔截代表 ViewGroup 要自己決定要不要處理該事件

● ViewGroup ChildView 攔截事件:處理方式相同 (上一小節),這裡主要看看不同的部分,該事件由 ViewGroup 自己處理 (dispatchTransformedTouchEvent 方法)


// ViewGroup.java

    // 從該 ViewGroup 開始,往下串接點擊的目標
    private TouchTarget mFirstTouchTarget;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ... 省略部分

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {

            // 取得目前行為
            final int action = ev.getAction();
            // 與 Mask 進行 and 操作,取得真正的 Action
            final int actionMasked = action & MotionEvent.ACTION_MASK;


            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // 在 Action Down 時才執行 reset

                cancelAndClearTouchTargets(ev);

                resetTouchState();
            }

        }

        ... 省略部分

        return handled;
    }

A. 清除先前的事件:注意 resetTouchState 方法,它會在 ViewGroup#dispatchTouchEvent 事件是 ACTION_DOWN 時執行

同上,請參考上一小節

B. 是否會呼叫 ViewGroup#onInterceptTouchEvent 方法:會有兩個條件,再加上一個 FLAG 判斷

A. 目前是 ACTION_DOWN 事件

B. 已經有 ChildView 處理這個點擊事件 (如果有子 View 處理事件就會給 mFirstTouchTarget 賦值)

C. 目前 ViewGroup 是否被禁止攔截 (一般 ViewGroup 接收到 ACTION_DOWN 時,如果沒有禁止攔截的話,就會執行 onInterceptTouchEvent 方法)

同上,請參考上一小節

C. 若 ViewGroup 自身沒有攔截,就會遞迴 ChildView:並一一執行 ViewGroup#dispatchTransformedTouchEvent 分發給每個 ChildView 處理


// ViewGroup
    
    // 從該 ViewGroup 開始,往下串接點擊的目標
    private TouchTarget mFirstTouchTarget;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ... 省略部分

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            ...
                
            if (!canceled && !intercepted) {
                // 這邊會循環該 ViewGroup 中的所有的 ChildView,看有沒有 View 處理事件
                // 
                // 目前狀況是都沒有 ChildView 要處理這個事件
            }
        }
        ...
    }

D. ChildView 接收但 全部都不攔截事件ViewGroup#mFirstTouchTarget 為 null


// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        ... 省略部分

        if (onFilterTouchEventForSecurity(ev)) {

            ... 省略上面分析 (ChildView 分發)

            // 目前情況,沒有 ChildView 處理事件
            if (mFirstTouchTarget == null) {
                // 注意傳入的第三個參數 null
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                ...
            }

        }

        ...

        return handled;
    }


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

        ... 省略部分

        if (child == null) {
            // 回到 ViewGroup 的 Parent dispatchTouchEvent
            handled = super.dispatchTouchEvent(transformedEvent);

        } else {
            ...
        }
        ...
        return handled;
    }

這裡可以看出 ViewGroup 預設不處理事件,直接將事件返回到上一層 View

View 收到事件:處理 ACTION_UP

假設 ViewGroup 不攔截事件,最終會呼叫目標 View#dispatchTouchEvent 方法,讓 View 處理事件

View 首先分發給 onTouch 若是沒有處理則分發到 onTouchEvent,順序如下

A. 判斷 onTouch 接口:最先執行 onTouch 接口,若是已經處理,就 onTouchEvent 就不會接收到事件

B. 判斷 onTouchEvent 接口:若 onTouch 沒有處理這個事件,就會輪到該 View 的 onTouchEvent 處理事件


// View.java

    public boolean dispatchTouchEvent(MotionEvent event) {
        ... 省略部分

        boolean result = false;

        ... 省略部分

        if (onFilterTouchEventForSecurity(event)) {
            ...

            // 最一開始事件分發到 onTouch
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    // 判斷該 View 是 enable
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            
            // @ 分析 onTouchEvent
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        ... 省略

        return result;
    }

● 在 View#onTouchEvent 方法中,可以看到 onClick、onLongClick 接口的呼叫


// View.java

    // PerformClick 代表該 View 的點擊事件
    private PerformClick mPerformClick;

    public boolean onTouchEvent(MotionEvent event) {

        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();


        // 只要設定 CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE 其中一個,
        // 那該 View 就是可點擊的
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;


        // 點擊事件的代理 TouchDelegate
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }


        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {

                case MotionEvent.ACTION_UP:
                    ... 省略部分

                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;

                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        ...

                        // 長按任務 mHasPerformedLongPress
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // 移除長按 Callback
                            removeLongPressCallback();
                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {

                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }

                                // @ 分析 performClickInternal
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }

                        ...
                    }
                    mIgnoreNextUpEvent = false;
                    break;
            }
        }
    }

● 這邊可以看到 View 的點擊事件是 透過 Handler 傳送 Click 點擊任務 (Runnable),這樣就不會造成點擊事件堵塞


// View.java

    public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        
        if (attachInfo != null) {
            // 將 Click 任務放入 Handler 
            return attachInfo.mHandler.post(action);
        }
        getRunQueue().post(action);
        return true;
    }

    private boolean performClickInternal() {
        notifyAutofillManagerOnClick();
        
        // @ 分析 performClick
        return performClick();
    }

    public boolean performClick() {
        notifyAutofillManagerOnClick();
        
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            // 執行使用者的 OnClick 接口
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        notifyEnterOrExitForAutoFillIfNeeded(true);
        return result;
    }

ViewGroup:ACTION_DOWN 總結

A. 判斷事件是否被 ViewGroup 攔截:

● ViewGroup 攔截事件 -> 直接走 3

● ViewGroup 不攔截事件 -> 先走 2 再走 3

B. ViewGroup 不攔截,並有其中一個 ChildView 攔截事件並處理

● ViewGroup#newTouchTarget 被賦予值

● 分發到自身的 View (super.dispatchTouchEvent 呼叫自己的父類)


// ViewGroup.java

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    ... 省略部分

    mLastTouchDownX = ev.getX();
    mLastTouchDownY = ev.getY();

    // @ addTouchTarget 方法內會賦予 mFirstTouchTarget 值
    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
    // 跳出迴圈
    break;
}

C. 事件分發、處理:有兩種情況

● 所有 ChildView 不攔截這個事件,所以 mFirstTouchTarget 為 null,相當於是最後一個 View


// 分發程式

if (mFirstTouchTarget == null) {
    // 第三個參數為 null
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
        TouchTarget.ALL_POINTER_IDS);
}

因為沒有任何 ChildView 處理,所以 ViewGroup#View 自己處理 Down 事件

● 有 ChildView 攔截,所以 mFirstTouchTarget 不為 null,並且 while 只循環一次,因為 ChildView 已處理 (分發時處理)


// 分發程式

if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
    handled = true;
}

ACTION_MOVE:View 收到 MOVE

Move 事件仍會先走 DecorView 這個 ViewGroup,在執行 ACTION_MOVE 事件時先需要知道幾件事情

A. ViewGroup 不會清理點擊的 Flag

B. TouchTarget 類型的 mFirstTouchTarget 元素 不為空 (因為已經有 ChildView 元素處理)

● ACTION_MOVE 事件的判斷流程圖如下,它主要做的事情是

A. ViewGroup 是否攔截事件 (以下預設不攔截事件)

B. 不攔截、Move 不分發事件

C. 分發 or 處理

● 同樣先來觀察 ViewGroup#dispatchTouchEvent 方法,並從這裡開始分析

A. 不會進入 reset 環節


// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        // 不清理 Flag,不會進入 reset 環節
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            ... 省略
        }
    }

B. 判斷當前 ViewGroup 是否有禁止攔截 FLAG_DISALLOW_INTERCEPT,如果沒有的話就調用自身的 onInterceptTouchEvent 方法 檢查是否攔截


// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        
        ... 省略部分

        // Check for interception.
        // 由於 mFirstTouchTarget 不為空,所以會進入
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {

            // 判斷 ViewGroup 是否禁止攔截
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

            // 預設 ViewGroup 不攔截
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
    }

C. 不是 ACTION_DOWN 事件,所以不會進入最初的 ACTION_DOWN 事件分發環節


// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

            ... 省略部分

        // 注意這兩個區域變數,下面會用到
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        // 進入
        if (!canceled && !intercepted) {

            // 2. 不是 Down 事件,所以不進入
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                // 省略... 由於不進入,所以不會對 ChildView 分發

            }
        }

        ... 省略部分
    }

D. ACTION_MOVE 的重點在事件分發 (請看註解)


// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        ... 省略部分

        // mFirstTouchTarget 不為空 (因為已經是 View 處理事件)
        if (mFirstTouchTarget == null) {
            ...

        } else {
            // ++重點在這++TouchTarget predecessor = null;

            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;

                // alreadyDispatchedToNewTouchTarget 是 false
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;

                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;

                    // @ 分析 dispatchTransformedTouchEvent 分發給 ChildView
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    ...
                }
                predecessor = target;
                target = next;
            }
        }
    }

● 接下來透過 dispatchTransformedTouchEvent 方法:分發 ACTION_MOVE 事件給 ChildView 的 dispatchTouchEvent


// ViewGroup.java 

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) 

        ...省略部分

        if (child == null) {
            ...
        } else {
            // 加設有 ChildView 則會進入這裡
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }
    }

ACTION_MOVE:ViewGroup 攔截 MOVE

● 先了解目前情況:已經有 ChildView 處理 ACTION_DOWN 事件,而我們自訂一個 ViewGroup 並複寫 ViewGroup#onInterceptTouchEvent 方法,ACTION_VIEW 時返回 true 攔截


// 自定義 ViewGroup.java

public class MyViewGroup extends View {

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if(ev.getAction() == MotionEvent.ACTION_MOVE) {
            // ViewGroup 自己消耗事件
            return true;
        }
        return super.onInterceptTouchEvent(ev);
    }

}

ACTION_MOVE 事件是多次發生

A. 第一個 ACTION_MOVE (從 ParentView 進來)的目的是:1. 取消 ChildView 事件(事件改為 ACTION_CANCEL)、2.ViewGroup#mFirstTouchTarget 置為空,這時 ParentView 是不處理事件的

結果:這時 ChildView 會收到 ACTION_CANCEL 事件 (事件被取消,之後都會由 ViewGroup 處理事件)


/**
 * ViewGroup.java
 * 
 * 由於複寫 onInterceptTouchEvent 方法會讓 Move 事件為 ture,所以 cancelChild 也為 ture,
 * 傳入 dispatchTransformedTouchEvent 方法的 cancel 參數為 ture,
 * 
 * 這時事件就會被改為 ACTION_CANCEL,下面的 ChildView 就會接收到 ACTION_CANCEL 事件
 */ 
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        //... 省略

        if (mFirstTouchTarget == null) {
            ...

        } else {

            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    // ACTION_MOVE 第一次進入
                    // ^1^ 重點 intercepted 是 true,所以 cancelChild = true
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;

                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }

                    // ^2^ 重點,mFirstTouchTarget = next,next 為 null
                    // 所以 mFirstTouchTarget = null
                    if (cancelChild) {
                        if (predecessor == null) {
                            // next 為空,所以 mFirstTouchTarget 置為空 (next 為空)
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        ...
    }


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

        final boolean handled;

        final int oldAction = event.getAction();
        // cancel 為 true 會進入
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {

            // 改變事件為 ACTION_CANCEL
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                ...
            } else {
                // 往下分發 ChildView 的事件就為 cancel
                // 事件被上層攔截時觸發
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        ... 省略部分
            
        return handled;
    }

B. 第二個 ACTION_MOVE(從 ParentView 進來),由於 mFirstTouchTarget 為空,所以 ParentView 不會分發

結果:ParentView 自己處理事件


/**
 * ViewGroup.java
 */ 
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

            // 在第一個 ACTION_MOVE 時 mFirstTouchTarget 被置為空,所以不會進入
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {

                // ...省略,因為 mFirstTouchTarget = null
            } else {
                intercepted = true;
            }

            // 由於 intercepted = true 也就是攔截,就不會進入
            if (!canceled && !intercepted) {
                //... 分發部份
            }


            // 進入
            if (mFirstTouchTarget == null) {
                // ChildView 為空 (第三個參數)
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {

                ...
            }

            ...
    }


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

        final boolean handled;

        ... 省略部分
        
        
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }

Android View 事件衝突

為何會發生衝突 ? 因為事件只會有一個,若是響應的元件不是自己要的 View 原件,這時就可以稱之為事件衝突

Ex: ViewPager 包裹 RecyclerView

● 分為兩種方法處理

A. 內部攔截法:需要 ChildView#dispatchTouchEvent 處理 & 需要配合改動父容器 (ParentView) 的 onInterceptTouchEvent 攔截

B. 外部攔截法 (較常使用):只需要 ParentView 處理

內部攔截:ChildView 處理

在 ChildView 中使用 requestDisallowInterceptTouchEvent 方法,控制 FLAG_DISALLOW_INTERCEPT 元素,讓 ParentView 不會往下分發事件 (先介紹這裡的坑~)

● 內部攔截作法 - 2 個重點步驟 (都必須)

以下假設,ChildView 只處理垂直滑動、水平滑動給 ParentView 處理

A. ChildView 在適當時機透過 requestDisallowInterceptTouchEvent 方法,要求 ParentView 不要攔截事件


// 自訂 View

public class MyView extends View {

    int dispatchX, dispatchY;

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch(event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 要求 ParentView 不攔截事件
                getParent().requestDisallowInterceptTouchEvent(true);
                break;

            case MotionEvent.ACTION_UP:
                break;

            case MotionEvent.ACTION_MOVE:
                int deltaX = x - dispatchX;
                int deltaY = y - dispatchY;
                
                // 水平時,事件交由 ViewGroup 處理
                if(Math.abs(deltaX) > Math.abs(deltaY)) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
        }

        dispatchX = x;
        dispatchY = y;

        return super.dispatchTouchEvent(event);
    }

}

B. 重寫 ParentView 的 onInterceptTouchEvent 方法,設定在 ACTION_DOWN 時不攔截事件,這樣事件才能傳入 ChildView


// 自訂 ViewGroup

public class MyViewGroup extends ViewGroup {

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if(ev.getAction() == MotionEvent.ACTION_DOWN) {
            // ACTION_DOWN 時強制 ViewGroup 不攔截事件
            return false;
        }
        return super.onInterceptTouchEvent(ev);
    }

}

requestDisallowInterceptTouchEvent 細節說明: 為啥 requestDisallowInterceptTouchEvent 無法控制 ACTION_DOWN 事件

● 發生原因

ViewGroup#ACTION_DOWN 事件,會清除 ViewGroup 的禁止攔截事件,所以就算 ChildView 有要求 ViewGroup 不攔截事件,但事件都無法傳入 ChildView 就沒有用了


// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {

                cancelAndClearTouchTargets(ev);
                // @ 查看 resetTouchState
                resetTouchState();
            }

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {    // mFirstTouchTarget != null 滿足

                // 控制 FLAG_DISALLOW_INTERCEPT
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                // 如果為 ture 則 ParentView 就不會往下分發
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                intercepted = true;
            }

            // 省略其他...
        }
    }


    // 這個方法可以控制 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);
        }
    }

A. 從上面可看出 ViewGroup 時 ACTION_DOWN 使用 resetTouchState() 方法,這裡會清理 FLAG_DISALLOW_INTERCEPT 這個 Flag,導致事件 ACTION_DOWN 一定會被分發


// ViewGroup.java
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        
        ... 省略部分

        final boolean intercepted;

        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {

            // 清理後 FLAG_DISALLOW_INTERCEPT 為空,結果就是為 false
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            
            if (!disallowIntercept) {
                // 判斷 ViewGroup#onInterceptTouchEvent 是否擷取
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                // ++不擷取++ 注意 intercepted 變數
                intercepted = false;
            }
        }
        ...
    }

B. 解坑:ViewGroup 中複寫 onInterceptTouchEvent,讓 ViewGroup 在 ACTION_DOWN 事件時返回 false(不擷取),這樣 ACTION_DOWN 就會分發,這樣才能往下分發到 ChildView,而其他的動作則會攔截

原因是因為 ViewGroup#resetTouchState 會清除 FLAG_DISALLOW_INTERCEPT,導致上面的設定失效


/**
 * 自定義 ViewGroup 內重新 Override
 */ 
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // ACTION_DOWN 事件時,就不擷取,分發到 ChildView
        if(ev.getAction() == ACTION_DOWN) {
            return false;
        }
        return true;
    }

外部攔截:ParentView 處理

● 由 ParentView 處理事件攔截比較簡單,只需要覆寫 ParentView#onInterceptTouchEvent 決定哪個時間(條件)攔截事件


// 自定義的 ViewGroup.java

public class MyExternalViewGroup extends ViewGroup {

    int interceptX, interceptY;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercept = false;
        boolean defaultIntercept = super.onInterceptTouchEvent(event);

        int x = (int) event.getX();
        int y = (int) event.getY();

        switch(event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - interceptX;
                int deltaY = y - interceptY;
                
                // 水平滑動,就攔截 !
                if(Math.abs(deltaX) > Math.abs(deltaY)) {
                    intercept = true;
                } else {
                    intercept = false;
                }
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_DOWN:
                // 不攔截事件
                intercept = false;
                break;
                
            default:
                intercept = defaultIntercept;
                break;
        }

        interceptX = x;
        interceptY = y;

        return intercept;
    }
}

Show 6 Comments

6 Comments

  1. 這篇文章對於 Android 點擊事件的傳遞機制解釋得很清楚,是否有實際的程式碼範例可以參考?

    • 感謝你的肯定~ 這部分我有空會寫的 github 範例(但是現在滿忙的… 😅😅😅)

  2. 這篇文章非常詳細地解釋了 Android 點擊事件的分發和處理機制,對於開發者來說非常有幫助。特別是對於 View 和 ViewGroup 的事件攔截和衝突處理部分,提供了清晰的實例講解。如果你是 Android 開發的初學者,這些內容將幫助你更好地理解事件機制的運作方式。是否有實際的範例可以參考,以進一步加深理解?

    • 感謝你的肯定~ 這部分我有空會寫的 github 範例(但是現在滿忙的… 😅😅😅)

  3. 這篇文章深入探討了 Android 中點擊事件的傳遞與處理機制,對於理解 View 和 ViewGroup 的行為非常有幫助。通過實際例子,詳細解釋了從事件分發到多層級視圖的處理邏輯,特別是內部與外部攔截策略。對於 Android 開發者來說,這是一個提升事件處理能力的絕佳資源。你是否能分享一個實際開發中遇到的點擊事件衝突案例及其解決方案?

    • 感謝你的肯定~ 這部分我有空會寫的 github 範例(但是現在滿忙的… 😅😅😅)

發表迴響