Android面試複習之View事件體系(源碼分析)

前言

昨天面試了騰訊Android,基本上是照着簡歷問,但都問的比較深刻。其中問到了事件體系,含含糊糊的答了出來(以前有看過藝術探索),但後來本身想一想感受本身答的並非特別好。雖然面試結果還不知道,但以爲仍是應該好好整理一下。面試

分析的起點

不論是書上仍是網上都說事件的起點是ViewGroup的dispatchEvent,但大多數都沒有給出理由,本着探索的精神,我採用了最簡單的方法:斷點調試。 bash

image.png
點擊這個View,果真,查看棧幀:
image.png
是經過WindowCallback傳遞到Activity,再專遞到Activity的Window->DecorView,DecorView實際上就是一個FrameLayout,最終調用的就是ViewGroup的dispatchTouchEvent,因此下面就能夠愉快的分析DispatchEvent啦。

ViewGroup#dispatchTouchEvent()

// 前面省略...
            final int action = ev.getAction();
            // 獲取事件類型
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // ACTION_DOWN就是你手機接觸屏幕的事件,一般被認爲是一系列觸摸事件的起點
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // 這裏是重置當前的事件狀態,後面會分析
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // 檢查是否攔截
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                // 這個標誌若是有效,則不會調用本身的onInterceptTouchEvent方法
                // 能夠經過ViewParent#requestDisallowInterceptTouchEvent()修改
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    // 若是intercepted爲true,就會攔截這一系列事件,具體能夠在後面的源碼到
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); 
                } else {
                    intercepted = false;
                }
            } else {
                // 觸摸事件不是ACTION_DOWN,而且touchTarget==null
                intercepted = true;
            }
複製代碼

能夠看到,是否攔截的邏輯還與touchTarget這個成員相關,這個成員是什麼呢?ui

private static final class TouchTarget {
        private static final int MAX_RECYCLED = 32;
        private static final Object sRecycleLock = new Object[0];
        private static TouchTarget sRecycleBin;
        private static int sRecycledCount;

        public static final int ALL_POINTER_IDS = -1; // all ones

        // The touched child view.
        public View child;

        // The combined bit mask of pointer ids for all pointers captured by the target.
        public int pointerIdBits;

        // The next target in the target list.
        public TouchTarget next;
複製代碼

看一下這個類的結構,很容易想到,這是個鏈表節點的結構,而它的child是什麼呢?能夠經過後面的代碼去挖掘,由於mFirstTouchTarget這個成員變量是在後面賦值的,初始爲null,因此咱們能夠把它認爲是null,帶着這個條件去走下面的邏輯。 按照ViewGroup的默認狀況,不攔截事件,這個先看intercept爲false的狀況。如下是不攔截本次事件的時候會執行的一段代碼。this

// 首先會判斷是否是ACTION_DOWN或者支持多指時是否是其餘手機按下或者是鼠標按下(Hover是跟鼠標相關的處理,這裏不用過多關心)
if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    // 獲取按下的手指編號,暫時不用關心
                    final int actionIndex = ev.getActionIndex(); 
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // 獲取子View的列表,順序能夠經過ViewGroup提供的接口自定義
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        // 按必定順序遍歷子View
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            ...
                            // 判斷這個View是否接收事件,並判斷事件是否在View對應的那塊矩形內,若是不在,找下一個
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            
                            // 這個方法其實是遍歷mFirstTouchTarget這個鏈表,找到child域和當前View相同的TouchTarget,但第一次收到down時,這個會返回null
                            newTouchTarget = getTouchTarget(child);
                            // 若是找到了,會把touchTarget響應的手指編號信息更新
                            if (newTouchTarget != null) 
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }
                            resetCancelNextUpFlag(child);
                            // 分發事件,若是成功處理,更新事件處理的信息並退出循環,這裏是把事件交給child去分發,具體如何實現這裏不展開,邏輯比較簡單
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    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;
                            }

                           
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
                    ...
複製代碼

這裏的代碼就比較長了,但也不是很難懂,重要的地方都在註釋。咱們這裏暫時是以第一次點擊事件來描述這個流程的,所以去掉了一些與這個流程無關的代碼。這段代碼實際上對一些特殊狀況進行了處理,這裏我們先略過。後面雖然還有不少代碼,但實際上會發現,執行到這個地方,基本就結束了,alreadyDispatchedToNewTouchTarget被置爲了true,帶入源碼讀,能夠看到spa

TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        ...
                    }
                    predecessor = target;
                    target = next;
                }
            }
複製代碼

對於這個流程來說,else分支已經不重要了,到此DOWN事件處理完畢。 固然,咱們如今能夠回過頭看前面的問題。DOWN事件分發到的時候到底作了什麼呢? 首先是cancelAndClearTouchTargets方法調試

private void cancelAndClearTouchTargets(MotionEvent event) {
        if (mFirstTouchTarget != null) {
            boolean syntheticEvent = false;
            if (event == null) {
                final long now = SystemClock.uptimeMillis();
                event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                syntheticEvent = true;
            }

            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            clearTouchTargets();

            if (syntheticEvent) {
                event.recycle();
            }
        }
    }
複製代碼

ViewGroup#clearTouchTargetscode

// 清空鏈表
private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }
複製代碼

能夠看到,在這裏會分發event,可是即便event不爲null,傳給dispatchTransformedTouchEvent的cancel的值爲true,在這個方法處理的時候,會把event的Action設爲ACTION_CANCEL,因此咱們在處理ACTION_CANCEL的時候,通常要把事件相關的狀態和變量重置。 接下來會調用ViewGroup#resetTouchStateorm

private void resetTouchState() {
        // 清空鏈表
        clearTouchTargets();
        // 重置狀態
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }
複製代碼

這裏咱們能看到,它會把FLAG_DISALLOW_INTERCEPT這個標誌設置爲false,也就是說,它這個時候會調用本身的interceptTouchEvent方法。由此咱們得出一條結論: ACTION_DOWN事件不能被取消攔截 假設咱們按下來,移動手指,這樣就會產生一個move事件,這裏,仍然假設默認不會攔截。cdn

if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) 
...
複製代碼

這一串代碼天然不會執行,到了下面blog

if (mFirstTouchTarget == null) {  
               // 暫不關心
            } else {
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    // alreadyDispatchedToNewTouchTarget這個時候是false,會執行else分支
                    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;
                }
            }
複製代碼

這個時候,會遍歷touchTarget這個鏈表並分發事件,從源碼中能夠看出,只要又一個touchTarget的child成功處理這個事件,handled就是true。 這裏又有疑問了,爲何touchTarget會用鏈表來存?會有多個touchTarget的狀況嗎?這個時候,就要想到以前分析忽略的地方,對多指的支持。首先仍是看上面那一長串代碼的進入條件:

if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE)
...
複製代碼

還有一個ACTION_POINTER_DOWN條件。什麼是POINTER_DOWN呢?首先DOWN是指你第一個手指觸摸屏幕,而後你第一根手指不放,按下第二根手指、第三根手指都會產生這樣的事件,而且,還會記錄手指的id。 能夠看到上面的split這個變量,這是一個flag,當關心多指時爲true(默認true)。接下來,獲取本次pointerId:

final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;
複製代碼

idBitsToAssign實際上就是把手指id那一位置1的數。 接下來,會把以前處理過這個手指id的touchTarget清除。

private void removePointersFromTouchTargets(int pointerIdBits) {
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            // 處理了這個手指的事件
            if ((target.pointerIdBits & pointerIdBits) != 0) {
                // 把這個手指對應的位置0
                target.pointerIdBits &= ~pointerIdBits;
                // 置0後沒有對應處理的手指id了,則從鏈表中刪除
                if (target.pointerIdBits == 0) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                    } else {
                        predecessor.next = next;
                    }
                    target.recycle();
                    target = next;
                    continue;
                }
            }
            predecessor = target;
            target = next;
        }
    }
複製代碼

也不難理解爲何須要清除前面的,這個方法是爲了同步狀態,前面的手指,由於對於當前手指來講,至關於新開始一個DOWN事件,因此前面不該該有處理這個事件的touchTarget,這樣作也是爲了保險,可見Google大佬思惟的嚴密。接下來的就有三種狀況了:

  • 狀況一:
newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }
複製代碼

在遍歷的時候,首先遍歷了已經在touchTarget中的child,這個時候顯然沒有增長新的touchTarget,而是把它的處理的手指對應位置一。而以後的流程如前面分析,遍歷touchTarget,分發事件。

  • 狀況二: 先遇到了一個沒有在鏈表中的結點,就會像前面處理DOWN事件那樣添加到鏈表中。以後的處理也相似。
  • 狀況三:
if (newTouchTarget == null && mFirstTouchTarget != null) {
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
複製代碼

遍歷完子View都沒有找到,這時候把鏈表最後一個(最近添加的)手指信息對應位置1。 事實上,我的認爲比較常見的是狀況一。狀況2、狀況三的話須要改變遍歷順序或者移除上一次處理過的View。 上面是intercept=false的狀況。那若是intercept=true呢?這個就比較簡單了。

if (mFirstTouchTarget == null) {
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            }
複製代碼

會把dispatchTransformedTouchEvent的child參數設置爲null,若是爲null,會把事件交給super.dispatchTouchEvent。super是誰?可別忘了ViewGroup的爸爸是View!View又有本身的dispatchTouchEvent方法,這個方法就相對來說比較簡單了,主要是touchEventListener、click、longClick等的處理。

結語

固然,這個方法裏還有一些細節的處理我沒有分析,好比上面那段代碼的canceled變量、accessibilityFocus的處理等。這裏先埋個坑,以後有空回來補。 若是有什麼分析錯誤的地方,歡迎各位大神指正!

相關文章
相關標籤/搜索