Android 事件分發介紹

来源:https://www.cnblogs.com/zhiqinlin/Undeclared/17957738
-Advertisement-
Play Games

Android 中 View 的佈局是一個樹形結構,各個 ViewGroup 和 View 是按樹形結構嵌套佈局的,從而會出現用戶觸摸的位置坐標可能會落在多個 View 的範圍內,這樣就不知道哪個 View 來響應這個事件,為瞭解決這一問題,就出現了事件分發機制。 ...


目錄

一、目的

        最開始接觸Android時,僅僅是知道Android系統存在的點擊事件、觸摸事件,但是並不清楚這些事件的由來。
        之後,在面試Oppo和美圖時,皆有問到Android的事件分發機制,但是都被問得很懵逼,歸根到底都是對於其實現邏輯的不理解。
        隨後,想去彌補該模塊的不足,瀏覽很多關於Android事件分發的博文,但仍存在一些疑惑,就想著去閱讀下源碼,整理下筆記,希望對同學們有幫助。

二、環境

  1. 版本:Android 11
  2. 平臺:展銳 SPRD8541E

三、相關概念

3.1 事件分發

        Android 中 View 的佈局是一個樹形結構,各個 ViewGroup 和 View 是按樹形結構嵌套佈局的,從而會出現用戶觸摸的位置坐標可能會落在多個 View 的範圍內,這樣就不知道哪個 View 來響應這個事件,為瞭解決這一問題,就出現了事件分發機制。

四、詳細設計

4.1應用佈局

4.1.1 應用佈局結構

        如下為一個Activity打開後,其對應視圖的層級結構。

4.1.2 LayoutInspector

        Layout Inspector是google提供給我們進行佈局分析的一個工具,也是目前google在棄用Hierarchy View後推薦使用的一款佈局分析工具。

4.2 關鍵View&方法

4.2.1 相關View

組件 描述
Activity Android事件分發的起始端,其為一個window視窗,內部持有Decorder視圖,該視圖為當前窗體的根節點,同時,它也是一個ViewGroup容器。
ViewGroup Android中ViewGroup是一個佈局容器,可以嵌套多個 ViewGroup 和 View,事件傳遞和攔截都由 ViewGroup 完成。
View 事件傳遞的最末端,要麼消費事件,要麼不消費把事件傳遞給父容器

4.2.2 相關方法

方法 描述
dispatchTouchEvent 分發事件
onInterceptTouchEvent 攔截事件
onTouchEvent 觸摸事件

4.2.3 View與方法關係

組件 dispatchTouchEvent onInterceptTouchEvent onTouchEvent
Activity
ViewGroup
View

4.3 事件分發概念圖

4.3.1 事件分發類圖

4.3.2 事件分發模型圖

        Android的ACTION_DOWN事件分發如圖,從1-9步驟,描述一個down事件的分發過程,如果大家能懂,就不用看下麵文字描述了(寫完這個篇幅,感覺文字好多,不好理解!)

  1. ACTION_DOWN事件觸發。 當我們手指觸摸屏幕,tp驅動會響應中斷,通過ims輸入系統,將down事件的相關信息發送到當前的視窗,即當前的Activity。
  2. Activity事件分發。 會引用dispatchTouchEvent()方法,對down事件分發。Activity本身會持有一個window對象,window對象的實現類PhoneWindow會持有一個DecorView對象,DecorView是一個ViewGroup對象,即我們可以理解為,Activity最終會將事件分發給下一個節點——ViewGroup。
  3. ViewGroup事件攔截。 ViewGroup接收到事件後,會先引用onInterceptTouchEvent(),查看當前的視圖容器是否做事件攔截。
  4. ViewGroup消費事件。 如當前的ViewGroup對事件進行攔截,即會調用onTouchEvent(),對事件消費。
  5. ViewGroup事件不攔截。 則ViewGroup會繼續遍歷自身的子節點,並且當事件的坐標位於子節點上,則繼續下發到下一個節點。ViewGroup的子節點有可能是View,也可能是ViewGroup(當然,ViewGroup最後也是繼承於View的,突然感覺有點廢話)。
  6. ViewGroup事件分發。 目標視圖如果是ViewGroup,會引用其super類的dispatchTouchEvent()方法,即事件下發,不管目標視圖是View或者ViewGroup最終引用的是View類的分發方法。
  7. View事件消費。 在View的dispatchTouchEvent()方法中會根據當前View是否可以點擊、onTouch()是否消費、onTouchEvent()是否消費等條件,來判斷當前是否為目標View。
  8. View事件未消費。 View事件未消費,則其父節點,即ViewGroup會調用onTouchEvent()方法,並根據返回值來決定是否消費事件。
  9. ViewGroup事件未消費。 ViewGroup事件未消費,擇其父節點,即Actviity會調用onTouchEvent()方法

PS:
(1) ACTION_MOVEACTION_UP事件,流程與ACTION_DOWN的分發過程基本一致,MOVE和UP事件也是通過Activity開始,藉助DOWN事件產生的目標View,逐級分發。
(2) ACTION_CANCEL事件,是在down與up、move事件切換過程中,事件被攔截,兩次的touchTarget目標view不一致,而產生的事件。用於對之前的目標View做恢復處理,避免down與up/move事件不對稱。

4.4 Activity組件

4.4.1 Activity->dispatchTouchEvent()

        底層上報的事件信息,最終會引用到該方法。Activity會持有一個根視圖DecordView,事件最終會往該ViewGroup分發,如所有的View都未消費該事件,則最終由Activity的onTouchEvent()
來兜底處理。

@frameworks\base\core\java\android\app\Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    if (getWindow().superDispatchTouchEvent(ev)) {//Step 1. 查看Window對應的View是否分發該事件
        return true;
    }
    return onTouchEvent(ev);//Step 2. 如果沒有組件消費事件,則由Activity兜底處理
}

4.4.2 Activity->getWindow()

        我們每次啟動一個Activity的組件,會先打開一個window視窗,而PhoneWindow是Window唯一的實現類。

@frameworks\base\core\java\android\app\Activity.java
public Window getWindow() {
    return mWindow;
}

final void attach(Context context, ActivityThread aThread...) {
    ...
    mWindow = new PhoneWindow(this, window, activityConfigCallback);//PhoneWindow是Window視窗唯一的實現類
    ...
}

        PhoneWindow對象內部持有DecorView對象,而該View正是該視窗對應的視圖容器,也是根節點。(此部分不具體分析)

@frameworks\base\core\java\com\android\internal\policy\PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.     Callback {
    ...
    private DecorView mDecor;//
    ...
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);//往View的根節點分發事件
    }
}

4.4.3 Activity->onTouchEvent()

        Activity的onTouchEvent方法,是在沒有任何組件消費事件的情況下,觸發的方法。

@frameworks\base\core\java\android\app\Activity.java
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }
    return false;
}

4.5 ViewGroup組件

        ViewGroup組件在整個事件分發的模型中,既有分發事件的責任,又要具備處理事件的能力,真的典型的當爹又當媽。
        當Activity調用superDispatchTouchEvent,即最終會使用到DecorView的superDispatchTouchEvent方法,而DecorView是繼承於ViewGroup,即最終會引用ViewGroup的dispatchTouchEvent方法。

4.5.1 ViewGroup->dispatchTouchEvent()

此方法為事件分發最核心的代碼。其主要處理如下四件事情:
Setp 1. 重置事件。 一次完整觸摸的事件:DOWN -> MOVE -> UP,即我們可以理解為DOWN是所有觸摸事件的起始事件。當輸入事件是ACTION_DOWN時,重置觸摸事件狀態信息,避免產生干擾。
Step 2. 攔截事件。 攔截事件是ViewGroup特有的方法,用於攔截事件,並將該事件分發給自己消費,防止事件繼續下發。
Step 3.查找目標View。 查找目標View主要針對於Down事件。當ViewGroup未攔截事件,且輸入事件是ACTION_DOWN時,會遍歷該ViewGroup的所有子節點,並根據觸摸位置的坐標,來決定當前子節點是否是下一級目標View。當找到目標View節點後,會分發Down事件,並記錄該節點信息。
Step 4.下發事件。 如果目標View未找到的話,則會將事件交由自己的onTouchEvent()處理;如果目標View已經找到,則Down事件就此結束(此處暫不考慮多指場景);Move和Up事件將繼續下發(預設情況下Move、Up和Down事件是成對出現的,如果目標View已經存在,則Down事件已經下發,即意味著Move和Up事件也需要下發給對應的目標View)。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    if (actionMasked == MotionEvent.ACTION_DOWN) {//Step 1.重置事件信息,避免影響下一次事件
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }

    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);//Step 2.攔截事件
            ev.setAction(action); // restore action in case it was changed
        }
    } 
    ...
    if (!canceled && !intercepted) {//Step 3.查找目標View
        if (actionMasked == MotionEvent.ACTION_DOWN
                || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            ...
            if (newTouchTarget == null && childrenCount != 0) {
                ...
                for (int i = childrenCount - 1; i >= 0; i--) {//遍歷所有的子節點
                    ...
                    if (!child.canReceivePointerEvents()
                            || !isTransformedTouchPointInView(x, y, child, null)) {// 子節點不可以接收事件,或者觸摸位置不在子節點的範圍上
                        continue;
                    }
                    ...
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//找到目標View
                        ...
                        break;
                    }
                }
                ...
            }
            ...
        }
    }
    //Step 4.根據找到的目標View情況,繼續下發事件
    if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);//沒有找到目標View或者事件被攔截,事件下發給自己
    } else {
        ...
        while (target != null) {//多組數據,一般是指多指場景
            final TouchTarget next = target.next;
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {//此場景一般是down事件
                handled = true
            } else {
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {//此場景一般是move、up事件
                    handled = true;
                }
                ...
            }
            predecessor = target;
            target = next;
        }
        ...
    }
    ...
    return handled;
}

4.5.2 ViewGroup->dispatchTransformedTouchEvent()

事件分發關鍵方法,主要用於向目標View分發事件,具體邏輯如下:
Step 1.Cancel事件分發。 之前我們提過Down和Up事件是成對存在的,如果Down事件已經下發的情況下,Up事件卻因為事件攔截等原因,未能下發給目標View,目標View未收到Up事件,此時就可能產生一些按壓狀態的異常問題,故,在當前場景下,將會分發一個ACTION_CANCEL事件給目標View。
Step 2.事件處理。 如果事件未找到目標View,則child會為null,此時的事件將由自身處理。
Step 3.事件分發。 如果事件還存在目標View,則此時的事件會再分發。

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        ...
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {//Step 1.下發取消事件
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
        ...
        if (child == null) {//Step 2.如果事件未找到目標View,則觸摸事件會發給自己
            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);//Step 3.找到目標View,事件下發給子節點
        }
        ...
        return handled;
    }

4.6 View組件

        View組件在事件處理模型中,主要是處理事件。我們知道ViewGroup,也是繼承於View,所以ViewGroup也是同樣具備View的處理事件能力。

4.6.1 View->dispatchTouchEvent()

Step 1.觸發onTouch()方法。 如果當前的View是可點擊的,且配置了onTouch事件監聽,則觸發該View的onTouch()方法。
Step 2.觸發onTouchEvent()方法。 如果該事件在上一步的onTouch()函數中未被消費,則觸發onTouchEvent()方法。

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    ...
    if (onFilterTouchEventForSecurity(event)) {
        ...
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {//Step 1.觸發onTouch事件
            result = true;
        }

        if (!result && onTouchEvent(event)) {//Step 2.如onTouch未消費,觸發onTouchEvent事件
            result = true;
        }
    }
    ...
    return result;
}

4.6.2 OnTouchListener->onTouch()

        View可以設置事件監聽,用於監聽onTouch事件的回調,當然,像我們常見的onClick()、onLongClick()等事件也可監聽,其相關源碼如下:

@frameworks\base\core\java\android\view\View.java
public void setOnTouchListener(OnTouchListener l) {//設置onTouch監聽
    getListenerInfo().mOnTouchListener = l;
}

ListenerInfo getListenerInfo() {
    if (mListenerInfo != null) {
        return mListenerInfo;
    }
    mListenerInfo = new ListenerInfo();
    return mListenerInfo;
}

public interface OnTouchListener {//Touch介面,用於回調onTouch事件
    boolean onTouch(View v, MotionEvent event);
}

4.6.3 View->onTouchEvent()

        事件如未被onTouch消費掉,則會引用到onTouchEvent()方法,該方法會涉及ACTION_UP、ACTION_DOWN、ACTION_CANCEL、ACTION_MOVE事件的處理,View的onClick()、onLongClick()也是由該方法觸發。此外,如果當前的View是可點擊的話,則直接消費該事件。

public boolean onTouchEvent(MotionEvent event) {
    ...
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;//當前View是否可點擊
    ...
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP://抬起
                ...
                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    if (!focusTaken) {
                        removeLongPressCallback();//若有長按事件未處理,則移除長按事件
                        if (mPerformClick == null) {
                            mPerformClick = new PerformClick();
                        }
                        if (!post(mPerformClick)) {//通過Hanlder將點擊事件發送到主線程執行
                            performClickInternal();//如果不成功,則直接引用點擊事件
                        }
                    }
                }
                if (mUnsetPressedState == null) {
                    mUnsetPressedState = new UnsetPressedState();//更新按鈕的按壓事件
                }
                ...
                break;
            case MotionEvent.ACTION_DOWN://按下
                ...
                if (isInScrollingContainer) {//在可滾動的容器內,為了容錯,延遲點擊
                    ...
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    setPressed(true, x, y);//設置按下的狀態
                    checkForLongClick(
                            ViewConfiguration.getLongPressTimeout(),
                            x,
                            y,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);//開啟一個長按延時事件
                }
                break;

            case MotionEvent.ACTION_CANCEL://取消
                ...
                break;
            case MotionEvent.ACTION_MOVE://移動
                ...
                break;
        }
        return true;//如果是可點擊的View,即消費事件
    }
    ...
    return false;
}

4.7 例子-點擊事件時序圖

        如下是Android的點擊事件時序圖,如果能夠理解單擊事件的由來,對於整個事件分發的知識要點已大體掌握。

五、小結&問題點

  1. 事件分發流程?包括ACTION_DWON、ACTION_UP、ACTION_MOVE事件的處理過程;
  2. ACTION_CANCEL事件的使用場景?父控制項對move事件攔截場景?
  3. 單擊、長按、觸摸事件的產生過程?
  4. 點擊一個View未抬起,同時move該事件直至離開當前View的範圍,處理過程如何?
  5. 如果所有View都未消費事件,流程如何?
  6. ViewPage+ListView,左右滑動和上下滑動衝突的解決問題?即事件攔截過程?
  7. 普通的View是根據什麼來決定是否消費事件,例如Button?
    =>答:如無重寫onTouchEvent事件,根據當前的View是否可點擊,來決定是否消費事件。

        我最開始沒有看源碼,直接去看博客上的內容,彎彎繞繞,似懂非懂。在面試的過程中,面試官舉個場景分析流程,我都懵逼,分析不出來,現場很尷尬。之後看源碼,整體流程代碼量很少,感嘆於Android事件分發流程的設計,很少的代碼量,卻承載了很重要的功能,而沒有見過該模塊發生過異常。
        多讀書,多看報,少吃零食,多睡覺!

六、代碼倉庫地址

Demo地址:  https://gitee.com/linzhiqin/custom-demo

七、參考資料

https://zhuanlan.zhihu.com/p/623664769?utm_id=0
事件分發視頻(總結很好,但是得先理解基本概念,才方便學習)
https://www.bilibili.com/video/BV1sy4y1W7az?p=1&vd_source=f222e3bf3083cad8d9f660629bc47c16


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

-Advertisement-
Play Games
更多相關文章
  • 使用STM32CubeMX軟體配置STM32F407開發板上串口USART1進行DMA傳輸數據,然後實現與實驗STM32CubeMX教程9 USART/UART 非同步通信相同的目標 ...
  • 文件系統結構 unix的文件系統相關知識 unix將可用的磁碟空間劃分為兩種主要類型的區域:inode區域和數據區域。 unix為每個文件分配一個inode,其中保存文件的關鍵元數據,如文件的stat屬性和指向文件數據塊的指針。 數據區域中的空間會被分成大小相同的數據塊(就像記憶體管理中的分頁)。數據 ...
  • 1月9日,計世資訊(CCW Research)發佈《2022-2023年中國信創資料庫行業市場研究報告》(以下簡稱“報告”),從產品技術能力和市場及戰略能力兩個維度對我國主要資料庫產品服務商進行競爭力分析。其中,中國電信天翼雲憑藉其產品豐富的管理功能、靈活的部署架構,位列雲資料庫產品領域領導者象限。 ...
  • 作者:俊達 引言 MySQL是MySQL安裝包預設的客戶端,該客戶端程式通常位於二進位安裝包的bin目錄中,或者通過rpm安裝包安裝mysql-community-client,是資料庫管理系統的重要組成部分。MySQL客戶端不僅僅是一個簡單的軟體工具,更是連接用戶與資料庫之間的橋梁,對於有效地使用 ...
  • 作者:櫰木 環境準備 本次使用到的二進位軟體包目錄為:系統初始化前提是操作系統已完成安裝、各個主機之間網路互通,系統常用命令已安裝,本預設這些前提條件已具備,不在闡述。 1 主機環境初始化 安裝centos系統完成後需要對主機進行初始化配置和驗證工作,在所有主機上(hd1.dtstack.com-h ...
  • 摘要 隨著任務數量、任務類型需求不斷增長,對我們的數據開發平臺提出了更高的要求。本文主要分享我們將調度引擎升級到 Apache DolphinScheduler 的實踐經驗,以及對數據開發平臺的一些思考。 1. 背景 首先介紹下我們的大數據平臺架構: 數據計算層承接了全公司的數據開發需求,負責運行各 ...
  • 一、背景 為瞭解決應卡頓,分析耗時。 二、原理 Looper中的loop方法: public static void loop() { ... for (;;) { ... // This must be in a local variable, in case a UI event sets th ...
  • ☞ Github ☜ ☞ Gitee ☜ 說明 Binder作為Android系統跨進程通信的核心機制。網上也有很多深度講解該機制的文章,如: Android跨進程通信詳解Binder機制原理 Android系統核心機制Binder【系列】 這些文章和系統源碼可以很好幫助我們理解Binder的實現原 ...
一周排行
    -Advertisement-
    Play Games
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...
  • 目錄前言PostgreSql安裝測試額外Nuget安裝Person.cs模擬運行Navicate連postgresql解決方案Garnet為什麼要選擇Garnet而不是RedisRedis不再開源Windows版的Redis是由微軟維護的Windows Redis版本老舊,後續可能不再更新Garne ...
  • C#TMS系統代碼-聯表報表學習 領導被裁了之後很快就有人上任了,幾乎是無縫銜接,很難讓我不想到這早就決定好了。我的職責沒有任何變化。感受下來這個系統封裝程度很高,我只要會調用方法就行。這個系統交付之後不會有太多問題,更多應該是做小需求,有大的開發任務應該也是第二期的事,嗯?怎麼感覺我變成運維了?而 ...
  • 我在隨筆《EAV模型(實體-屬性-值)的設計和低代碼的處理方案(1)》中介紹了一些基本的EAV模型設計知識和基於Winform場景下低代碼(或者說無代碼)的一些實現思路,在本篇隨筆中,我們來分析一下這種針對通用業務,且只需定義就能構建業務模塊存儲和界面的解決方案,其中的數據查詢處理的操作。 ...
  • 對某個遠程伺服器啟用和設置NTP服務(Windows系統) 打開註冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer 將 Enabled 的值設置為 1,這將啟用NTP伺服器功 ...
  • title: Django信號與擴展:深入理解與實踐 date: 2024/5/15 22:40:52 updated: 2024/5/15 22:40:52 categories: 後端開發 tags: Django 信號 松耦合 觀察者 擴展 安全 性能 第一部分:Django信號基礎 Djan ...
  • 使用xadmin2遇到的問題&解決 環境配置: 使用的模塊版本: 關聯的包 Django 3.2.15 mysqlclient 2.2.4 xadmin 2.0.1 django-crispy-forms >= 1.6.0 django-import-export >= 0.5.1 django-r ...
  • 今天我打算整點兒不一樣的內容,通過之前學習的TransformerMap和LazyMap鏈,想搞點不一樣的,所以我關註了另外一條鏈DefaultedMap鏈,主要調用鏈為: 調用鏈詳細描述: ObjectInputStream.readObject() DefaultedMap.readObject ...
  • 後端應用級開發者該如何擁抱 AI GC?就是在這樣的一個大的浪潮下,我們的傳統的應用級開發者。我們該如何選擇職業或者是如何去快速轉型,跟上這樣的一個行業的一個浪潮? 0 AI金字塔模型 越往上它的整個難度就是職業機會也好,或者說是整個的這個運作也好,它的難度會越大,然後越往下機會就會越多,所以這是一 ...
  • @Autowired是Spring框架提供的註解,@Resource是Java EE 5規範提供的註解。 @Autowired預設按照類型自動裝配,而@Resource預設按照名稱自動裝配。 @Autowired支持@Qualifier註解來指定裝配哪一個具有相同類型的bean,而@Resourc... ...