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的佈局邏輯。
可以看到,在程式碼①的位置先通過setOpticalFrame
或setFrame
方法為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,循著本文的脈絡試著一步步探索原始碼中的邏輯,學習效果可能會更好。