Android事件分發機制之原始碼完美解析(上)

fvcasdgva發表於2018-02-03

學事件分發是為了什麼呢?還不是為了解決滑動衝突的。

實際上,如果僅僅是為了解決滑動衝突的,大可不必看原始碼,只需要掌握事件分發的外在規律即可。


只要記住這張圖,再明白內部攔截法和外部攔截法,滑動衝突這一塊,都可以輕鬆解決了。

分享一個非常好的滑動衝突的例項:http://blog.csdn.net/qq_36523667/article/details/78825810

只需要掌握上述的內容,事件分發再無難題了。


但是,如果想掌握的更通透一點,原始碼是不可或缺的。因為原始碼裡還有很多細節,enable?clickable?performClick?。。。


最重要的一個問題,如果在自定義一個view的時候,onTouchEvent返回了true,代表是消費這系列事件。而且onTouchEvent不會再呼叫父容器的onTouchEvent;如果返回false,會轉而呼叫父容器的onTouchEvent。這個問題是事件分發的核心問題,可是在原始碼中哪裡有答案???

在view group裡有一段核心的程式碼

for (int i = childrenCount - 1; i >= 0; i--) {
...
// 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;
    }
}
上面會有一個for迴圈,進行手指觸控區域的查詢,看看是否會落在child裡面。找到那個手指所落在的那個child view裡。然後執行child.dispatchTouchEvent。進行一個遞迴查詢。(為什麼自動就進行一個遞迴查詢了呢?別看這裡是view哦,以為是直接就呼叫了view的dispatchTouchEvent。錯誤!比如你這個child查詢到是FrameLayout,然後你其實是一個view group,所以你的dispatch touch event 肯定是view group的dispatch touch event(因為view group重寫了view的dispatchTouchEvent))

所以for迴圈的任務是:找到當前view group中所有view、view group的區域中包含該手指落點的view、viewgroup,並進行一個遞迴的分發。

不過不是還有一種找不到的情況嘛!那種情況大部分是mFirstTouchTarget為空。

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}
也就是上面的這段程式碼。而下面的else總體上就是指代的CANCEL情況,不過本文討論的重點不是CACEL。

這種情況就是1.沒有child 2.child不需要 這個時候由這個view group 呼叫super.dispatchTouchEvent。即把這個view group作為view,呼叫他自身的view.dispatchTouchEvent。


下面著重講講子view不為空的情況,去解決那個核心問題。

重新提一下問題:如果子view返回true,為什麼就不呼叫父view的邏輯了?如果子view返回false,為什麼就呼叫父view的邏輯了?

其實上面已經給出了答案。上面的原始碼先是一個for迴圈,那個for迴圈考慮的就是子view的情況;下面有一個if,if考慮的就是父view的情況。

如果1.view group有子view 2.且這個子view返回了true,願意接收這個事件 這種情況mFirstTouchTarget就不為空了。所以下面的if判斷條件就不成立了。而如果1.view group沒有子view 2.或者這個子view 返回了 false。這個時候mFirstTouchTarget就是空了。自然就只能交給view group來處理了。

現在用最正宗的虛擬碼概括一下view group的程式碼:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    for (;;) {
        遞迴所有的子View
        1.有子View返回了true進行消費 mFirstTouchTarget = xxx
        2.沒有       
    }
    if (mFirstTouchTarget == null) {
        super.dispatchTouchEvent(ev);
    } else {
        CANCLE的情況
    }
}

接下來我們用原始碼作最後的證明:為什麼子View的onTouchEvent的返回值可以決定mFirstTouchTarget?

上面的for迴圈的邏輯虛擬碼如下



TouchTarget newTouchTarget;
for (;;) {
    if (dispatchTransformedTouchEvent(...)) {
        newTouchTarget = addTouchTarget(child...);
    }
}
所以我們需要檢視一下,addTouchTarget方法的原始碼

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
所以在這裡就完成了mFirstTouchTarget的賦值。(這裡解釋下mFirstTouchTarget的概念,他在view group中的dispatch程式碼中起了十分重要的作用。least recently added target。意思就是最近在for迴圈中搜到的並且願意消費的view。)


解決了上述問題,繼續追溯虛擬碼中的dispatchTransformedTouchEvent方法:

// 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);
}
這裡是dispatchTransformedTouchEvent中的核心程式碼。這裡是遞迴呼叫邏輯的具體實現。直接看else吧。因為for迴圈中傳入的引數是child。(if中傳入的child引數才為空)所以直接看view的dispatch程式碼就可以了。


view的dispatch的核心程式碼

if (!result && onTouchEvent(event)) {
    result = true;
}
return result;
所以view的dispatch的返回值是由view的onTouchEvent來決定的。


所以,得出結論:如果view的onTouchEvent返回true,view的dispatch多半也會返回true,然後父view group的dispatchTransformedTouchEvent多半也會返回true,然後就mFirstTouchTarget也會被賦值,因此父view group的邏輯就不會被執行了;反之,view的onTouchEvent...然後mFirstTouchTarget依然為空,因此父view group的邏輯就會被得到執行。


到這裡告一段落,這篇文章主要分析事件分發的核心機制,讓我們不僅僅停留在會用的基礎上,也知其所以然了。下篇文章會像流水賬一樣分析所有事件分發的程式碼。

相關文章