天天看點

從源碼角度解析Android事件分發機制

事件分發是Android中的一個重點也是一個難點,在自定義控件中很是常用。前後看了好多書和部落格,感覺寫的東西順序都稍微有些不對,讓剛接觸的人看起來不是很好懂。在這裡也是将我從不清楚到熟悉的過程寫下來,希望對大家有所幫助,對自己也起到總結的作用。

下面介紹幾個方法,隻要先有個印象就好,以後會慢慢解釋:

首先是

dispatchTouchEvent(MotionEvent ev)

分發事件,傳回true表示事件被目前的View或其子View消耗,傳回false表示事件沒被消耗。

onInterceptTouchEvent(MotionEvent ev)

是否攔截某個事件,傳回true表示攔截,傳回false表示不攔截

onTouchEvent(MotionEvent ev)

處理點選事件,傳回true表示事件被目前的ViewGroup/View消耗掉,傳回false表示沒消耗

任玉剛大神書中的一段代碼對這個邏輯闡述的非常清楚,下面是這段僞代碼:

public boolean dispatchTouchEvent(MotionEvent ev){
		boolean consume = false;
		if(onInterceptTouchEvent(ev)){
			consume = onTouchEvent(ev);
		}else{
			consume = child.dispatchTouchEvent(ev);
		}
		return consume;
}
           

先簡單的了解上面的代碼,在源碼的分析中将會對代碼進行進一步分析,而後給出結論。

源代碼分析:

當一個點選事件發生時,最先接收到事件的是Activity的dispatchTouchEvent方法,下面是相應源碼:

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

如果動作是ACTION_DOWN的話,會回調onUserIteraction方法,這個和我們關系不大。

我們先暫且認為getWindow().superDispatchTouchEvent(ev)是調用子View分發事件的方法,當子View沒有消耗掉這個事件(也就是子View的dispatchTouchEvent傳回false)時,Activity就會調用自身的onTouchEvent方法來處理這個事件。

進入getWindow().superDispatchTouchEvent(ev)發現這是一個抽象的方法,我們知道Window的實作類是PhoneWindow,檢視PhoneWindow的相應方法可以看到其調用了DecorView的superDispatchTouchEvent,在以前的學習中我們知道在setContentView中我們設定的View就是DecorView的子View,是以點選事件一定能傳遞到子View,而一般的子View就是ViewGroup。根據本文開始的分析,進入時應該調用的是dispatchTouchEvent方法,我們現在來看一下ViewGroup中dispatchTouchEvent方法的源碼:

if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
           

在方法開始的時候有上述的代碼片段,注釋上寫的很明确,要清除之前的所有狀态,具體要清除的是什麼狀态呢?先接着看代碼:

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

出現了一個mFirstTouchTarget變量,它代表的是什麼意思呢?在後文中會有對mFirstTouchTarget的說明,但是這裡為了解釋清楚,不妨先透露一下,如果目前View将事件分發到子View,那麼mFirstTouchTarget将被指派。

還有一個符号位FLAG_DISALLOW_INTERCEPT,看名字也應該可以猜出來,它的作用是不要攔截目前的事件,那和這個符号位進行與操作的mGroupFlags又是誰設定的?不難猜出,應該是子View設定的為了讓目前View不去攔截事件。

現在重新審視我們剛才寫過的内容,在進入dispatchTouchEvent方法時清除狀态所清除的内容也就很好了解了:

private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
           
private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }
           

很清楚,将mGroupFlags的狀态清除掉了,同時将mFirstTouchTarget置為null。

現在我們再來看給intercepted指派的代碼塊,不難得出下面這幾個結論:

1.每次ACTION_DOWN發生的時候,都會回調onInterceptEvent方法(因為每次ACTION_DOWN發生的時候狀态都會清除)。

2.一旦目前View将ACTION_DOWN事件攔截後,mFirstTouchTarget仍然為空,那麼在一個事件序列(一個ACTION_DOWN,中間任意多個ACTION_MOVE,一個ACTION_UP)内不會再調用onInterceptTouch方法進行判斷,intercepted變量始終為true,也就是所事件一直被目前View所攔截。

3.若ACTION_DOWN不攔截,那麼在一個事件序列内不管攔截了什麼動作,在下一個事件到來時都要回調onInterceptTouchEvent方法。

4.子View可以通過設定mGroupFLags值的方式來控制目前View攔截事件,但是ACTION_DOWN事件不被控制。

if (!canceled && !intercepted) {
                    final int childrenCount = mChildrenCount;
                    if (childrenCount != 0) {
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final View[] children = mChildren;
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);

                        final boolean customOrder = isChildrenDrawingOrderEnabled();
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder ?
                                    getChildDrawingOrder(childrenCount, i) : i;
                            final View child = children[childIndex];
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                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;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                mLastTouchDownIndex = childIndex;
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }
                    }
           

這裡删除了一些不必要的代碼。我們仔細觀察一下最後一個if語句塊,可以發現,當 dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法傳回true的時候for循環即可退出,下面是這個方法代碼中有用的片段:

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不為空,那麼其調用了child的dispatchTouchEvent方法。如果目前所有的子View的dispatchTouchEvent都傳回的是false,或者目前沒有子View,那麼for循環就會跳出,執行如下的代碼:

if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            }
           

這次傳入dispatchTransformedTouchEvent方法的參數是null,由上面的代碼可以看出将調用父View的dispatchTouchEvent方法。這裡的傳回值是handled,也就是子View的dispatchTouchEvent傳回值或者目前View的處理,也說明了開篇時僞代碼的正确性。

前面提到了mFirstTouchTarget,那麼這個參數是在哪指派的呢?仔細看dispatchTransformedTouchEvent傳回true時進入的if代碼塊,調用了一個函數是addTouchTarget(child, idBitsToAssign),其中的代碼如下:

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

在其中完成了對mFirstTouchTarget的指派。

至此我們完成了整個ViewGroup的分析過程,如果仔細觀察不難得出結論,目前View如果将事件分發給子View來進行處理,但是子View的dispatchTouchEvent方法傳回false的話,那麼目前View将會處理這個事件。同時不難用遞歸的思想來想這個問題,如果目前的View的dispatchTouchEvent也傳回false呢,那麼目前View的上一級View就會進行處理,而最後不難發現事件将會有Activity中的dispatchTouchEvent方法進行消耗,也就是調用了Activity中的onTouchEvent,如果忘記了可以去看最開始貼出的Activity源碼。

分析過了整個ViewGroup的分發過程,現在到了整個事件分發的最底端,也就是View的分發過程,同樣的,我們先來看一下dispatchTouchEvent中的代碼:

<span style="font-size:18px;">public boolean dispatchTouchEvent(MotionEvent event) {

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                return true;
            }

            if (onTouchEvent(event)) {
                return true;
            }
        }
        return false;
    }</span>
           

整個方法很簡單,if的判斷條件過濾了所有View不響應的狀态,我們看if代碼塊内的内容。值得注意的是,當為View設定了OnTouchListener後,并且重寫的onTouch方法傳回true的時候,整個方法直接傳回了。換句話說就是onTouchEvent方法并不會執行,這點要注意。另外,注意到整個dispatchTouchEvent方法的傳回值也就是onTouch或者onTouchEvent的傳回值,代表了目前的事件傳遞到了View是否被消耗了,如果并沒有被消耗,根據上文中我們得出的結論,那麼上一級View的dispatchTouchEvent就會被調用,這個關系可以依次向上級傳遞。

上面的方法中調用了onTouchEvent,我們現在來看一看這個方法的預設實作:

if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
        }
           

剛進入方法時來了一段這樣的片段,不難了解出它的意思。在目前的View狀态為DISABLE時,隻要它是可點選的,或者是可以長點選的話,onTouchEvent就會傳回true并且将目前的事件消費掉,但是并不響應這個事件。

繼續向後來閱讀:

if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
           

看到了這樣一個代碼塊,如果目前的View設定有TouchDelegate(代理,一般用于增加點選的範圍,有興趣的可以自行了解一下)的話,那麼事件是否被消耗掉取決于代理onTouchEvent的傳回值。

而後面的代碼比較多也和事件分發無關,如果感興趣可以去讀一讀。值得注意的是,在目前的View的Clickable或LongClickable至少有一個為真時,onTouchEvent就會傳回true,也就是說View會将這個事件消費掉,同時要注意上文中有一行代碼是ViewGroup調用super的dispatchTouchEvent方法,那麼最終也是會執行到這裡的。

繼續閱讀