原始碼解析Android中View的layout佈局過程
Android中的Veiw從記憶體中到呈現在UI介面上需要依次經歷三個階段:量算 -> 佈局 -> 繪圖,關於View的量算、佈局、繪圖的總體機制可參見博文 《 Android中View的佈局及繪圖機制》。量算是佈局的基礎,如果想了解量算的細節,可參見博文《原始碼解析Android中View的measure量算過程》。本文將從原始碼角度解析View的佈局layout過程,本文會詳細介紹View佈局過程中的關鍵方法,並對原始碼加上了註釋以進行說明。
對View進行佈局的目的是計算出View的尺寸以及在其父控制元件中的位置,具體來說就是計算出View的四條邊界分別到其父控制元件左邊界、上邊界的距離,即計算View的left、top、right、bottom的值。
layout
layout()方法是View佈局的入口,其原始碼如下所示:
public void layout(int l, int t, int r, int b) {
//成員變數mPrivateFlags3中的一些位元位儲存著和layout相關的資訊
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
//如果在mPrivateFlags3的低位位元組的第4位(從最右向左數第4位)的值為1,
//那麼就表示在layout佈局前需要先對View進行量算,
//這種情況下就會執行View的onMeasure方法對View進行量算
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
//量算完成後就會將mPrivateFlags3低位位元組的第4位重置為0,
//移除掉標籤PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//如果isLayoutModeOptical()返回true,那麼就會執行setOpticalFrame()方法,
//否則會執行setFrame()方法。並且setOpticalFrame()內部會呼叫setFrame(),
//所以無論如何都會執行setFrame()方法。
//setFrame()方法會將View新的left、top、right、bottom儲存到View的成員變數中
//並且返回一個boolean值,如果返回true表示View的位置或尺寸發生了變化,
//否則表示未發生變化
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//如果View的佈局發生了變化,或者mPrivateFlags有需要LAYOUT的標籤PFLAG_LAYOUT_REQUIRED,
//那麼就會執行以下程式碼
//首先會觸發onLayout方法的執行,View中預設的onLayout方法是個空方法
//不過繼承自ViewGroup的類都需要實現onLayout方法,從而在onLayout方法中依次迴圈子View,
//並呼叫子View的layout方法
onLayout(changed, l, t, r, b);
//在執行完onLayout方法之後,從mPrivateFlags中移除標籤PFLAG_LAYOUT_REQUIRED
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
//我們可以通過View的addOnLayoutChangeListener(View.OnLayoutChangeListener listener)方法
//向View中新增多個Layout發生變化的事件監聽器
//這些事件監聽器都儲存在mListenerInfo.mOnLayoutChangeListeners這個ArrayList中
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
//首先對mOnLayoutChangeListeners中的事件監聽器進行拷貝
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
//遍歷註冊的事件監聽器,依次呼叫其onLayoutChange方法,這樣Layout事件監聽器就得到了響應
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
//從mPrivateFlags中移除強制Layout的標籤PFLAG_FORCE_LAYOUT
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
//向mPrivateFlags3中加入Layout完成的標籤PFLAG3_IS_LAID_OUT
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
在layout()方法內部剛開始執行的時候,首先會根據mPrivateFlags3變數是否具有標誌位PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT判斷是否需要執行View的onMeasure()方法。如果具有標誌位PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT,則執行onMeasure()方法,從而對View進行量算,量算的結果會儲存到View的成員變數中。量算完成後就會將mPrivateFlags3低位位元組的第4位重置為0,移除掉標籤PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT。
如果isLayoutModeOptical()返回true,那麼就會執行setOpticalFrame()方法,否則會執行setFrame()方法。並且setOpticalFrame()內部會呼叫setFrame(),所以無論如何都會執行setFrame()方法。setFrame()方法會將View新的left、top、right、bottom儲存到View的成員變數中,並且返回一個boolean值,如果返回true表示View的位置或尺寸發生了變化,否則表示未發生變化。後面會對setFrame()方法詳細介紹。
如果View的佈局發生了變化,或者mPrivateFlags有需要LAYOUT的標籤PFLAG_LAYOUT_REQUIRED,就會觸發onLayout方法的執行,View中預設的onLayout方法是個空方法。不過繼承自ViewGroup的類都需要實現onLayout方法,從而在onLayout方法中依次迴圈子View,並呼叫子View的layout方法。在執行完onLayout方法之後,從mPrivateFlags中移除標籤PFLAG_LAYOUT_REQUIRED。然後會遍歷註冊的Layout Change事件監聽器,依次呼叫其onLayoutChange方法,這樣Layout事件監聽器就得到了響應。
最後,從mPrivateFlags中移除強制Layout的標籤PFLAG_FORCE_LAYOUT,向mPrivateFlags3中加入Layout完成的標籤PFLAG3_IS_LAID_OUT。
setFrame
setFrame()方法是具體用來完成給View分配尺寸以及位置工作的,在layout()方法中會呼叫setFrame()方法。其原始碼如下所示:
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (DBG) {
Log.d("View", this + " View.setFrame(" + left + "," + top + ","
+ right + "," + bottom + ")");
}
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
//將新舊left、right、top、bottom進行對比,只要不完全相對就說明View的佈局發生了變化,
//則將changed變數設定為true
changed = true;
//先儲存一下mPrivateFlags中的PFLAG_DRAWN標籤資訊
int drawn = mPrivateFlags & PFLAG_DRAWN;
//分別計算View的新舊尺寸
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
//比較View的新舊尺寸是否相同,如果尺寸發生了變化,那麼sizeChanged的值為true
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
//將新的left、top、right、bottom儲存到View的成員變數中
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
//mRenderNode.setLeftTopRightBottom()方法會呼叫RenderNode中原生方法的nSetLeftTopRightBottom()方法,
//該方法會根據left、top、right、bottom更新用於渲染的顯示列表
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
//向mPrivateFlags中增加標籤PFLAG_HAS_BOUNDS,表示當前View具有了明確的邊界範圍
mPrivateFlags |= PFLAG_HAS_BOUNDS;
if (sizeChanged) {
//如果View的尺寸和之前相比發生了變化,那麼就執行sizeChange()方法,
//該方法中又會呼叫onSizeChanged()方法,並將View的新舊尺寸傳遞進去
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
//有可能在呼叫setFrame方法之前,invalidate方法就被呼叫了,
//這會導致mPrivateFlags移除了PFLAG_DRAWN標籤。
//如果當前View處於可見狀態就將mPrivateFlags強制新增PFLAG_DRAWN狀態位,
//這樣會確保下面的invalidate()方法會執行到其父控制元件級別。
mPrivateFlags |= PFLAG_DRAWN;
invalidate(sizeChanged);
//invalidateParentCaches()方法會移除其父控制元件的PFLAG_INVALIDATED標籤,
//這樣其父控制元件就會重建用於渲染的顯示列表
invalidateParentCaches();
}
// 重新恢復mPrivateFlags中原有的PFLAG_DRAWN標籤資訊
mPrivateFlags |= drawn;
mBackgroundSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}
notifySubtreeAccessibilityStateChangedIfNeeded();
}
return changed;
}
在該方法中,會將新舊left、right、top、bottom進行對比,只要不完全相同就說明View的佈局發生了變化,則將changed變數設定為true。然後比較View的新舊尺寸是否相同,如果尺寸發生了變化,並將其儲存到變數sizeChanged中。如果尺寸發生了變化,那麼sizeChanged的值為true。
然後將新的left、top、right、bottom儲存到View的成員變數中儲存下來。並執行mRenderNode.setLeftTopRightBottom()方法會,其會呼叫RenderNode中原生方法的nSetLeftTopRightBottom()方法,該方法會根據left、top、right、bottom更新用於渲染的顯示列表。
如果View的尺寸和之前相比發生了變化,那麼就執行sizeChange()方法,該方法中又會呼叫onSizeChanged()方法,並將View的新舊尺寸傳遞進去。
如果View處於可見狀態,那麼會呼叫invalidate和invalidateParentCaches方法。invalidateParentCaches()方法會移除其父控制元件的PFLAG_INVALIDATED標籤,這樣其父控制元件就會重建用於渲染的顯示列表。
sizeChange
sizeChange方法會在View的尺寸發生變化時呼叫,在setFrame()方法中就可能會呼叫sizeChange()方法。當然,在View的setLeft()、setTop()、setRight()、setBottom()等其他改變View尺寸的方法中也會呼叫sizeChange()方法,其原始碼如下所示:
private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
//將View的新舊尺寸傳遞給onSizeChanged()方法
onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
if (mOverlay != null) {
mOverlay.getOverlayView().setRight(newWidth);
mOverlay.getOverlayView().setBottom(newHeight);
}
rebuildOutline();
}
在該方法中其主要將View的新舊尺寸傳遞給onSizeChanged()方法使其執行。
onSizeChanged
onSizeChanged()方法是個空方法,程式碼如下所示:
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
}
該方法會在View的尺寸發生變化時,通過sizeChange()方法的執行而被呼叫。當View第一次加入到View樹中時,該方法也會被呼叫,只不過傳入的舊尺寸oldWidth和oldHeight都是0。
總結
layout方法總的呼叫過程主線如下所示:
layout() -> onMeasure() -> setFrame() -> sizeChange() -> onSizeChanged() -> onLayout() ->遍歷執行OnLayoutChangeListener.onLayoutChange()
希望本文對大家理解View的layout佈局過程有所幫助!
相關閱讀:
《我的Android博文整理彙總》
《 Android中View的佈局及繪圖機制》
《原始碼解析Android中View的measure量算過程》
相關文章
- Android中View的測量和佈局過程AndroidView
- Android View 原始碼解析(三) – View的繪製過程AndroidView原始碼
- Android原始碼完全解析——View的Measure過程Android原始碼View
- Element原始碼分析系列1一Layout(佈局)原始碼
- Android XML佈局報錯:android/view/View$OnUnhandledKeyEventListenerAndroidXMLView
- Android View 原始碼解析(一) - setContentViewAndroidView原始碼
- android apk安裝過程原始碼解析AndroidAPK原始碼
- layout佈局
- 你需要知道的Android View的佈局AndroidView
- Android View的繪製過程AndroidView
- Android自定義View(四)側滑佈局AndroidView
- Android 事件分發機制原始碼解析-view層Android事件原始碼View
- 進擊的佈局之Grid Layout
- 原始碼解析.Net中Host主機的構建過程原始碼
- Android 中LayoutInflater(佈局載入器)原始碼篇之rInflate方法Android原始碼
- Spark 原始碼系列(六)Shuffle 的過程解析Spark原始碼
- Android中佈局的優化Android優化
- Xamarin 學習筆記 - Layout(佈局)筆記
- 從Chrome原始碼看DNS解析過程Chrome原始碼DNS
- 以太坊啟動過程原始碼解析原始碼
- SpringMVC原始碼解析(1)-啟動過程SpringMVC原始碼
- Dubbo服務呼叫過程原始碼解析④原始碼
- Spring Bean 的例項化過程原始碼解析SpringBean原始碼
- ASP.NET Core MVC 之佈局(Layout)ASP.NETMVC
- easyui-layout佈局高度自適應UI
- Dubbo原始碼解析之服務引入過程原始碼
- Dubbo原始碼解析之服務呼叫過程原始碼
- 看 Lumen 原始碼解析 Request 到 Response 過程原始碼
- Feign原始碼解析:初始化過程(一)原始碼
- Feign原始碼解析:初始化過程(二)原始碼
- 從原始碼角度解析 Springboot 2.6.2 的啟動過程原始碼Spring Boot
- SAP UI5 Form 表單的 Responsive Grid Layout 佈局中的 breakpointUIORM
- Android View 事件分發原始碼分析AndroidView事件原始碼
- Android View繪製原始碼分析 MeasureAndroidView原始碼
- Android 佈局Android
- Android 原始碼分析之 EventBus 的原始碼解析Android原始碼
- Android開發 - 檢視佈局屬性解析Android
- Universal-Image-Loader原始碼解解析---display過程 + 獲取bitmap過程原始碼
- jquery.dataTable.js 使用詳解 二、sDom佈局原始碼解析jQueryJS原始碼