Android布局中同級View的事件傳遞優先級

個人原創,轉載請注明出處:http://www.lxweimin.com/p/10a2d2304f1e

說起Android中View的事件分發機制,不少開發者腦海中應該會立刻浮現出一幅流程圖。已經有許多文章詳細的分析了點擊事件在上下級ViewViewGroup之間的傳遞規則。但同級View之間的點擊事件是如何專遞的呢?換句話說,處于同一個ViewGroup內的兩個View重合時,ViewGroup是如何決定傳遞到哪一個View的?部分有經驗的開發者可能會說:按照xml中的排列順序,最后的優先觸發。的確,在相當長的時間里我也是這么認為的。但在最近的開發中我遇到了一個比較棘手的問題,這也促使我從源碼中去進行更深入的探索。

決定事件傳遞對象的源碼分析

點擊事件的分發機制主要由dispatchTouchEvent(),onInterceptTouchEvent()onTouchEvent()三個方法來完成,其中后兩個方法都是在第一個方法中調用的,作用分別是攔截事件和處理事件,與本文關系不大。那么,決定父控件將點擊事件傳遞給哪個子控件的邏輯,就應該在dispatchTouchEvent()剩余的代碼里。通常dispatchTouchEvent()這個方法不太可能會被重寫,因此我們直接看ViewGroupdispatchTouchEvent()方法:

...
    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();
        final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
        final View[] children = mChildren;
        for (int i = childrenCount - 1; i >= 0; i--) {
            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
            ...
        }
    }
...

整段方法的代碼好長啊,足足有200+行!不過我們要善于抓住核心。在方法體內搜索child關鍵字能定位到上圖所示的一段代碼。通過行間的注釋我們可以得知判斷父控件將點擊事件傳遞給哪個子控件的邏輯就在這段代碼中。而決定這個子控件的實例的代碼應該就是最后一行:

final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);

很顯然getAndVerifyPreorderedView()這個方法決定了最終由哪個子控件來接收點擊事件。方法的具體邏輯我們先放到一邊,先來看看該方法接收的3個變量,其中children根據上方代碼可推測出是包含了所有(重合)子控件的實例數組,而另外兩個變量preorderedListchildIndex從變量名就能猜到和順序有關。我們先來看看決定preorderedListbuildTouchDispatchChildList()方法,該方法直接調用了buildOrderedChildList()方法,我們繼續看該方法的代碼:

ArrayList<View> buildOrderedChildList() {
    final int childrenCount = mChildrenCount;
    if (childrenCount <= 1 || !hasChildWithZ()) return null;

    if (mPreSortedChildren == null) {
        mPreSortedChildren = new ArrayList<>(childrenCount);
    } else {
        // callers should clear, so clear shouldn't be necessary, but for safety...
        mPreSortedChildren.clear();
        mPreSortedChildren.ensureCapacity(childrenCount);
    }

    final boolean customOrder = isChildrenDrawingOrderEnabled();
    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;
}

又是好長的一段代碼喲!不過眼尖的應該很快就能注意到第二行

if (childrenCount <= 1 || !hasChildWithZ()) return null;

這個判斷語句,childrenCount很明顯就是子控件的數量,如果小于等于1就不用判斷了。而hasChildWithZ()熟悉布局文件的開發者應該能猜到這是查看是否有child設置了Z軸相關屬性,取反意味著如果沒有child設置Z軸就返回null。其實搞清楚這里基本上下面的一大段代碼就不用看了!根據實際經驗很容易得出這段代碼就是讓Z軸越大的優先級越高!

接著再來看childIndex,進入方法getAndVerifyPreorderedIndex()中:

private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) {
    final int childIndex;
    if (customOrder) {
        final int childIndex1 = getChildDrawingOrder(childrenCount, i);
        if (childIndex1 >= childrenCount) {
            throw new IndexOutOfBoundsException("getChildDrawingOrder() "
                    + "returned invalid index " + childIndex1
                    + " (child count is " + childrenCount + ")");
        }
        childIndex = childIndex1;
    } else {
        childIndex = i;
    }
    return childIndex;
}

這段代碼的邏輯比較簡單,變量customOrder顧名思義就是自定義順序,如果為false就是childIndex取默認順序,而默認順序一般來講就是xml中子控件的定義順序了。其實看到這里整個判斷的邏輯已經比較清晰明了了,主要的影響因素就是Z軸大小xml中的定義順序

最后我們再回過頭來看給child最終賦值的getAndVerifyPreorderedView()方法:

private static View getAndVerifyPreorderedView(ArrayList<View> preorderedList, View[] children,
        int childIndex) {
    final View child;
    if (preorderedList != null) {
        child = preorderedList.get(childIndex);
        if (child == null) {
            throw new RuntimeException("Invalid preorderedList contained null child at index "
                    + childIndex);
        }
    } else {
        child = children[childIndex];
    }
    return child;
}

這時再看這段代碼就很明顯了,preorderedList不為null時優先看preorderedList,否則直接看childIndex,即設置了Z軸就Z軸大的優先,否則就是xml定義靠后的優先。到這里,決定父控件將點擊事件傳遞給哪個子控件的邏輯已基本清晰。

事件在子控件間的傳遞

一般來說不用考慮這個問題,因為ViewonTouchEvent()默認會消耗事件,除非它是不可點擊的,即ViewclickablelongClickable屬性都為false。當優先級最高的子View不可點擊時,事件會傳遞到次高的View上,以此類推。

總結

當父布局下有兩個重合的子控件A和B時,點擊事件的傳遞遵循:

  1. 如果子控件設置了Z軸(elevationtranslationZ),就Z軸大的優先
  2. 如果沒有設置Z軸或Z軸相同,則xml定義靠后的優先
  3. 當優先級最高的子控件為不可點擊(clickablelongClickable屬性都為false)時,事件會傳遞到優先級次高的控件上,否則會默認消耗掉事件。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。