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,循着本文的脈絡試着一步步探索源碼中的邏輯,學習效果可能會更好。

參考資料

https://blog.csdn.net/lfdfhl/article/details/51393131
https://blog.csdn.net/a553181867/article/details/51524527

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章