淺析安卓事件分發機制源碼

更多關於安卓源碼分析文章,請看:安卓源碼分析

最近工作需要需要做一些比較複雜的自定義View,其中事件分發的處理自然少不了,結合之前閱讀過的大量資料,工作是完成了,但是對事件分發的處理總覺得很不清晰,知其然不知其所以然的感覺讓人很不舒服。如果不知道事件分發原理,要是處理的情況很複雜的話,那就很難解決了。之前也看過任玉剛的《安卓開發藝術探索》對於事件分發源碼的分析,但只能說大致瞭解了事件分發的流程,而不知其中的道理。

索性踐行某位大師的名言——

“read the fucking source”

源碼的閱讀總是讓人痛並快樂着,一是因爲源碼很長邏輯很複雜,二是因爲源碼要考慮的東西太多,所以干擾的東西實在太多,經常跟進去就迷路。

介於本文是對源碼層次的分析,所以如果大家還沒了解過事件分發的基本流程的話,最好先看一下這方面的資料。

首先,關於事件分發,相信接觸過的人都看過類似這樣一張U型圖片:

http://www.jianshu.com/p/e99b5e8bd67b

並且也知道dispatchTouchEvent、InterceptTouchEvent、onTouchEvent這三個方法的作用分別分發事件、攔截事件、消費事件。

也看過類似的僞代碼:

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean result = false;             // 默認狀態爲沒有消費過

    if (!onInterceptTouchEvent(ev)) {   // 如果沒有攔截交給子View
        result = child.dispatchTouchEvent(ev);
    }

    if (!result) {                      
    // 如果事件沒有被消費,詢問自身onTouchEvent
        result = onTouchEvent(ev);
    }

    return result;
}

其實如果熟悉這些,簡單的事件分發已經可以處理了,但是這些只是單純記憶流程,並不知道具體的事件在源碼中何去何從。

總的來說,ViewGroup和View屬於樹的結構,事件分發就是從父節點到子節點一步步遍歷的過程,直到找到可以消費事件的View爲止。而在這一過程中,就是一個不斷遞歸調用以上僞代碼的過程。子View總是在經過dispatchTouchEvent的執行後將返回值交給父View,父View根據子View是否消費了事件再確定自己是否需要消費事件,然後再向自己的父View返回一個表示自己或者自己的子View是否消費了事件的布爾值。如果當前View(ViewGroup)自己或者子View不消費就會將事件轉給父View的onTouchEvent方法,所以就會呈現了上面的U型圖。分析了源碼之後,更能體會這其中設計的精妙

關於事件分發源碼我認爲最好還是模擬一個事件流,跟着代碼走,努力避開各種其他代碼的干擾(例如安全檢查等),並且從最簡單的事件入手,即單指觸模、控件不進行滑動、先不考慮ACTION_CANCEL事件。

本文基於Android23的源碼進行分析。請對照View和ViewGroup的源碼來看本文

首先要明確一個事件序列指的是從手指按下、滑動、擡起的這一個過程中的多個事件。分別是DOWN、MOVE、UP事件。

在這裏,假設一個情景,有一個Activity,裏面的佈局是最外層一個ViewGroup A(充滿屏幕),A裏面有個ViewGroup B(充滿A),B裏面有個Button C(比如在屏幕中間)。

情形1:
A完全(即A上的所有點都要攔截)攔截事件(即InterceptTouchEvent直接返回true),現在單指點擊了一下A範圍任意點,然後滑動並擡起。
(Activity、PhoneWindow 、DecorView的分發就不進行分析了)

現在的事件爲ACTION_DOWN。
首先DecorView調用了ViewGroup A的dispatchTouchEvent方法(當然這裏只挑關鍵代碼),看到ViewGroup的2103行:

// Check for interception.
            final boolean intercepted;
          
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                    //先判斷是否允許該ViewGroup攔截事件
                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;
            }

這一段很簡單,但是很重要。關鍵在 intercepted = onInterceptTouchEvent(ev);,大家也很熟悉,此時A要攔截事件的,所以intercepted 爲true。

於是,2134行的:

if (!canceled && !intercepted)

裏面的語句就不會被執行(裏面的語句主要是遍歷子View找到消費這一系列事件的子View)

然後來到2239行:

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

mFirstTouchTarget就是找到的那個要消費事件的子View,此時因爲根本沒有遍歷過子View去尋找,所以爲null,所以調用:

handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);

這裏dispatchTransformedTouchEvent很重要,還要注意第三個參數傳了null。

進入dispatchTransformedTouchEvent方法,先看下注釋:

Transforms a motion event into the coordinate space of a particular child view,filters out irrelevant pointer ids, and overrides its action if necessary.
If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.

最後一句話說的很明白了,如果child(即第三個參數)爲null,則該事件會傳給當前的ViewGroup,即A。

對應的代碼在dispatchTransformedTouchEvent方法中,處於ViewGroup的2567行:

 // Perform any necessary transformations and dispatch.
        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);
        }

是的,現在調用了

handled = super.dispatchTouchEvent(transformedEvent);

其實就是View的dispatchTouchEvent方法。

在View的dispatchTouchEvent中,關鍵看View的9285行:

if (onFilterTouchEventForSecurity(event)) {
            //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;
            }
        }

可以看到如果View已經設置了onTouchListener並且返回true,則onTouchEvent並不會被執行,並且整個dispatchTouchEvent方法最後也是返回這個result。

View默認onTouchListener爲null,所以onTouchEvent會被執行。

onTouchEvent中默認主要是對OnClickListener和OnLongClickListener等事件的處理。看源碼View的10288行:

if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) 

這裏的判斷語句如果條件不成立,即假如View不是Clickable(默認狀態)的話,onTouchEvent則返回false,當然也不執行各種點擊事件。

此時如果A是默認狀態,則A的dispatchTouchEvent返回false給DecorView,它的意義是:A本身和子View都不消耗該事件。因爲DecorView只有一個子View A,如果A不消費事件,那麼在這一系列事件的後續事件,及MOVE、UP,就不會分發給A了。

如果A是Clickable的(比如被set了OnClickListenrer),則onTouchEvent返回true,那麼返回true給DecorView,DecorView就會將後續的事件都傳給它處理。而在ACTION_UP事件傳遞過來的時候,onTouchEvent就會觸發OnClickListenrer等的點擊事件。

至於爲什麼,且聽後面分解。
在這裏,可以知道,在ViewGroup攔截事件的情況下,會通過dispatchTransformedTouchEvent去調用自己的super.dispatchTouchEvent方法最後調用onTouchEvent方法,也就是把自己當做一個View處理事件。

dispatchTransformedTouchEvent很關鍵,具體下面會說明~~

情形2:
A不攔截事件,B攔截事件,單指點擊屏幕任意點,然後滑動並擡起。

同樣,首先DecorView調用了ViewGroup A的dispatchTouchEvent方法,這時候intercepted已經是false了,所以到了ViewGroup的2134行的判斷

 if (!canceled && !intercepted) 

就需要進入了(此時的cancel爲false,一般情況下都爲false)。

此時來到ViewGroup的2144行:

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

目前只看第一個判斷actionMasked == MotionEvent.ACTION_DOWN,其實這個判斷語句內部是用於找出能夠消費Down事件的子View,這個對於此次系列的事件意義重大,我們進入判斷語句看。

首先看從ViewGroup的2155行開始的這一段:

 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.
                        
                        //從頂到底的子View集合
                        final ArrayList<View> preorderedList = buildOrderedChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder
                                    ? getChildDrawingOrder(childrenCount, i) : i;
                            final View child = (preorderedList == null)
                                    ? children[childIndex] : preorderedList.get(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);
                                //如果View不包含觸摸點則繼續遍歷
                                continue;
                            }

很明顯,ViewGroup在遍歷子View,buildOrderedChildList方法這裏是創建了一個從頂到底的子View集合去遍歷,所以越頂部的View越優先可以消費事件。最後主要是使用isTransformedTouchPointInView方法判斷觸摸點是否在子View上。

B是A的子View,並且觸摸點在B上,所以不會執行continue,即繼續執行後面的代碼。
來到ViewGroup的2199行:

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

又見到了isTransformedTouchPointInView方法,
進入該方法,跳過關於ACTION_CANCEL以及多指觸控的代碼,主要就是一下代碼:

// Perform any necessary transformations and dispatch.
        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);
        }

這裏主要是分child是否爲null兩種情況,在這裏先不討論null的情況,這裏的child爲通過遍歷且確定觸摸點在其中的View。

當child不爲null的時候,先做一些滑動偏移量處理,然後調用child的dispatchTouchEvent方法。

是的,這裏就是真正實現事件分發的地方,開始將事件傳遞到子View的dispatchTouchEvent方法。在已經確定觸摸點在View上的情況下,調用dispatchTransformedTouchEvent方法的作用是通過自View的dispatchTouchEvent的返回值來判斷該View是否要消費這個事件,true則爲消費,false則不消費。

現在子View爲ViewGroup B,B是攔截事件的,所以interceptTouchEvent返回true,所有B也會像情形1中的A一樣執行View的dispatchTouchEvent方法,再調用onTouchEvent方法。默認情況onTouchEvent返回false,所以不會走入前面A的

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

裏面的代碼,而是會走到ViewGroup 2239行的代碼:

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

是的,這裏的mFirstTouchTarget 是在

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

判斷語句內纔會被賦值,使得它持有消費了事件的View。
如果B的onTouchEvent返回true,即B消費事件的情況下才會被賦值,它本身爲一個ViewGroup內部類TouchTarget的鏈表,之所以爲鏈表主要是爲了多指觸摸情況,這裏我們暫且認爲它代表的是能夠消費該事件的View就可以。

走到這裏是已經遍歷完所有A的子View ,如果遍歷完發現沒有子View消費事件(mFirstTouchTarget == null),則調用:

handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);

注意到此時child參數傳null,回看dispatchTransformedTouchEvent的代碼就知道會調用super.dispatchTouchEvent,源碼中有關於此的註釋:

前面已經說過了,如果沒有子View消費,當前ViewGroup就自己調用dispatchTouchEvent嘗試去消費。

如果B可以消費事件呢(B的onTouchEvent返回true)?那麼在A遍歷到B的時候,A的判斷語句中的dispatchTransformedTouchEvent就會返回true(當此時B已經執行完onTouchEvent方法),那麼就會執行前面列出來的,ViewGroup的2201行開始的代碼。

重點是ViewGroup的2215行:

 newTouchTarget = addTouchTarget(child, idBitsToAssign);

看下addTouchTarget方法:

  /**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     */
    private TouchTarget addTouchTarget(View child, int pointerIdBits) {
        TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

其實就是在以mFirstTouchTarget爲頭的鏈表插入一個新的頭節點。不考慮多指觸摸情況,就是類似賦值child給mFirstTouchTarget持有的意思。

這裏很關鍵,mFirstTouchTarget 被賦值了,保存的是A中需要消費事件的子View,然後在dispatchTouchEvent剩下的代碼中,會在mFirstTouchTarget 不爲null的情況下,返回true,向DecorView報告A的子View或自己有View消費事件。

mFirstTouchTarget 有什麼意義呢?記得事件分發中有一條規則:
一旦一個View消費了DOWN事件,那麼該系列的後續事件都由該View處理。

現在DOWN事件結束了,來了MOVE事件。
同樣的A的dispatchTouchEvent方法,又來到ViewGroup 2144行的:

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

判斷語句,這次是ACTION_MOVE事件,當然無法進入語句內部,於是乎A遍歷子View找出能夠消費事件的View都沒有執行,直接跳到前面提到的ViewGroup的2240行的代碼,然後進入2244行的else語句,其中關鍵是2251行代碼:

if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }

alreadyDispatchedToNewTouchTarget 表示是否是新添加TouchTarget的,這個在上一個事件DOWN的時候是被置爲true,但是在這次事件中由於沒有添加新的TouchTarget,所以爲false。

所以會走到2256行:

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

這裏的target.child就是mFirstTouchTarget中持有的View,在這裏就是ViewGroup B。所以通過dispatchTransformedTouchEvent我們知道這裏將當前事件ACTION_MOVE傳給了B的dispatchTouchEvent方法。

總的來說就是通過mFirstTouchTarget保存DOWN事件的消費View B,然後在後續的事件直接傳給了B的dispatchTouchEvent處理。

此時如果B要消費這個MOVE事件,則handle賦值爲true,則A的dispatchTouchEvent返回handle爲true。如果B不要消費這個MOVE事件,那麼A的dispatchTouchEvent返回false給DecorView。

情形3:
A不攔截事件,B也不攔截事件,單指點擊Button,然後滑動並擡起。

其實和情形2很相似了。

首先是Down事件,DecorView調用A的dispatchTouchEvent方法,A因爲onInterceptTouchEvent返回false,遍歷點擊到的子View找到B,調用了B的dispatchTouchEvent方法,B因爲onInterceptTouchEvent返回false,遍歷子View找到Button C,Button因爲本身爲Clickable,所以dispatchTouchEvent方法返回true給B,所以B將C記錄在B的mFirstTouchTarget中,然後B的dispatchTouchEvent返回true給A,告訴它“我這邊可以處理這個事件”,然後A將B記錄在A的mFirstTouchTarget中,A的dispatchTouchEvent方法返回true給DecorView。

於是等到MOVE事件下來,A直接找A的mFirstTouchTarget持有的View,即B,B直接找B的mFirstTouchTarget持有的View C,如果C要消費這個事件那還是一路往上返回true。

那如果C這時候不要消費事件呢?那麼C的dispatchTouchEvent返回false,B的dispatchTouchEvent返回false,且還不能調用自己的super.dispatchTouchEvent處理,所以又將false返回給A,A也和B一樣,所以因爲遞歸最終這個事件交給了Activity處理。

這就是事件分發規則中:
“如果View不消耗DOWN以外的其他事件,則父View不會調用onTouchEvent處理這個事件,同時該View仍然可以繼續接收到後續的事件,這些View不處理的事件都交給Activtiy處理”。

那如果View不消耗DOWN事件呢?其實前面情形1已經簡單說過了,這裏結合A,B,C以及後面兩個情形一起來說會更加直觀。就是C如果不接受DOWN事件,那麼B的onTouchEvent方法會處理事件(不考慮onTouchListener情況),如果B不可以消費DOWN事件,則調用A的onTouchEvent方法處理。所以出現了U型圖那樣將事件往上拋的情形。
如果B可以消費DOWN事件,那麼C的
mFirstTouchTarget就記錄爲B,此時B的mFirstTouchTarget爲null,所以後續事件來到C後,C直接交給了B,B因爲事件不是ACTION_DOWN了,所以不會遍歷子View,直接判斷mFirstTouchTarget是否爲null。由於沒有遍歷子View,所以mFirstTouchTarget仍然爲null,所以B會調用自己的super.dispatchTouchEvent處理,以之前分析的類推,後續事件不會被C接收(這部分可以重看下ViewGroup的2144行的判斷語句)。

這就對應了另一個事件分發的規律:
一旦一個View在onTouchEvent中不消耗事件,則後續的事件都不會交給他來處理。(看源碼發現如果DOWN觸摸點在多個View中,應該說是這幾個View都不消耗事件爲前提?)

最後,當一個系列事件結束之後,新的系列事件來到的時候,會將上一次的 保存的狀態清空(比如mFirstTouchTarget),看ViewGroup的dispatchTouchEvent在ViewGroup中的2094行:

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

關於事件分發機制就先簡單說到這裏,還有很多東西沒有提及,比如多指觸摸、CANCEL事件等。事件分發由於涉及遞歸,有時候一層層進入又一層層出來很容易讓人迷路。我也是對源碼研究不深入,難免有疏漏或者錯誤的地方,望各位指正~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章