Android事件分發與責任鏈模式

来源:https://www.cnblogs.com/renhui/archive/2020/01/02/12127680.html
-Advertisement-
Play Games

一、責任鏈模式 責任鏈模式是一種行為模式,為請求創建一個接收者的對象鏈.這樣就避免,一個請求鏈接多個接收者的情況.進行外部解耦.類似於單向鏈表結構。 優點: 1. 降低耦合度。它將請求的發送者和接收者解耦。 2. 簡化了對象。使得對象不需要知道鏈的結構。 3. 增強給對象指派職責的靈活性。通過改變鏈 ...


一、責任鏈模式

責任鏈模式是一種行為模式,為請求創建一個接收者的對象鏈.這樣就避免,一個請求鏈接多個接收者的情況.進行外部解耦.類似於單向鏈表結構。

優點:

1. 降低耦合度。它將請求的發送者和接收者解耦。

2. 簡化了對象。使得對象不需要知道鏈的結構。

3. 增強給對象指派職責的靈活性。通過改變鏈內的成員或者調動它們的次 序,允許動態地新增或者刪除責任。

4. 增加新的請求處理類很方便。

缺點:

1. 不能保證請求一定被接收。

2. 系統性能將受到一定影響,而且在進行代碼調試時不太方便,可能會造成迴圈調用。

3. 可能不容易觀察運行時的特征,有礙於除錯。

責任鏈的模式在事件分發場景方面的原理:

一般我們理解的事件分發的模式如下(傳統模式):

使用責任鏈模式直接將message丟到鏈中,讓他們自己匹配.

二、Android 事件分發傳遞機制

1. View事件傳遞分發層級結構

 a). 事件收集之後最先傳遞給 Activity, 然後依次向下傳遞,大致如下:

Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View

這樣的事件分發機制邏輯非常清晰,可是,你是否註意到一個問題?如果最後分發到View,如果這個View也沒有處理事件怎麼辦,就這樣讓事件浪費掉?當然不會啦。

 b). 如果沒有任何View消費掉事件,那麼這個事件會按照反方向回傳,最終傳回給Activity,如果最後 Activity 也沒有處理,本次事件才會被拋棄:

Activity <- PhoneWindow <- DecorView <- ViewGroup <- ... <- View

可以看到,這是一個非常經典的責任鏈模式,如果我能處理就攔截下來自己乾,如果自己不能處理或者不確定就交給責任鏈中下一個對象。 這種設計是非常精巧的,上層View既可以直接攔截該事件,自己處理,也可以先詢問(分發給)子View,如果子View需要就交給子View處理,如果子View不需要還能繼續交給上層View處理。既保證了事件的有序性,又非常的靈活。

View點擊事件分發有三個關鍵流程方法:

1.dispatchTouchEvent:事件下發 --- View和ViewGroup都有的方法

2.onInterceptTouchEvent:攔截下發的事件,並交給自己OnTouchEvent處理處理 ---ViewGroup才有的方法

3.onTouchEvent:事件上報 --- View和ViewGroup都有的方法

以下是不同層級對事件的分發、攔截和消費的功能表:

可以看到 Activity 和 View 都是沒有事件攔截的:

a). Activity 作為原始的事件分發者,如果 Activity 攔截了事件會導致整個屏幕都無法響應事件,這肯定不是我們想要的效果。

b). View最為事件傳遞的最末端,要麼消費掉事件,要麼不處理進行回傳,根本沒必要進行事件攔截。

下圖是點擊View,事件傳遞但是都沒有被處理,生成的一個完整的事件分發流程圖:

如果事件被View處理了,那麼事件分發流程圖應該如下:

如果事件被ViewGroup攔截處理了, 那麼事件分發流程圖應該如下:

從上面的流程,我們可以概括Android的事件分發機製為:責任鏈模式,事件層層傳遞,直到被消費。

三、Q&A

上面我們講解了一下Android的事件分發機制,可能很多人會有疑惑,下麵我們針對部分疑惑進行分析和說明:

1. 為什麼 View 會有 dispatchTouchEvent ?

答:我們知道 View 可以註冊很多事件監聽器,例如:單擊事件(onClick)、長按事件(onLongClick)、觸摸事件(onTouch),並且View自身也有 onTouchEvent 方法,那麼問題來了,這麼多與事件相關的方法應該由誰管理?毋庸置疑就是 dispatchTouchEvent,所以 View 也會有事件分發。

View的dispatchTouchEvent源碼:

/**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }
View Code

2. View事件分發時各個方法調用順序是怎樣的?

a). 單擊事件(onClickListener) 需要兩個兩個事件(ACTION_DOWN 和 ACTION_UP )才能觸發,如果先分配給onClick判斷,等它判斷完再交由其他相應時間顯然是不合理的,會造成 View 無法響應其他事件,應該最後調用。(所以此調用順序最後)

b). 長按事件(onLongClickListener) 同理,也是需要長時間等待才能出結果,肯定不能排到前面,但因為不需要ACTION_UP,應該排在 onClick 前面。(onLongClickListener > onClickListener)

c). 觸摸事件(onTouchListener) 如果用戶註冊了觸摸事件,說明用戶要自己處理觸摸事件了,這個應該排在最前面。(最前)

d). View自身處理(onTouchEvent) 提供了一種預設的處理方式,如果用戶已經處理好了,也就不需要了,所以應該排在 onClickListener 後面。(onTouchListener > onClickListener)

所以事件的調度順序應該是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener

3. ViewGroup 的事件分發流程又是如何的呢?

在預設的情況下 ViewGroup 事件分發流程是這樣的。

a). 判斷自身是否需要(詢問 onInterceptTouchEvent 是否攔截),如果需要,調用自己的 onTouchEvent。

b). 自身不需要或者不確定,則詢問 ChildView ,一般來說是調用手指觸摸位置的 ChildView。

c). 如果子 ChildView 不需要則調用自身的 onTouchEvent。

ViewGroup的dispatchTouchEvent源碼:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }

        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.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

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

            // If intercepted, start normal event dispatch. Also if there is already
            // a view that is handling the gesture, do normal event dispatch.
            if (intercepted || mFirstTouchTarget != null) {
                ev.setTargetAccessibilityFocus(false);
            }

            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {

                // If the event is targeting accessibility focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } 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) {
                        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;
                }
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }
View Code

4. ViewGroup將事件分發給ChildView的機制

ViewGroup分發事件時會遍歷 ChildView,如果手指觸摸的點在 ChildView 區域內就分發給這個View。當 ChildView 重疊時,一般會分配給顯示在最上面的 ChildView。

5. ViewGroup 和 ChildView 同時註冊了事件監聽器(onClick等),哪個會執行?

事件優先給 ChildView,會被 ChildView消費掉,ViewGroup 不會響應。

 

附:參考資料:

1. Android事件傳遞機制分析

2. Android 事件分發機制詳解

3. 安卓自定義View進階-事件分發機制原理

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 修改表中一行或多行數據: SELECT*FROM student;+ + + +| id | name | ban |+ + + +| 1 | yy | 1913 || 7 | ss | 1923 || 8 | 許 | 1913 || 9 | 應 | 1913 || 10 | Aron | 1913 ...
  • Unicode字元集就是為瞭解決字元集這種不相容的問題而產生的,它所有的字元都用兩個位元組表示,即英文字元也是用兩個位元組表示 如果還為了這個糾結,就直接看看後面的解說,做決定吧。 一般如果用到中文或者其它特殊字元,我就會使用n開頭的類型,否則的話直接使用var開頭的。 sql server中的varc ...
  • 1.設置主伺服器配置 必須在主伺服器上啟用二進位日誌,因為二進位日誌是將更改從主伺服器複製到從伺服器的基礎,如果未啟用log-bin,則無法進行複製 複製組內的每個伺服器必須配置有唯一的id,此id用於標識組中的各個伺服器,1到2³²-1之間的正整數。 配置: [mysqld] log-bin=my ...
  • 有的欄位重覆,有的欄位不重覆,想要根據某個欄位去重,採用如下方法可行 ...
  • 1.查詢當前年、月、周相關時間 1.1.查詢當前年份 SELECT TO_CHAR(SYSDATE,'YYYY') AS YEAR FROM DUAL--查詢當前年份 SELECT TO_CHAR(SYSDATE,'YYY') AS YEAR FROM DUAL--查詢當前年份後兩位 SELECT ...
  • 1、查看磁碟空間情況 執行 df -h 選擇剩餘空間最大的目錄 (以/目錄為例) 2、創建備份目錄: cd / mkdir backup cd backup 3、創建備份Shell腳本: vim monarch-bak.sh #monarch是資料庫名 mysqldump -uroot -p9802 ...
  • 獲取資料庫中,所有用戶表中每一列名和其數據類型。 SELECT OBJECT_NAME(c.OBJECT_ID) AS [Table_Name], c.[name] AS [Column_Name] ,t.[name] AS [Data_Type_Name] FROM sys.columns AS ...
  • 禁用或啟用資料庫所有觸發器。 禁用: use Test Exec sp_msforeachtable "ALTER TABLE ? DISABLE TRIGGER all" GO 啟用: use Test Exec sp_msforeachtable "ALTER TABLE ? ENABLE TR ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...