主頁 > 移動端開發 > Android事件分發機制二:viewGroup與view對事件的處理

Android事件分發機制二:viewGroup與view對事件的處理

2021-01-24 19:24:14 移動端開發

前言

很高興遇見你~

在上一篇文章 Android事件分發機制一:事件是如何到達activity的? 中,我們討論了觸摸資訊從螢屏產生到發送給具體 的view處理的整體流程,這里先來簡單回顧一下:

整體流程

  1. 觸摸資訊從手機觸摸螢屏時產生,通過IMS和WMS發送到viewRootImpl
  2. viewRootImpl把觸摸資訊傳遞給他所管理的view
  3. view根據自身的邏輯對事件進行分發
  4. 常見的如Activity布局的頂層viewGroup為DecorView,他對事件分發方法進行了重新,會優先回呼windowCallBack也就是Activity的分發方法
  5. 最后事件都會交給viewGroup去分發給子view

前面的分發步驟我們清楚了,那么viewGroup是如何對觸摸事件進行分發的呢?View又是如何處理觸摸資訊的呢?正是本文要討論的內容,

事件處理中涉及到的關鍵方法就是 dispatchTouchEvent ,不管是viewGroup還是view,在viewGroup中,dispatchTouchEvent 方法主要是把事件分發給子view,而在view中,dispatchTouchEvent 主要是處理消費事件,而主要的消費事件內容是在 onTouchEvent 方法中,下面討論的是viewGroup與view的默認實作,而在自定義view中,通常會重寫 dispatchTouchEventonTouchEvent 方法,例如DecorView等,

秉著邏輯先行原始碼后到的原則,本文雖然涉及到大量的原始碼,但會優先講清楚流程,有時間的讀者仍然建議閱讀完整原始碼,

理解MotionEvent

事件分發中涉及到一個很重要的點:多點觸控,這是在很多的文章中沒有體現出來的,而要理解viewGroup如何處理多點觸控,首先需要對觸摸事件資訊類:MotionEvent,有一定的認識,MotionEvent中承載了觸摸事件的很多資訊,理解它更有利于我們理解viewGroup的分發邏輯,所以,首先需要先理解MotionEvent,

觸摸事件的基本型別有三種:

  • ACTION_DOWN: 表示手指按下螢屏
  • ACTION_MOVE: 手指在螢屏上滑動時,會產生一系列的MOVE事件
  • ACTION_UP: 手指抬起,離開螢屏

一個完整的觸摸事件系列是:從ACTION_DOWN開始,到ACTION_UP結束 ,這其實很好理解,就是手指按下開始,手指抬起結束,

手指可能會在螢屏上滑動,那么中間會有大量的ACTION_MOVE事件,例如:ACTION_DOWN、ACTION_MOVE、ACTION_MOVE...、ACTION_UP,

這是正常的情況,而如果出現了一些例外的情況,事件序列被中斷,那么會產生一個取消事件:

  • ACTION_CANCEL:當出現例外情況事件序列被中斷,會產生該型別事件

所以,完整的事件序列是:從ACTION_DOWN開始,到ACTION_UP或者ACTION_CANCEL結束 ,當然,這是我們一個手指的情況,那么在多指操作的情況是怎么樣的呢?這里需要引入另外的事件型別:

  • ACTION_POINTER_DOWN: 當已經有一個手指按下的情況下,另一個手指按下會產生該事件
  • ACTION_POINTER_UP: 多個手指同時按下的情況下,抬起其中一個手指會產生該事件

區別于ACTION_DOWN和ACTION_UP,使用另外兩個事件型別來表示手指的按下與抬起,使得ACTION_DOWN和ACTION_UP可以作為一個完整的事件序列的邊界

同時,一個手指的事件序列,是從ACTION_DOWN/ACTION_POINTER_DOWN開始,到ACTION_UP/ACTION_POINTER_UP/ACTION_CANCEL結束,

到這里先簡單做個小結:

觸摸事件的型別有:ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_POINTER_DOWN、ACTION_POINTER_UP,他們分別代表不同的場景,

一個完整的事件序列是從ACTION_DOWN開始,到ACTION_UP或者ACTION_CANCEL結束,
一個手指的完整序列是從ACTION_DOWN/ACTION_POINTER_DOWN開始,到ACTION_UP/ACTION_POINTER_UP/ACTION_CANCEL結束,


第二,我們需要理解MotionEvent中所攜帶的資訊,

假如現在螢屏上有兩個手指按下,如下圖:

觸摸點a先按下,而觸摸點b按下,那么自然而然就會產生兩個事件:ACTION_DOWN和ACTION_POINTER_DOWN,那么是不是ACTION_DOWN事件就只包含有觸摸點a的資訊,而ACTION_POINTER_DOWN只包含觸摸點b的資訊呢?換句話說,這兩個事件是不是會獨立發出觸摸事件?答案是:不是,

每一個觸摸事件中,都包含有所有觸控點的資訊,例如上述的點b按下時產生的ACTION_POINTER_DOWN事件中,就包含了觸摸點a和觸摸點b的資訊,那么他是如何區分這兩個點的資訊?我們又是如何知道ACTION_POINTER_DOWN這個事件型別是屬于觸摸點a還是觸摸點b?

在MotionEvent物件內部,維護有一個陣列,這個陣列中的每一項對應不同的觸摸點的資訊,如下圖:

image.png

陣列下標稱為觸控點的索引,每個節點,擁有一個觸控點的完整資訊,這里要注意的是,一個觸控點的索引并不是一成不變的,而是會隨著觸控點的數目變化而變化,例如當同時按下兩個手指時,陣列情況如下圖:

image.png

而當手指a抬起后,陣列的情況變為下圖:

image.png

可以看到觸控點b的索引改變了,所以跟蹤一個觸控點必須是依靠一個觸控點的id,而不是他的索引

現在我們知道每一個MotionEvent內部都維護有所有觸控點的資訊,那么我們怎么知道這個事件是對應哪個觸控點呢?這就需要看到MotionEvent的一個方法:getAction

這個方法回傳一個整型變數,他的低1-8位表示該事件的型別,高9-16位表示觸控點索引,我們只需要將這16位進行分離,就可以知道觸控點的型別和所對應的觸控點,同時,MotionEvent有兩個獲取觸控點坐標的方法:getX()/getY() ,他們都需要傳入一個觸控點索引來表示獲取哪個觸控點的坐標資訊,

同時還要注意的是,MOVE事件和CANCEL事件是沒有包含觸控點索引的,只有DOWN型別和UP型別的事件才包含觸控點索引,這里是因為非DOWN/UP事件,不涉及到觸控點的增加與洗掉,

這里我們再來小結一下:

  • 一個MotionEvent物件內部使用一個陣列來維護所有觸控點的資訊
  • UP/DOWN型別的事件包含了觸控點索引,可以根據該索引做出對應的操作
  • 觸控點的索引是變化的,不能作為跟蹤的依據,而必須依據觸控點id

關于MotionEvent需要了解一個更加重要的點:事件分離,

首先需要知道事件分發的一個原則:一個view消費了某一個觸點的down事件后,該觸點事件序列的后續事件,都由該view消費 ,這也比較符合我們的操作習慣,當我們按下一個控制元件后,只要我們的手指一直沒有離開螢屏,那么我們希望這個手指滑動的資訊都交給這個view來處理,換句話說,一個觸控點的事件序列,只能給一個view消費,

經過前面的描述我們知道,一個事件是包含所有觸摸點的資訊的,當viewGroup在派發事件時,每個觸摸點的資訊就需要分開分別發送給感興趣的view,這就是事件分離,

例如Button1接收了觸摸點a的down事件,Button2接收了觸摸點b的down事件,那么當一個MotionEvent物件到來時,需要將他里面的觸摸點資訊,把觸摸點a的資訊拆開發送給button1,把觸摸點b的資訊拆開發送給button2,如下圖:

事件分離

那么,可不可以不進行分離?當然可以,這樣的話每次都把所有觸控點的信息發送給子view,這可以通過FLAG_SPLIT_MOTION_EVENTS這個標志進行設定是否要進行分離,

小結一下:

一個觸控點的序列一般情況下只給一個view處理,當一個view消費了一個觸控點的down事件后,該觸控點的事件序列后續事件都會交給他處理,

事件分離是把一個motionEvent中的觸控點資訊進行分離,只向子view發送其感興趣的觸控點資訊,

我們可以通過設定FLAG_SPLIT_MOTION_EVENTS標志讓viewGroup是否對事件進行分離


到這里關于MotionEvent的內容就講得差不多,當然在分離的時候,還需要進行一定的調整,例如坐標軸的更改、事件型別的更改等等,放在后面講,接下來看看ViewGroup是如何分發事件的,

ViewGroup對于事件的分發

這一步可以說是事件分發中的重頭戲了,不過在理解了上面的MotionEvent之后,對于ViewGroup的分發細節也就容易理解了,

整體來說,ViewGroup分發事件分為三個大部分,后面的內容也會圍繞著三大部分展開:

  1. 攔截事件:在一定情況下,viewGroup有權利選擇攔截事件或者交給子view處理
  2. 尋找接收事件序列的控制元件:每一個需要分發給子view的down事件都會先尋找是否有適合的子view,讓子view來消費整個事件序列
  3. 派發事件:把事件分發到感興趣的子view中或自己處理

大體的流程是:每一個事件viewGroup會先判斷是否要攔截,如果是down事件(這里的down事件表示ACTION_DOWN和ACTION_POINTER_DOWN,下同),還需要挨個遍歷子view看看是否有子view消費了down事件,最后再把事件派發下去,

在開始決議之前,必須先了解一個關鍵物件:TouchTarget,

TouchTarget

前面我們講到:一個觸控點的序列一般情況下只給一個view處理,當一個view消費了一個觸控點的down事件后,該觸控點的事件序列后續事件都會交給他處理,對于viewGroup來說,他有很多個子view,如果不同的子view接受了不同的觸控點的down事件,那么ViewGroup如何記錄這些資訊并精準把事件發送給對應的子view呢?答案就是:TouchTarget,

TouchTarget中維護了每個子view以及所對應的觸控點id,這里的id可以不止一個,TouchTarget本身是個鏈表,每個節點記錄了子view所對應的觸控點id,在viewGroup中,該鏈表的鏈表頭是mFirstTouchTarget,如果他為null,表示沒有任何子view接收了down事件,

TouchTarget有個非常神奇的設計,他只使用一個整型變數來記錄所有的觸控id,整型變數中哪一個二進制位為1,則對應系結該id的觸控點,

例如 00000000 00000000 00000000 10001000,則表示系結了id為3和id為7的兩個觸控點,因為第3位和第7位的二進制位是1,這里可以間接說明系統支持的最大多點觸控數是32,當然實際上一般是8比較多,當要判斷一個TouchTarget系結了哪些id時,只需要通過一定的位操作即可,既提高了速度,也優化了空間占用,

當一個down事件來臨時,viewGroup會為這個down事件尋找適合的子view,并為他們創建一個TouchTarget加入到鏈表中,而當一個up事件來臨時,viewGroup會把對應的TouchTarget節點資訊洗掉,那接下來,就直接看到viewGroup中的dispatchTouchEvent 是如何分發事件的,首先看到原始碼中的第一部分:事件攔截,


事件攔截

這里的攔截分為兩部分:安全攔截和邏輯攔截,

安全攔截是一直被忽略的一種情況,當一個控制元件a被另一個非全屏控制元件b遮擋住的時候,那么有可能被惡意軟體操作發生危險,例如我們看到的界面是這樣的:

但實際上,我們看到的這個按鈕時不可點擊的,實際上觸摸事件會被分發到這個按鈕后面的真正接收事件的按鈕:

然后我們就白給了,這個安全攔截行為由兩個標志控制:

  • FILTER_TOUCHES_WHEN_OBSCURED:這個標志可以手動給控制元件設定,表示被非全屏控制元件覆寫時,直接過濾掉所有觸摸事件,
  • FLAG_WINDOW_IS_OBSCURED:這個標志表示當前視窗被一個非全屏控制元件覆寫,

具體的原始碼如下:

View.java api29
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
    // 兩個標志,前者表示當被覆寫時不處理;后者表示當前視窗是否被非全屏視窗覆寫
    if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
            && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
        // Window is obscured, drop this touch.
        return false;
    }
    return true;
}

第二種攔截是邏輯攔截,如果當前viewGroup中沒有TouchTarget,而且這個事件不是down事件,這就意味著viewGroup自己消費了先前的down事件,那么這個事件就無須分發到子view必須自己消費,也就不需要攔截這種情況的事件,除此之外的事件都是需要分發到子view,那么viewGroup就可以對他們進行判斷是否進行攔截,簡單來說,只有需要分發到子view的事件才需要攔截

判斷是否攔截主要依靠兩個因素:FLAG_DISALLOW_INTERCEPT標志和 onInterceptTouchEvent() 方法,

  1. 子view可以通過requestDisallowInterupt方法強制要求viewGroup不要攔截事件,viewGroup中會設定一個FLAG_DISALLOW_INTERCEPT標志表示不攔截事件,但是當前事件序列結束后,這個標志會被清除,如果需要的話需要再次呼叫requestDisallowInterupt方法進行設定,
  2. 如果子view沒有強制要求不攔截,那么會呼叫onInterceptTouchEvent() 方法判斷是否需要攔截,onInterceptTouchEvent方法默認只對一種特殊情況作了攔截,一般情況下我們會重寫這個方法來攔截事件:
// 只對一種特殊情況做了攔截
// 滑鼠左鍵點擊了滑動塊
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}

viewGroup的 dispatchTouchEvent 方法邏輯中對于事件攔截部分的原始碼分析如下:

ViewGroup.java api29
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
        
    // 對遮蓋狀態進行過濾
    if (onFilterTouchEventForSecurity(ev)) {
        
        ...

        // 判斷是否需要攔截
        final boolean intercepted;
        // down事件或者有target的非down事件則需要判斷是否需要攔截
        // 否則不需要進行攔截判斷,因為一定是交給自己處理
        if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
            // 此標志為子view通過requestDisallowInterupt方法設定
            // 禁止viewGroup攔截事件
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // 呼叫onInterceptTouchEvent判斷是否需要攔截
                intercepted = onInterceptTouchEvent(ev);
                // 恢復事件狀態
                ev.setAction(action); 
            } else {
                intercepted = false;
            }
        } else {
            // 自己消費了down事件,那么后續的事件非down事件都是自己處理
            intercepted = true;
        }
        ...;
    }
    ...;
}

尋找消費down事件的子控制元件

對于每一個down事件,不管是ACTION_DOWN還是ACTION_POINTER_DOWN,viewGroup都會優先在控制元件樹中尋找合適的子控制元件來消費他,因為對于每一個down事件,標志著一個觸控點的一個嶄新的事件序列,viewGroup會盡自己的最大能力尋找合適的子控制元件,如果找不到合適的子控制元件,才會自己處理down事件,因為,消費了down事件,意味著接下來該觸控點的事件序列事件都會交給該view消費,如果viewGroup攔截了事件,那么子view就無法接收到任何事件訊息,

viewGroup尋找子控制元件的步驟也不復雜,首先viewGroup會為他的子控制元件構造一個控制元件串列,構造的順序是view的繪制順序的逆序,也就是一個view的z軸系數越高,顯示高度越高,在串列的順序就會越靠前,這其實比較好理解,顯示越高的控制元件肯定是優先接收點擊的,除了默認情況,我們也可以進行自定義串列順序,這里就不展開了,

viewGroup會按順序遍歷整個串列,判斷觸控點的位置是否在該view的范圍內、該view是否可以點擊等,尋找合適的子view,如果找到合適的子view,則會把down事件分發給他,如果該view接收事件,則會為他創建一個TouchTarget,將該觸控id和view進行系結,之后該觸控點的事件就可以直接分發給他了,

而如果沒有一個控制元件適合,那么會默認選取TouchTarget鏈表的最新一個節點,也就是當我們多點觸控時,兩次手指按下,如果沒有找到合適的子view,那么就被認為是和上一個手指點擊的是同個view,因此,如果viewGroup當前有正在消費事件的子控制元件,那么viewGroup自己是不會消費down事件的,

接下來我們看看原始碼分析(代碼有點長,需要慢慢分析理解):

ViewGroup.java api29
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
         
    // 對遮蓋狀態進行過濾
    if (onFilterTouchEventForSecurity(ev)) {
        
        // action的高9-16位表示索引值
        // 低1-8位表示事件型別
        // 只有down或者up事件才有索引值
        final int action = ev.getAction();
        // 獲取到真正的事件型別
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        ...

        // 攔截內容的邏輯
        if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
            ...
        } 

        ...

        // 三個變數:
        // split表示是否需要對事件進行分裂,對應多點觸摸事件
        // newTouchTarget 如果是down或pointer_down事件的新的系結target
        // alreadyDispatchedToNewTouchTarget 表示事件是否已經分發給targetview了
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        
        // 如果沒有取消和攔截進入分發
        if (!canceled && !intercepted) {
			...
			// down或pointer_down事件,表示新的手指按下了,需要尋找接收事件的view
            if (actionMasked == MotionEvent.ACTION_DOWN
                || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                
                // 多點觸控會有不同的索引,獲取索引號
                // 該索引位于MotionEvent中的一個陣列,索引值就是陣列下標值
                // 只有up或down事件才會攜帶索引值
                final int actionIndex = ev.getActionIndex(); 
                // 這個整型變數記錄了TouchTarget中view所對應的觸控點id
                // 觸控點id的范圍是0-31,整型變數中哪一個二進制位為1,則對應系結該id的觸控點
                // 例如 00000000 00000000 00000000 10001000
                // 則表示系結了id為3和id為7的兩個觸控點
                // 這里根據是否需要分離,對觸控點id進行記錄,
                // 而如果不需要分離,則默認接收所有觸控點的事件
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                    : TouchTarget.ALL_POINTER_IDS;

                // down事件表示該觸控點事件序列是一個新的序列
                // 清除之前系結到到該觸控id的TouchTarget
                removePointersFromTouchTargets(idBitsToAssign);

                final int childrenCount = mChildrenCount;
                // 如果子控制元件數目不為0而且還沒系結到新的id
                if (newTouchTarget == null && childrenCount != 0) {
                    // 使用觸控點索引獲取觸控點位置
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // 從前到后創建view串列
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    // 判斷是否是自定義view順序
                    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);
                        
                        ...

                        // 檢查該子view是否可以接受觸摸事件和是否在點擊的范圍內
                        if (!child.canReceivePointerEvents()
                            || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        // 檢查該子view是否在touchTarget鏈表中
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // 鏈表中已經存在該子view,說明這是一個多點觸摸事件
                            // 即兩次都觸摸到同一個view上
                            // 將新的觸控點id系結到該TouchTarget上
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }

                        resetCancelNextUpFlag(child);
                        // 找到合適的子view,把事件分發給他,看該子view是否消費了down事件
                        // 如果消費了,需要生成新的TouchTarget
                        // 如果沒有消費,說明子view不接受該down事件,繼續回圈尋找合適的子控制元件
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // 保存該觸控事件的相關資訊
                            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();
                            // 保存該view到target鏈表
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            // 標記已經分發給子view,退出回圈
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }

                        ...
                    }// 這里對應for (int i = childrenCount - 1; i >= 0; i--)
                    ...
                }// 這里對應判斷:(newTouchTarget == null && childrenCount != 0)

                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // 沒有子view接收down事件,直接選擇鏈表尾的view作為target
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
                
            }// 這里對應if (actionMasked == MotionEvent.ACTION_DOWN...)
        }// 這里對應if (!canceled && !intercepted)
        ...
    }// 這里對應if (onFilterTouchEventForSecurity(ev))
    ...
}

派發事件

經過了攔截與尋找消費down事件的控制元件之后,無論前面的處理結果如何,最終都是需要將事件進行派發,不管是派發給自己還是子控制元件,這里派發的物件只有兩個:viewGroup自身或TouchTarget,

經過了前面的尋找消費down事件子控制元件步驟,那么每個觸控點都找到了消費自己事件序列的控制元件并系結在了TouchTarget中;而如果沒有找到合適的子控制元件,那么消費的物件就是viewGroup自己,因此派發事件的主要任務就是:把不同觸控點的資訊分發給合適的viewGroup或touchTarget,

派發的邏輯需要結合前面MotionEvent和TouchTarget的內容,我們知道MotionEvent包含了當前螢屏所有觸控點資訊,而viewGroup的每個TouchTarget則包含了不同的view所感興趣的觸控點,
如果不需要進行事件分離,那么直接將當前的所有觸控點的資訊都發送給每個TouchTarget即可;
如果需要進行事件分離,那么會將MotionEvent中不同觸控點的資訊拆開分別創建新的MotionEvent,并發送給感興趣的子控制元件;
如果TouchTarget鏈表為空,那么直接分發給viewGroup自己;所以touchTarget不為空的情況下,viewGroup自己是不會消費事件的,這也就意味著viewGroup和其中的view不會同時消費事件,

事件分離派發事件

上圖展示了需要事件分離的情況下進行的事件分發,

在把原MotionEvent拆分成多個MotionEvent時,不僅需要把不同的觸控點資訊進行分離,還需要對坐標進行轉換和改變事件型別:

  • 我們接收到的觸控點的位置資訊并不是基于螢屏坐標系,而是基于當前view的坐標系,所以當viewGroup往子view分發事件時,需要把觸控點的資訊轉換成對應view的坐標系,
  • viewGroup收到的事件型別和子view收到的事件型別并不是完全一致的,在分發給子view的時候,viewGroup需要對事件型別進行修改,一般有以下情況需要修改:
    1. viewGroup收到一個ACTION_POINTER_DOWN事件分發給一個子view,但是該子view前面沒有收到其他的down事件,所以對于該view來說這是一個嶄新的事件序列,所以需要把這個ACTION_POINTER_DOWN事件型別改為ACTION_DOWN再發送給子view,
    2. viewGroup收到一個ACTION_POINTER_DOWN或ACTION_POINTER_UP事件,假設這個事件型別對應觸控點2,但是有一個子view他只對觸控點1的事件序列感興趣,那么在分離出觸控點1的資訊之后,還需要把事件型別改為ACTION_MOVE再分發給該子view,
  • 注意,把原MotionEvent物件拆分為多個MotionEvent物件之后,觸控點的索引也發生了改變,如果需要分發一個ACTION_POINTER_DOWN/UP事件給子view,那么需要注意更新觸控點的索引值,

viewGroup中真正執行事件派發的關鍵方法是 dispatchTransformedTouchEvent ,該方法會完成關鍵的事件分發邏輯,原始碼分析如下:

ViewGroup.java api29
// 該方法接收原MotionEvent事件、是否進行取消、目標子view、以及目標子view感興趣的觸控id
// 如果不是取消事件這個方法會把原MotionEvent中的觸控點資訊拆分出目標view感興趣的觸控點資訊
// 如果是取消事件則不需要拆分直接發送取消事件即可
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) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    // oldPointerIdBits表示現在所有的觸控id
    // desirePointerIdBits來自于該view所在的touchTarget,表示該view感興趣的觸控點id
    // 因為desirePointerIdBits有可能全是1,所以需要和oldPointerIdBits進行位與
    // 得到真正可接收的觸控點資訊
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

    // 控制元件處于不一致的狀態,正在接受事件序列卻沒有一個觸控點id符合
    if (newPointerIdBits == 0) {
        return false;
    }

    // 來自原始MotionEvent的新的MotionEvent,只包含目標感興趣的觸控點
    // 最終派發的是這個MotionEvent
    final MotionEvent transformedEvent;
    
    // 兩者相等,表示該view接受所有的觸控點的事件
    // 這個時候transformedEvent相當于原始MotionEvent的復制
    if (newPointerIdBits == oldPointerIdBits) {
        // 當目標控制元件不存在通過setScaleX()等方法進行的變換時,
        // 為了效率會將原始事件簡單地進行控制元件位置與滾動量變換之后
        // 發送給目標的dispatchTouchEvent()方法并回傳,
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);

                handled = child.dispatchTouchEvent(event);

                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        // 復制原始MotionEvent
        transformedEvent = MotionEvent.obtain(event);
    } else {
        // 如果兩者不等,說明需要對事件進行拆分
        // 只生成目標感興趣的觸控點的資訊
        // 這里分離事件包括了修改事件的型別、觸控點索引等
        transformedEvent = event.split(newPointerIdBits);
    }

    // 對MotionEvent的坐標系,轉換為目標控制元件的坐標系并進行分發
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        // 計算滾動量偏移
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        // 存在scale等變換,需要進行矩陣轉換
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }
		// 呼叫子view的方法進行分發
        handled = child.dispatchTouchEvent(transformedEvent);
    }

    // 分發完畢,回收MotionEvent
    transformedEvent.recycle();
    return handled;
}

好了,了解完上面的內容,來看看viewGroup的 dispatchTouchEvent 中派發事件的代碼部分:

ViewGroup.java api29
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
        
    // 對遮蓋狀態進行過濾
    if (onFilterTouchEventForSecurity(ev)) {
		...

		
        if (mFirstTouchTarget == null) {
            // 經過了前面的處理,到這里touchTarget依舊為null,說明沒有找到處理down事件的子控制元件
            // 或者down事件被viewGroup本身消費了,所以該事件由viewGroup自己處理
            // 這里呼叫了dispatchTransformedTouchEvent方法來分發事件
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                                                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 已經有子view消費了down事件
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            // 遍歷所有的TouchTarget并把事件分發下去
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    // 表示事件在前面已經處理了,不需要重復處理
                    handled = true;
                } else {
                    // 正常分發事件或者分發取消事件
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
                    // 這里呼叫了dispatchTransformedTouchEvent方法來分發事件
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                                                      target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    // 如果發送了取消事件,則移除該target
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        // 如果接收到取消獲取up事件,說明事件序列結束
        // 直接洗掉所有的TouchTarget
        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);
            // 如果僅僅只是一個PONITER_UP
            // 清除對應觸控點的觸摸資訊
            removePointersFromTouchTargets(idBitsToRemove);
        }
        
    }// 這里對應if (onFilterTouchEventForSecurity(ev))

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

小結

到這里,viewGroup的事件分發原始碼就決議完成了,這里再來小結一下:

  • 每一個觸控點的事件序列,只能給一個view消費;如果一個view消費了一個觸控點的down事件,那么該觸控點的后續事件都會給他處理,
  • 每一個事件到達viewGroup,如果需要分發到子view,那么viewGroup會新判斷是否要攔截,
    • 當viewGroup的touchTarget!=null || 事件的型別為down 需要進行判斷是否攔截;
    • 判斷是否攔截受兩個因素影響:onInterceptTouchEvent和FLAG_DISALLOW_INTERCEPT標志
  • 如果該事件是down型別,那么需要遍歷所有的子控制元件判斷是否有子控制元件消費該down事件
    • 當有新的down事件被消費時,viewGroup會把該view和對應的觸控點id系結起來存盤到touchTarget中
  • 根據前面的處理情況,將事件派發到viewGroup自身或touchTarget中
    • 如果touchTarget==null,說明沒有子控制元件消費了down事件,那么viewGroup自己處理事件
    • 否則將事件分離成多個MotionEvent,每個MotionEvent只包含對應view感興趣的觸控點的資訊,并派發給對應的子view

viewGroup中的原始碼很多,但大體的邏輯也就這三大部分,理解好MotionEvent和TouchTarget的設計,那么理解viewGroup的事件分發原始碼也是手到擒來,上面的原始碼我省略了一些細節內容,下面附上完整的viewGroup分發代碼,

ViewGroup.java api29
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 一致性檢驗器,用于除錯用途
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }
        
    // 輔助功能,用于輔助有障礙人群使用;
    // 如果這個事件是輔助功能事件,那么他會帶有一個target view,要求事件必須分發給該view
    // 如果setTargetAccessibilityFocus(false),表示取消輔助功能事件,按照常規的事件分發進行
    // 這里表示如果當前是目標target view,則取消標志,直接按照普通分發即可
    // 后面還有很多類似的代碼,都是同樣的道理
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }   

    boolean handled = false;
    // 對遮蓋狀態進行過濾
    if (onFilterTouchEventForSecurity(ev)) {
        
        // action的高9-16位表示索引值
        // 低1-8位表示事件型別
        // 只有down或者up事件才有索引值
        final int action = ev.getAction();
        // 獲取到真正的事件型別
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // ACTION_DOWN事件,表示這是一個全新的事件序列,會清除所有的touchTarget,重置所有狀態
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        // 判斷是否需要攔截
        final boolean intercepted;
        // down事件或者有target的非down事件則需要判斷是否需要攔截
        // 否則直接攔截自己處理
        if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
            // 此標志為子view通過requestDisallowInterupt方法設定
            // 禁止viewGroup攔截事件
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // 呼叫onInterceptTouchEvent判斷是否需要攔截
                intercepted = onInterceptTouchEvent(ev);
                // 恢復事件狀態
                ev.setAction(action); 
            } else {
                intercepted = false;
            }
        } else {
            // 自己消費了down事件
            intercepted = true;
        }

        // 如果已經被攔截、或者已經有了目標view,取消輔助功能的target標志
        if (intercepted || mFirstTouchTarget != null) {
            ev.setTargetAccessibilityFocus(false);
        }

        // 判斷是否需要取消
        // 這里有很多種情況需要發送取消事件
        // 最常見的是viewGroup攔截了子view的ACTION_MOVE事件,導致事件序列中斷
        // 那么需要發送cancel事件告知該view,讓該view做一些狀態恢復作業
        final boolean canceled = resetCancelNextUpFlag(this)
            || actionMasked == MotionEvent.ACTION_CANCEL;

        // 三個變數:
        // 是否需要對事件進行分裂,對應多點觸摸事件
        // newTouchTarget 如果是down或pointer_down事件的新的系結target
        // alreadyDispatchedToNewTouchTarget 是否已經分發給target view了
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        
        // 下面部分的代碼是尋找消費down事件的子控制元件
        // 如果沒有取消和攔截進入分發
        if (!canceled && !intercepted) {
			// 如果是輔助功能事件,我們會尋找他的target view來接收這個事件
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;
            
			// down或pointer_down事件,表示新的手指按下了,需要尋找接收事件的view
            if (actionMasked == MotionEvent.ACTION_DOWN
                || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                
                // 多點觸控會有不同的索引,獲取索引號
                // 該索引位于MotionEvent中的一個陣列,索引值就是陣列下標值
                // 只有up或down事件才會攜帶索引值
                final int actionIndex = ev.getActionIndex(); 
                
                // 這個整型變數記錄了TouchTarget中view所對應的觸控點id
                // 觸控點id的范圍是0-31,整型變數中哪一個二進制位為1,則對應系結該id的觸控點
                // 例如 00000000 00000000 00000000 10001000
                // 則表示系結了id為3和id為7的兩個觸控點
                // 這里根據是否需要分離,對觸控點id進行記錄,
                // 而如果不需要分離,則默認接收所有觸控點的事件
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                    : TouchTarget.ALL_POINTER_IDS;

                // 清除之前獲取到該觸控id的TouchTarget
                removePointersFromTouchTargets(idBitsToAssign);

                // 如果子控制元件的數量等于0,那么不需要進行遍歷只能給viewGroup自己處理
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    // 使用觸控點索引獲取觸控點位置
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // 從前到后創建view串列
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    // 這一句判斷是否是自定義view順序
                    final boolean customOrder = preorderedList == null
                        && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    
                     // 遍歷所有子控制元件
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        // 獲得真正的索引和子view
                        final int childIndex = getAndVerifyPreorderedIndex(
                            childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                            preorderedList, children, childIndex);

                        // 如果是輔助功能事件,則優先給對應的target先處理
                        // 如果該view不處理,再交給其他的view處理
                        if (childWithAccessibilityFocus != null) {
                            if (childWithAccessibilityFocus != child) {
                                continue;
                            }
                            childWithAccessibilityFocus = null;
                            i = childrenCount - 1;
                        }

                        // 檢查該子view是否可以接受觸摸事件和是否在點擊的范圍內
                        if (!child.canReceivePointerEvents()
                            || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        // 檢查該子view是否在touchTarget鏈表中
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // 鏈表中已經存在該子view,說明這是一個多點觸摸事件
                            // 將新的觸控點id系結到該TouchTarget上
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }
						
                        // 設定取消標志
                        // 下一次再次呼叫這個方法就會回傳true
                        resetCancelNextUpFlag(child);
                        
                        // 找到合適的子view,把事件分發給他,看該子view是否消費了down事件
                        // 如果消費了,需要生成新的TouchTarget
                        // 如果沒有消費,說明子view不接受該down事件,繼續回圈尋找合適的子控制元件
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // 保存資訊
                            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();
                            // 保存該view到target鏈表
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            // 標記已經分發給子view,退出回圈
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }

                        // 輔助功能事件對應的targetView沒有消費該事件,則繼續分發給普通view
                        ev.setTargetAccessibilityFocus(false);
                        
                    }// 這里對應for (int i = childrenCount - 1; i >= 0; i--)
                    
                    if (preorderedList != null) preorderedList.clear();
                    
                }// 這里對應判斷:(newTouchTarget == null && childrenCount != 0)

                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // 沒有子view接收down事件,直接選擇鏈表尾的view作為target
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }// 這里對應if (actionMasked == MotionEvent.ACTION_DOWN...)
        }// 這里對應if (!canceled && !intercepted)

        if (mFirstTouchTarget == null) {
            // 經過了前面的處理,到這里touchTarget依舊為null,說明沒有找到處理down事件的子控制元件
            // 或者down事件被viewGroup本身消費了,所以該事件由viewGroup自己處理
            // 這里呼叫了dispatchTransformedTouchEvent方法來分發事件
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                                                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 已經有子view消費了down事件
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            // 遍歷所有的TouchTarget并把事件分發下去
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    // 表示事件在前面已經處理了,不需要重復處理
                    handled = true;
                } else {
                    // 正常分發事件或者分發取消事件
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
                    // 這里呼叫了dispatchTransformedTouchEvent方法來分發事件
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                                                      target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    // 如果發送了取消事件,則移除該target
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        // 如果接收到取消獲取up事件,說明事件序列結束
        // 直接洗掉所有的TouchTarget
        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);
            // 如果僅僅只是一個PONITER_UP
            // 清除對應觸控點的觸摸資訊
            removePointersFromTouchTargets(idBitsToRemove);
        }
        
    }// 這里對應if (onFilterTouchEventForSecurity(ev))

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

View對于事件的分發

不管是viewGroup自己處理事件,還是view處理事件,如果沒有被子類攔截(子類重寫方法),最終都會呼叫到 view.dispatchTouchEvent 方法來處理事件,view處理事件的邏輯就比viewGroup簡單多了,因為它不需要向下去分發事件,只需要自己處理,整體的邏輯如下:

  1. 首先判斷是否被其他非全屏view覆寫,這和上面viewGroup的安全性檢查是一樣的
  2. 經過檢查之后先檢查是否有onTouchListener監聽器,如果有則呼叫它
  3. 如果第2步沒有消費事件,那么會呼叫onTouchEvent方法來處理事件
    • 這個方法是view處理事件的核心,里面包含了點擊、雙擊、長按等邏輯的處理需要重點關注,

我們先看到 view.dispatchTouchEvent 方法原始碼:

View.java api29
public boolean dispatchTouchEvent(MotionEvent event) {
    // 首先處理輔助功能事件
    if (event.isTargetAccessibilityFocus()) {
        // 本控制元件沒有獲取到焦點,不處理事件
        if (!isAccessibilityFocusedViewOrHost()) {
            return false;
        }
        // 獲取到焦點,按照常規處理事件
        event.setTargetAccessibilityFocus(false);
    }

    // 表示是否消費事件
    boolean result = false;

    // 一致性檢驗器,檢驗事件是否一致
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    } 

    // 如果是down事件,停止嵌套滑動
    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        stopNestedScroll();
    }

    // 安全過濾,本視窗位于非全屏視窗之下時,可能會阻止控制元件處理觸摸事件
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            // 如果事件為滑鼠拖動滾動條
            result = true;
        }
        // 先呼叫onTouchListener監聽器
        // 當我們設定onTouchEventListener之后,L
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        // 若onTouchListener沒有消費事件,呼叫onTouchEvent方法
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    // 一致性檢驗
    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }

    // 如果是事件序列終止事件或者沒有消費down事件,終止嵌套滑動
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}

原始碼內容不長,主要的邏輯內容上面已經講了,其他的都是一些細節的處理,onTouchListener一般情況下我們是不會使用,那么接下來我們直接看到onTouchEvent方法,

onTouchEvent總體上就做一件事:根據按下情況選擇觸發onClickListener或者onLongClickListener ,也就是判斷是單擊還是長按事件,其他的原始碼都是實作細節,onTouchEvent方法正確處理每一個事件型別,來確保點擊與長按監聽器可以被準確地執行,理解onTouchEvent的原始碼之前,有幾個重要的點需要先了解一下,

我們的操作模式有按鍵模式、觸摸模式,按鍵模式對應的是外接鍵盤或者以前的老式鍵盤機,在按鍵模式下我們要點擊一個按鈕通常都是先使用方向游標選中一個button(也就是讓該button獲取到focus),然后再點擊確認按下一個button,但是在觸摸模式下,button卻不需要獲取焦點,如果一個view在觸摸模式下可以獲取焦點,那么他將無法回應點擊事件,也就是無法呼叫onClickListener監聽器 ,例如EditText,

view辨別單擊和長按的方法是設定延時任務,在原始碼中會看到很多的類似的代碼,這里延時任務使用handler來實作,當一個down事件來臨時,會添加一個延時任務到訊息佇列中,如果時間到還沒有接收到up事件,說明這是個長按事件,那么就會呼叫onLongClickListener監聽器,而如果在延時時間內收到了up事件,那么說明這是個單擊事件,取消這個延時的任務,并呼叫onClickListener,判斷是否是一個長按事件,呼叫的是 checkForLongClick 方法來設定延時任務:

// 接收四個引數:
// delay:延時的時長;x、y: 觸控點的位置;classification:長按型別分類
private void checkForLongClick(long delay, float x, float y, int classification) {
    // 只有是可以長按或者長按會顯示工具提示的view才會創建延時任務
    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
        // 標志還沒觸發長按
        // 如果延遲時間到,觸發長按監聽,這個變數 就會被設定為true
        // 那么當up事件到來時,就不會觸摸單擊監聽,也就是onClickListener
        mHasPerformedLongPress = false;

        // 創建CheckForLongPress
        // 這是一個實作Runnable介面的類,run方法中回呼了onLongClickListener
        if (mPendingCheckForLongPress == null) {
            mPendingCheckForLongPress = new CheckForLongPress();
        }
        // 設定引數
        mPendingCheckForLongPress.setAnchor(x, y);
        mPendingCheckForLongPress.rememberWindowAttachCount();
        mPendingCheckForLongPress.rememberPressedState();
        mPendingCheckForLongPress.setClassification(classification);
        // 使用handler發送延時任務
        postDelayed(mPendingCheckForLongPress, delay);
    }
}

上面這個方法的邏輯還是比較簡單的,下面看看 CheckForLongPress 這個類:

private final class CheckForLongPress implements Runnable {
...
    @Override
    public void run() {
        if ((mOriginalPressedState == isPressed()) && (mParent != null)
                && mOriginalWindowAttachCount == mWindowAttachCount) {
            recordGestureClassification(mClassification);
            // 在延時時間到之后,就會運行這個任務
            // 呼叫onLongClickListener監聽器
            // 并設定mHasPerformedLongPress為true
            if (performLongClick(mX, mY)) {
                mHasPerformedLongPress = true;
            }
        }
    }
...
}

延遲時間結束后,就會運行 CheckForLongPress 物件,回呼onLongClickListener,這樣就表示這是一個長按的事件了,

另外,在默認的情況下,當我們按住一個view,然后手指滑動到該view所在的范圍之外,那么系統會認為你對這個view已經不感興趣,所以無法觸發單擊和長按事件,當然,很多時候并不是如此,這就需要具體的view來重寫onTouchEvent邏輯了,但是view的默認實作是這樣的邏輯,

好了,那么接下來就來看一下完整的 view.onTouchEvent 代碼:

View.java api29
public boolean onTouchEvent(MotionEvent event) {
    // 獲取觸控點坐標
    // 這里我們發現他是沒有傳入觸控點索引的
    // 所以默認情況下view是只處理索引為0的觸控點
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();

    // 判斷是否是可點擊的
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

    // 一個被禁用的view如果被設定為clickable,那么他仍舊是可以消費事件的
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            // 如果是按下狀態,取消按下狀態
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        // 回傳是否可以消費事件
        return clickable;
    }
    
    // 如果設定了觸摸事件代理你,那么直接呼叫代理來處理事件
    // 如果代理消費了事件則回傳true
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    // 如果該控制元件是可點擊的,或者長按會出現工具提示
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                // 如果是長按顯示工具類標志,回呼該方法
                if ((viewFlags & TOOLTIP) == TOOLTIP) {
                    handleTooltipUp();
                }
                // 如果是不可點擊的view,同時會清除所有的標志,恢復狀態
                if (!clickable) {
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;
                }
                
                // 判斷是否是按下狀態
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    // 如果可以獲取焦點但是沒有獲得焦點,請求獲取焦點
                    // 正常的觸摸模式下是不需要獲取焦點,例如我們的button
                    // 但是如果在按鍵模式下,需要先移動游標選中按鈕,也就是獲取focus
                    // 再點擊確認觸摸按鈕事件
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }

                    if (prepressed) {
                        // 確保用戶看到按下狀態
                        setPressed(true, x, y);
                    }

                    // 兩個引數分別是:長按事件是否已經回應、是否忽略本次up事件
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // 這是一個單擊事件,還沒到達長按的時間,移除長按標志
                        removeLongPressCallback();

                        // 只有不能獲取焦點的控制元件才能觸摸click監聽
                        if (!focusTaken) {
                            // 這里使用發送到訊息佇列的方式而不是立即執行onClickListener
                            // 原因在于可以在點擊前觸發一些其他視覺效果
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClickInternal();
                            }
                        }
                    }

                    // 取消按下狀態
                    // 這里也是個post任務
                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }
                    if (prepressed) {
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                        // 如果發送到佇列失敗,則直接取消
                        mUnsetPressedState.run();
                    }

                    // 移除單擊標志
                    removeTapCallback();
                }
                // 忽略下次up事件標志設定為false
                mIgnoreNextUpEvent = false;
                break;

            case MotionEvent.ACTION_DOWN:
                // 輸入設備源是否是可觸摸螢屏
                if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                    mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                }
                // 標志是否是長按
                mHasPerformedLongPress = false;

                // 如果是不可點擊的view,說明是長按提示工具的view
                // 直接檢查是否發生了長按
                if (!clickable) {
                    // 這個方法會發送一個延遲的任務
                    // 如果延遲時間到還是按下狀態,那么就會回呼onLongClickListener介面
                    checkForLongClick(
                            ViewConfiguration.getLongPressTimeout(),
                            x,
                            y,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    break;
                }

                // 判斷是否是滑鼠右鍵或者手寫筆的第一個按鈕
                // 特殊處理直接回傳
                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

                // 向上遍歷view查看是否在一個可滑動的容器中
                boolean isInScrollingContainer = isInScrollingContainer();

                // 如果在一個可滑動的容器中,那么需要延遲一小會再回應反饋
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    // 利用訊息佇列來延遲檢測一個單擊事件,延遲時間是ViewConfiguration.getTapTimeout()
                    // 這個時間是100ms
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // 沒有在可滑動的容器中,直接回應觸摸反饋
                    // 設定按下狀態為true
                    setPressed(true, x, y);
                    checkForLongClick(
                            ViewConfiguration.getLongPressTimeout(),
                            x,
                            y,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                // 取消事件,恢復所有的狀態
                if (clickable) {
                    setPressed(false);
                }
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                break;

            case MotionEvent.ACTION_MOVE:
                // 通知view和drawable熱點改變
                // 暫時不知道什么意思
                if (clickable) {
                    drawableHotspotChanged(x, y);
                }

                final int motionClassification = event.getClassification();
                final boolean ambiguousGesture =
                        motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
                int touchSlop = mTouchSlop;
                
                // view已經被設定了長按標志且目前的事件標志是模糊標志
                // 系統并不知道用戶的意圖,所以即使滑出了view的范圍,并不會取消長按標志
                // 而是延長越界的誤差范圍和檢查長按的時間
                // 因為這個時候系統并不知道你是想要長按還是要滑動,結果就是兩種行為都沒有回應
                // 由你接下來的行為決定
                if (ambiguousGesture && hasPendingLongPressCallback()) {
                    final float ambiguousMultiplier =
                            ViewConfiguration.getAmbiguousGestureMultiplier();
                    // 判斷此時觸控點的位置是否還在view的范圍內
                    // touchSlop是一個小范圍的誤差,超出view位置slop距離依舊判定為在view范圍內
                    if (!pointInView(x, y, touchSlop)) {
                       // 移除原來的長按標志
                        removeLongPressCallback();
                        // 延長等待時間,這里是原來長按等待的兩倍
                        long delay = (long) (ViewConfiguration.getLongPressTimeout()
                                * ambiguousMultiplier);
                        // 減去已經等待的時間
                        delay -= event.getEventTime() - event.getDownTime();
                        // 添加新的長按標志
                        checkForLongClick(
                                delay,
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    }
                    touchSlop *= ambiguousMultiplier;
                }

                // 判斷此時觸控點的位置是否還在view的范圍內
                // touchSlop是一個小范圍的誤差,超出view位置slop距離依舊判定為在view范圍內
                if (!pointInView(x, y, touchSlop)) {
                    // 如果已經超出范圍,直接移除點擊標志和長按標志,點擊和長按事件均無法回應
                    removeTapCallback();
                    removeLongPressCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        // 取消按下標志
                        setPressed(false);
                    }
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                }

                final boolean deepPress =
                        motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
                // 表示用戶在螢屏上用力按壓,加快長按回應速度
                if (deepPress && hasPendingLongPressCallback()) {
                    // 移除原來的長按標志,直接回應長按事件
                    removeLongPressCallback();
                    checkForLongClick(
                            0 /* 延遲時間為0 */,
                            x,
                            y,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
                }
                break;
        }

        return true;
    } // 對應if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) 

    return false;
}

最后

如果你能看到這里,說明你對于viewGroup和view的事件處理原始碼已經了如指掌了,(高興之余不如給筆者點個贊?(: ~)

最后這里再來總結一下:

  • 觸摸事件,從螢屏產生后,經過系統服務的處理,最侄訓發送到viewRootImpl來進行分發;
  • viewRootImpl會呼叫它所管理的view的 dispatchTouchEvent 方法來分發事件,那么這里就會分為兩種情況:
    1. 如果是view,那么會直接處理事件
    2. 如果是viewGroup,那么會向下派發事件
  • viewGroup會為每個觸控點盡量尋找感興趣的子view,最后再自己處理事件,viewGroup的任務就是把事件分發按照原則精準地分發給他子view,
    • 事件分發中一個非常重要的原則就是:一個觸控點的事件序列,只能給一個view消費,除了特殊情況,如被viewGroup攔截,
    • viewGroup為了踐行這個原則,touchTarget的設計是非常重要的;他將view與觸控點進行系結,讓一個觸控點的事件只會給一個view消費
  • view的 dispatchTouchEvent 主要內容是處理事件,首先會呼叫onTouchListener,如果其沒有處理則會呼叫onTouchEvent方法,
    • onTouchEvent的默認實作中的主要任務就是辨別單擊與長按事件,并回呼onClickListener與onLongClickListener

到此本文的內容就結束了,事件分發的整體流程回顧、學了事件分發有什么作用、高頻面試題相關文章,將會在后續繼續創作,

原創不易,你的點贊是我最大的動力,感謝閱讀 ~

優秀文獻

在學習程序中,以下相關資料給了我非常大的幫助,都是非常優秀的文章:

  • 《深入理解android卷Ⅲ》:學習android系統必備,作者對于android系統的理解非常透徹,可以幫助我們認識到最本質的知識,而不是停留在表層,但對于新手可能會比較難以讀懂,
  • 《Android開發藝術探索》:進階學習android必備,作者講得比較通俗易懂,深度可能相對而言可能較淺,但對新手比較友好,例如筆者,
  • Android 觸摸事件分發機制(三)View觸摸事件分發機制 : 這篇文章采用拆分原始碼的思路來講解原始碼,更好地吸收原始碼中的內容,筆者也是借鑒了他的寫法來創作本文,文中對于原始碼的分析非常到位,值得一看,
  • 安卓自定義View進階-事件分發機制詳解 : 作者言語幽默,通俗易懂,不可多得的好文,
  • Android事件分發機制 詳解攻略,您值得擁有 : 著名博主carson_Ho的文章,特點是干貨滿滿,全文無廢話,只講重要知識點,適合用來復習知識點,
  • Android事件分發機制 : gityuan大佬的博客,對于原始碼的研究都很深入,但對于一些原始碼細節并沒有做過多的解釋,有些地方難以理解,

全文到此,原創不易,覺得有幫助可以點贊收藏評論轉發,
筆者才疏學淺,有任何想法歡迎評論區交流指正,
如需轉載請評論區或私信交流,

另外歡迎光臨筆者的個人博客:傳送門

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/251664.html

標籤:其他

上一篇:【Android】關于連續多次點擊控制元件的控制方案(新建監聽類)

下一篇:Android實作EditView獲取焦點但不彈出軟鍵盤

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more