這篇事件分發機制里的東西你都懂的話,我賠錢!

1.前言

事件分發這個東西嘛,大家一直都在講,但總有人覺得吃不透。為什么呢?因為事件分發是多維的,有好多條思維分岔路口,而文章基本上只能用一維的方式從左到右,從上到下進行表達,所以基本不可能讓普通智力的人從入門到精通。我們所要做的,就是踏踏實實打開源碼,自己多琢磨,多整理。才能徹底理解這些多維的知識點。
下面內容請配合源碼食用!不然基本上索然無味!

2.Touch與Click的前生今世

首先,我們先來做點前戲,搞清楚setOnTouchListenersetOnClickListener以及onTouchEvent之間的關系。

2.1 setOnTouchListener

因為這一系列操作都是針對View的,所以我們直接看其源碼,精準定位到dispatchTouchEvent()方法。

 public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        boolean result = false;
        ...
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

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

這段代碼非常簡單,直接將一切都暴露了出來。

result變量十分關鍵,它是用來控制Touch與Click執行流程的。最開始result為false,如果我們通過setOnTouchListener()為某個View設置了touch監聽,并且在監聽的onTouch()方法中返回true,那么result變量就會被賦值為true,此時dispatchTouchEvent()執行完畢,就不會執行接下來View本身的onTouchEvent()方法。

2.2 onTouchEvent

相反,如果我們沒有為View設置touch監聽,或者設置了touch監聽但是在監聽的onTouch()方法中返回false,那么result依舊為false,就會執行View本身的onTouchEvent()方法。我們來看看onTouchEvent()做了什么,由于只是熱身運動,所以只貼出了與其有關的部分代碼。

public boolean onTouchEvent(MotionEvent event) {
      ...
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                   ...
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }
      ...
    }

可以看到,在View本身的onTouchEvent()方法中,先去判斷了該View是否可以被點擊,接著判斷觸摸事件的類型,如果是ACTION_UP類型,則執行performClick()方法。

2.3 setOnClickListener

performClick()這個方法比較短,直接展示出來。

public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

顯而易見,如果通過setOnClickListener為當前View設置了Click監聽,此時就會去執行監聽中onClick()方法。

2.4 小結

到此為止,前戲就算結束了。我們總結下,setOnTouchListenersetOnClickListener是程序員可以設置的,而onTouchEvent是View本身的方法,在onTouchEvent中會去執行setOnClickListener中設置的OnClick方法。而在View的dispatchTouchEvent()中,首先會去判斷是否設置了OnTouchListener并且其OnTouch方法返回為true,如果是,則不會執行View本身的onTouchEvent方法,如果不是,則會執行onTouchEvent進而執行OnClick方法。

我個人是這樣記住他們的關系的:Touch是觸摸,Click是點擊,從邏輯上來說,觸摸包含了點擊。所以如果設置了觸摸的監聽,那么其必定包含點擊,于是點擊的監聽也就沒什么必要了。

3.事件分發

3.1 事件分發的開始

下面進入正題,在使用安卓手機時,我們用手指觸摸了屏幕,物理設備就會一層層將觸摸事件傳遞出來,這是底層的活兒,我們暫且不去了解。屬于Android開發的故事,從Activity的dispatchTouchEvent()方法開始。請注意,這是Activity的dispatchTouchEvent(),不要和View的dispatchTouchEvent()混淆起來。

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

分析一波,首先判斷觸摸事件,如果是ACTION_DOWN,則調用onUserInteraction(),這是一個空方法,專門用來讓用戶重寫的,可以用于在事件發生前做一些操作。
接著getWindow().superDispatchTouchEvent(ev)就比較重要了。一路跟來的同學肯定知道Activity中的Window就是PhoneWindw,不知道的傳送門在這里。我們直接看其superDispatchTouchEvent方法。

   @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

顯而易見,這里調用了DecorView中的superDispatchTouchEvent方法

  public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }

DecorView是PhoneWindow的內部類,我們去看看他的父類是誰。

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker

可見,FrameLayout 是DecorView的父類,所以就會調用FrameLayout的dispatchTouchEvent方法,遺憾的是,FrameLayout并沒有這個方法,所以還要去找FrameLayout 的父類ViewGroup。ViewGroup中的dispatchTouchEvent是本篇最大的高潮,我們下一節專門來講。在此,我們回到Activity的dispatchTouchEvent方法中

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

看最后一行代碼——年輕的程序員啊,請記住,只要外層ViewGroup的dispatchTouchEvent返回為true,那么就代表事件被消耗了,此時連Activity中的onTouchEvent都不會被執行!

3.2 ViewGroup.dispatchTouchEvent

3.2.1 事件重置

我們從上往下,慢慢分析ViewGroup的dispatchTouchEvent方法。

Android是支持殘障人士使用的,AccessibilityService能夠模擬觸摸事件,而現在我們通常用它來搶紅包,其實現原理就和下面這段代碼息息相關,先挖個坑。

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

下一行代碼對handled賦值為false,這里單獨拿出來就說明這個參數很重要,從名字可以看出這個值代表了事件是否被處理,后面還會多次遇到。

boolean handled = false;

接著判斷事件是否是安全的,如果OJBK,則通過事件掩碼獲取actionMasked,這里提一嘴,MotionEvent.ACTION_MASK可以翻譯成事件掩碼,主要作用是在多點觸摸時分辨觸摸事件。

  if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
            ...

繼續分析,如果事件是ACTION_DOWN,則調用cancelAndClearTouchTargets(ev)resetTouchState()兩兄弟。先看前面一個方法。

 /**
     * Cancels and clears all touch targets.
     */
    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();
            }
        }
    }

顧名思義,cancelAndClearTouchTargets是用來重新初始化TouchTarget的,畢竟Down事件是一次用戶觸摸的開始,所以在開始之前都要把之前的TouchTarget都清除掉。那么TouchTarget又是個啥玩意兒呢?

 /* Describes a touched view and the ids of the pointers that it has captured.
     *
     * This code assumes that pointer ids are always in the range 0..31 such that
     * it can use a bitfield to track which pointer ids are present.
     * As it happens, the lower layers of the input dispatch pipeline also use the
     * same trick so the assumption should be safe here...
     */
    private static final class TouchTarget {
        private static final int MAX_RECYCLED = 32;
        ...
        // The next target in the target list.
        public TouchTarget next;

從注釋上可以看出,TouchTarget形容了觸摸的點。它是一個單向鏈表,最大長度為32,也就是說,Android最多允許32個觸摸點同時進行操作,算一下,起碼2個人把手腳都放在同一個屏幕上才能(先不考慮能不能放得下)把設備弄成傻逼。

resetTouchState()的作用是清除標志位,就不仔細看了。我們稍微總結一下這部分功能,ViewGroup的dispatchTouchEvent會判斷觸摸事件類型,如果當前為DOWN事件,則會將所有狀態都初始化,開始新的一輪事件處理。

3.2.2 事件攔截

下面繼續分析dispatchTouchEvent。結束了事件重置之后,這里定義了一個intercepted變量,顯而易見,這是用來做事件攔截的。

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

在if判斷中,只要當前觸摸事件為DOWN或者存在TouchTarget就會繼續執行intercepted判斷。這里有一個與運算mGroupFlags & FLAG_DISALLOW_INTERCEPT(兩位同時為“1”,結果才為“1”,否則為0)。來想想我們是如何請求父控件不要攔截觸摸事件的?沒錯,就是getParent().requestDisallowInterceptTouchEvent(true)

@Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

當參數disallowIntercept為true時,會執行mGroupFlags |= FLAG_DISALLOW_INTERCEPT運算(參加運算的兩個對象只要有一個為1,其值為1),由于是getParent,所以此時的mGroupFlags 就是ViewGroup中的mGroupFlags ,下面的運算屬于計算機基礎,X|A&A=A,所以最終mGroupFlags的結果就是FLAG_DISALLOW_INTERCEPT。我們看源碼發現FLAG_DISALLOW_INTERCEPT的值為0x80000不等于0,因此如果子View執行了getParent().requestDisallowInterceptTouchEvent(true)這個方法,父View中的intercepted參數就會被賦值為false。

知道了這樣一個流程后,我們再把思維分叉開來,回到之前的代碼中

 if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }

默認情況下,disallowIntercept結果是false,此時就會執行onInterceptTouchEvent(ev)并將其返回值賦值給intercepted,而onInterceptTouchEvent一般是會由程序員來重寫的。

好了,現在你已經搞清楚intercepted這個標志位是怎么被賦值為true或者false的,不過這只是個標志位,并沒有執行什么攔截的操作,接下來我們回到ViewGroup的dispatchTouchEvent方法中,去看看具體的攔截操作是怎么執行的。

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

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

此時又獲取了canceled 標志位,顧名思義用來判斷事件是否被取消,這位兄弟一般都為true,并不是什么重點。重點在于if判斷,這里先討論intercepted為true的情況,此時就不會執行if中的一大段代碼,直接跳到下面的流程中:

            // 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 設置為Null,所以此時會執行dispatchTransformedTouchEvent()方法,這是事件分發中最重要的方法,請注意第三個參數為null:

 // 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就是調用方法時傳入的第三個參數,當child==null時,會執行super.dispatchTouchEvent(transformedEvent)并將返回結果賦值給handled,我們此時是在ViewGroup中,其父類是View,所以我們要去考察View的dispatchTouchEvent方法:

什么?
你居然還在等著看View的dispatchTouchEvent源碼?
文章的第二部分是白看的嗎?
前戲是白做的嗎?
快回去重新讀一遍!

請注意!雖然最后代碼跑到了View中,但這個View是ViewGroup的父類!也就是說最終執行的Touch或Click方法依然是外層ViewGroup中重寫的Touch或Click方法!請區分ViewGroup、View、父View與子View的區別~

我知道有人還是懵逼的,我們總結下。導致intercepted為true的原因有兩個,一是父View重寫了onInterceptTouchEvent方法并返回true,二是子View沒有請求getParent().requestDisallowInterceptTouchEvent(true)方法。而當intercepted為true時,父View就會執行攔截操作,在源碼中的表現就是dispatchTransformedTouchEvent()的第三個參數為null,從而執行super.dispatchTouchEvent(transformedEvent)方法,這個方法最終會調用在父View中重寫的Touch或Click方法。

別放松,還沒完呢。看View中的這段代碼:

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

如果父View中onTouchEvent返回false,那么result的結果就是false(結合文章第二段看更加清晰喲)。此時再回到Activity的dispatchTouchEvent方法中:

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

由于result為false,所以getWindow().superDispatchTouchEvent(ev)也為false,此時就仍然會執行Activity的onTouchEvent(ev)方法!因此,縱使父View攔截了事件,只要他的onTouchEvent返回false,Activity中的onTouchEvent(ev)方法依舊會得到執行!

OK,到此為止事件攔截就算講完了。道友們且好好消化,下面繼續發車!

3.2.3 事件分發

在前面事件攔截的分析中,我們假設intercepted為true,所以就會跳過下面代碼中的if判斷

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

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

而跳過的一大段代碼恰恰是實現事件分發的代碼。默認情況下,intercepted都為false,因此if判斷中的代碼基本都會執行,現在讓我們一起來看看事件分發是如何實現的。

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

在事件分發的開始,又出現了Accessibility相關的字段,這是android可以實現自動化測試的原因之一,這里先加深一波印象。

下面還是條件判斷,由于此時仍然是DOWN事件,自然而然就進去了。

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    ...省略幾行代碼...
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        ...省略下面代碼...

接著判斷當前控件是否含有子控件,如果包含子View,則通過buildTouchDispatchChildList()對所有子View進行重排序,這個方法也是挺有意思的,它會回調buildOrderedChildList()

  ArrayList<View> buildOrderedChildList() {
        ...省略...
        for (int i = 0; i < childrenCount; i++) {
            // add next child (in child order) to end of list
            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
            final View nextChild = mChildren[childIndex];
            final float currentZ = nextChild.getZ();

            // insert ahead of any Views with greater Z
            int insertIndex = i;
            while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
                insertIndex--;
            }
            mPreSortedChildren.add(insertIndex, nextChild);
        }
        return mPreSortedChildren;
    }

為什么要重排序呢?因為View是一層層添加到Window上的,在事件分發的時候,如果某一觸摸點下面有多層子View,自然應該是最外層的子View先接收到事件。遺憾的是,在View的添加過程中,并不是先添加到父View中的子View就一定在最外層,因此我們就有必要通過每個子View的Z軸數值對他們進行重排序。

重排序之后,就按照排好的順序一個個拿到子View,并通過if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null))這句代碼來判斷子View是否可以接收當前的觸摸事件。

                 for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            ...省略Accessibility相關代碼...
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

先看前一個方法,顯而易見接收觸摸事件有兩個條件,一是可見,二是不在執行動畫。

private static boolean canViewReceivePointerEvents(@NonNull View child) {
        return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null;
    }

接著看下一個方法,關在在于transformPointToViewLocal會加上偏移值,而child.pointInView(point[0], point[1])會判斷該child是否可以接收到(x,y)點的觸摸事件

 protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }

綜合上述兩處,我們總結View能夠接收觸摸事件的條件一共有四個:

1.可見
2.不在執行動畫
3.可點擊
4.觸摸點在View內

在之前的分析中,如果遍歷到的子View不能接收事件,就直接continue,反之則執行下面的代碼:

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

getTouchTarget()也是重點方法:

  private TouchTarget getTouchTarget(@NonNull View child) {
        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            if (target.child == child) {
                return target;
            }
        }
        return null;
    }

當觸摸事件為DOWN時,mFirstTouchTarget會被重置為null,因此此時getTouchTarget返回null,什么都沒有發生,代碼繼續向下執行。那么為什么又說這是重點方法呢,因為當觸摸事件為MOVE時,mFirstTouchTarget不為null,此時就會直接break出當前循環。我們知道MOVE是十分頻繁的調用,所以這里相當于是做了一層性能優化。具體是怎么優化的,我們在下個篇章還會介紹。

拓展完畢回到主線上,代碼再次進入條件判斷

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

dispatchTransformedTouchEvent()在之前出現過,當父View攔截事件時,該方法被調用,第三個參數為null,并最終調用了父View的Touch或Click方法。而此時,第三個參數不再為null,取而代之的是可以接收觸摸事件的子View,我們重新來看dispatchTransformedTouchEvent()中最關鍵的代碼:

                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;

當child不為空時,先進行觸摸點的偏移計算,接著執行handled = child.dispatchTouchEvent(event)。如果child是ViewGroup,就相當于重新執行上面的一大波步驟;如果child是View,則類似于父View攔截事件的過程,會執行View本身的Touch或Click方法。

3.2.4 事件回調

好了好了,事件分發下去的過程終于梳理完了,我們接著看分發結果的回調。

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

如果子View的dispatchTouchEvent()返回true,就會進入判斷體并執行最重要的一行代碼newTouchTarget = addTouchTarget(child, idBitsToAssign):

 private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

之前說過TouchTarget表示觸摸目標,其本質是一個單向鏈表。顯而易見,addTouchTarget()的作用就是為mFirstTouchTarget 賦值,初始化這個鏈表。

現在mFirstTouchTarget 不為Null了,我們來看最后一段源碼:

           // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                       ...
                }
            }

當子View返回true時,子View就消耗了這個事件,執行else中的代碼,handled被賦值為true;而當子View不消耗事件返回false時,執行if中的代碼,dispatchTransformedTouchEvent一共出現了3次,大家應該很熟悉了,當第三個參數為null時,會調用父View的Touch或Click方法,就這樣一層一層的回調上去,整個過程是一個很完美的遞歸。

3.3 MOVE事件

在前面的文章中,我們基本是以DOWN事件為例進行分析的,如果你熟練理解了上面所講的內容,那么請換上這輛快車,繼續來看看MOVE事件是怎樣的玩法。

3.3.1 MOVE事件攔截

MOVE事件也能夠被攔截的原因就在于這個if判斷。

 if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } 

由于DOWN事件會為mFirstTouchTarget賦值,因此MOVE時mFirstTouchTarget!=null,該攔截的繼續攔截。

3.3.2 MOVE事件分發

現在回憶一下DOWN事件分發的那一大堆步驟,什么子View重排序啊、遍歷啊、判斷能否接收觸摸事件啊等等等等,其過程十分復雜,因為DOWN是點一下就完事了,所以可以這么整,而MOVE的調用非常頻繁,要也這樣操作,用戶界面絕對會被卡死。

所以我們才會用到TouchTarget,觸發Down事件后,TouchTarget被賦值,其目標就是可以接收觸摸事件的子View,因此在MOVE事件中,我們可以直接跳過前面的一大段代碼,直接從mFirstTouchTarget獲取需要的子View:

// Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

這段代碼不久前出現過,只是我省略了else中的部分代碼,因為之前在說DOWN事件,與MOVE無關。現在我們來看完整的流程,當mFirstTouchTarget不為Null時,最關鍵的是這段:

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

MOVE事件就是在這里進行分發與回調的!

4.總結

如果讀到最后,你有一種什么都聯系起來了,豁然開朗的感覺,那就點個贊唄!如果讀完心想這文章寫的什么[嗶]東西,請務必擺上一份源碼再讀一次!

而如果你真的什么都懂,底下留言!我賠錢!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377

推薦閱讀更多精彩內容