1. 程式人生 > >Android View原理解析之佈局流程(layout)

Android View原理解析之佈局流程(layout)

提示:本文的原始碼均取自Android 7.0(API 24)

前言

自定義View是Android進階路線上必須攻克的難題,而在這之前就應該先對View的工作原理有一個系統的理解。本系列將分為4篇部落格進行講解,本文主要對View的佈局流程進行講解。相關內容如下:

從View的角度看layout流程

在本系列的第一篇文章中講到整個檢視樹(ViewTree)的根容器是DecorView,ViewRootImpl通過呼叫DecorView的layout方法開啟佈局流程。layout是定義在View中的方法,我們先從View的角度來看看佈局過程中發生了什麼。

首先來看一下layout方法中的邏輯,關鍵程式碼如下:

/**
 * 通過這個方法為View及其所有的子View分配位置
 * 
 * 派生類不應該重寫這個方法,而應該重寫onLayout方法,
 * 並且應該在重寫的onLayout方法中完成對子View的佈局
 *
 * @param l Left position, relative to parent
 * @param t Top position, relative to parent
 * @param r Right position, relative to parent
 * @param b Bottom position, relative to parent
 */
public
void layout(int l, int t, int r, int b) { if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } int oldL = mLeft; int oldT = mTop; int
oldB = mBottom; int oldR = mRight; // ① 通過setOpticalFrame或setFrame為View設定座標,並判斷位置是否發生改變 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { // ② 如果位置發生了改變,就呼叫onLayout方法完成佈局邏輯 onLayout(changed, l, t, r, b); if (shouldDrawRoundScrollbar()) { if(mRoundScrollbarRenderer == null) { mRoundScrollbarRenderer = new RoundScrollbarRenderer(this); } } else { mRoundScrollbarRenderer = null; } mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLayoutChangeListeners != null) { ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone(); int numListeners = listenersCopy.size(); for (int i = 0; i < numListeners; ++i) { listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } } ....... }

layout方法和measure不同,並沒有使用final修飾,但註釋中也清清楚楚地寫著View的派生類不應該重寫這個方法,而應該重寫onLayout方法,並且應該在重寫的onLayout方法中完成對子View的佈局邏輯。

可以看到,在程式碼①的位置先通過setOpticalFramesetFrame方法為View設定left、right、top、bottom座標,並記錄View的位置相比之前是否發生了變化。setOpticalFrame最終也呼叫了setFrame方法,只是在這之前對傳入的四個引數做了一些更改。setFrame中的主要邏輯其實就是將傳入的四個引數分別賦值給View的四個座標,並且計算View當前的寬高,最後判斷位置是否發生了改變(只要四個座標中的任何一個值發生了變化都會返回true)。那就讓我們來看看setFrame中發生了什麼吧:

protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;

    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
    	// (A) 只要任何一個座標不同就認為View的位置發生了變化
    	changed = true; 

        int drawn = mPrivateFlags & PFLAG_DRAWN;// Remember our drawn bit

        // (B) 計算舊的寬高和新的寬高
        int oldWidth = mRight - mLeft;
        int oldHeight = mBottom - mTop;
        int newWidth = right - left;
        int newHeight = bottom - top;
        // 如果寬高與原來不同就認為View的大小發生了變化
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

        // 執行重繪流程
        invalidate(sizeChanged);

        // (C) 為座標賦新的值
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

        mPrivateFlags |= PFLAG_HAS_BOUNDS;

        // (D) 通知View的大小發生變化(最終會呼叫onSizeChanged方法)
        if (sizeChanged) {
            sizeChange(newWidth, newHeight, oldWidth, oldHeight);
        }

        if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
            mPrivateFlags |= PFLAG_DRAWN;
            invalidate(sizeChanged);
            invalidateParentCaches();
        }

        mPrivateFlags |= drawn;

        mBackgroundSizeChanged = true;
        mDefaultFocusHighlightSizeChanged = true;
        if (mForegroundInfo != null) {
            mForegroundInfo.mBoundsChanged = true;
        }

        notifySubtreeAccessibilityStateChangedIfNeeded();
    }
    return changed;
}

這個方法中的邏輯還是很清晰的,首先在程式碼(A)的位置記錄View的位置是否發生改變,然後在程式碼(B)的位置通過舊座標和傳入的新座標分別計算View的舊寬高和新寬高,如果兩者不同就認為View的大小發生了變化(sizeChanged)。緊接著在程式碼(C)的位置將傳入的引數賦值給View的四個座標,到了這一步View的位置資訊就真正發生變化了。最後在程式碼(D)的位置,如果sizeChanged為true,就呼叫sizeChange方法。View#onSizeChanged方法將在這裡呼叫,通知View的大小已經發生改變。View#onSizeChanged是一個空方法,子類可以重寫這個方法實現自己的邏輯。

執行完上面的步驟後,如果View的位置發生了改變,將在layout程式碼②的位置呼叫onLayout方法完成對子View的佈局邏輯,這個方法的程式碼如下:

/**
 * Called from layout when this view should
 * assign a size and position to each of its children.
 * 
 * 派生類應該重寫這個方法,並且完成對子View的佈局邏輯
 * 
 * @param changed This is a new size or position for this view
 * @param left Left position, relative to parent
 * @param top Top position, relative to parent
 * @param right Right position, relative to parent
 * @param bottom Bottom position, relative to parent
 */
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

可以看到在View中onLayout是個空方法,因為View自身的佈局邏輯已經在setFrame方法中完成了,這裡是要完成對子View的佈局邏輯。但是對於一個純粹的View而言,它是沒有子View的,所以這裡自然什麼都不用做。

因此,如果我們通過繼承View實現自定義View,理論上是不需要重寫layout和onLayout方法的,使用系統預設實現就好了。

從ViewGroup的角度看layout流程

講完了View中的佈局邏輯,現在我們再切換到ViewGroup的角度來看看layout流程中都要做些什麼。

首先依舊是layout方法,ViewGroup#layout方法程式碼如下:

@Override
public final void layout(int l, int t, int r, int b) {
    if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
        if (mTransition != null) {
            mTransition.layoutChange(this);
        }
        // 呼叫View的layout方法
        super.layout(l, t, r, b);
    } else {
        // record the fact that we noop'd it; request layout when transition finishes
        mLayoutCalledWhileSuppressed = true;
    }
}

雖然ViewGroup重寫了layout方法,但是關鍵邏輯依舊是通過呼叫View#layout實現的,咱們就不在這裡耗費時間了,直接看ViewGroup#onLayout方法:

@Override
protected abstract void onLayout(boolean changed,
        int l, int t, int r, int b);

當我們興沖沖地找到onLayout方法,才發現這卻是一個抽象方法。靜下心來想想,ViewGroup作為佈局容器的抽象父類,其實是無法提供一個通用佈局邏輯的,這一工作只能交給ViewGroup的具體子類實現。但是作為佈局容器,必須要實現對子View的佈局邏輯,所以ViewGroup將onLayout標記為抽象方法,保證它的子類一定會實現這個方法。

如果我們通過繼承ViewGroup的方式實現自定義View,就必須要實現onLayout方法。常規套路就是迴圈處理子View,根據希望的佈局方式計算每個子View的座標,然後呼叫子View的layout方法傳入計算好的座標。如果子View也是一個ViewGroup的話,又會在onLayout方法中繼續呼叫它的子View的layout方法,佈局流程就這樣從頂級容器逐漸向下傳播了。

整體的流程圖

上面分別從View和ViewGroup的角度講解了佈局流程,這裡再以流程圖的形式歸納一下整個layout過程,便於加深記憶:

佈局流程

小結

和上一篇文章中的測量流程相比,本文的內容相對簡單一點,但僅僅依靠閱讀很難形成深刻的記憶。不妨開啟AndroidStudio,循著本文的脈絡試著一步步探索原始碼中的邏輯,學習效果可能會更好。

參考資料