Android View — Layout
接續上一篇的 Android View — Measure 測量篇,這次要講的是佈局 (Layout)
前言
一旦所有的 View 都被測量完畢,Android 系統就會開始佈局這些 View。佈局的過程會確定每個 View 的位置,以及它們相對於父視圖和其他 View 的位置。其結果會存儲在每個 View 的 Left、Top、Right 和 Bottom 屬性中。
Layout 最大目的就是確定 ViewGroup 的大小。當 Android 系統在進行佈局時,最終目標是確定每個 View(包括 ViewGroup)的大小和位置,而 ViewGroup 的大小通常由其內部的子 View 和佈局參數所決定。
當 ViewGroup 完成佈局過程後,會根據其內部子 View 的佈局信息,以及自身的佈局參數,計算出自己的最終大小。這個最終大小包括了父視圖所能容納的所有子 View 的大小和間距,以及父視圖自身可能的邊界或填充。這樣,Android 系統就可以準確地知道這個 ViewGroup 在屏幕上的位置和大小,進而進行後續的繪製 (Draw)。
舉例來說
Layout 運作的過程,就像是在擺放一個房間的家具一樣。房間就像是你的屏幕,而家具就是各種不同的 View,比如按鈕、文字框等等。
開始佈局
- 想像你進入了一個完全空無一物的房間。這時候你決定要怎麼擺放這些家具,這就是佈局的過程。
- 在 Android 中,這個過程是由最外層的 ViewGroup 開始的,ViewGroup 會遍歷它的所有子 View,並決定每個子 View 的大小和位置。這個 ViewGroup 可能是一個 LinearLayout、ConstraintLayout 或者其他的自定義 ViewGroup。
測量
- 在你決定好要放哪些家具後,你需要知道每個家具的尺寸,以便安排好空間。這就是測量的過程。
- 在 Android 中,每個 View 都會被要求計算自己的尺寸,這是由
measure()方法完成的。在這個過程中,View 會根據自身的內容和佈局參數計算出自己的建議寬度和高度。
安排位置
- 當你知道每個家具的大小後你就可以把它們擺放到合適的位置上。這就是安排位置的過程。
- 在 Android 中,ViewGroup 會根據子 View 的尺寸和佈局參數來計算每個子 View 的確切位置,然後將它們擺放在父視圖中的合適位置上。
開始排列 ( onLayout )
- 當 Android 系統需要對一個 ViewGroup 進行佈局時,即當其子 View 的大小或位置發生變化時,會調用 ViewGroup 的
onLayout()方法,它會呼叫每個子 View 的layout()方法。 - 在這個方法中,每個子 View 會收到它應該佔用的空間的具體信息,然後根據這些信息來設置自己的位置。
程式面
ViewRootImp
View 的 layout 起始點也是從 ViewRootImpl 開始的,ViewRootImpl 的 performLayout 方法會調用DecorView 的 layout 方法來啟動 layout 流程,傳入的後兩個參數即屏幕的寬高大小。
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
if (host == null) {
return;
}
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
Log.v(mTag, "Laying out " + host + " to (" +
host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
//開始 layout 流程
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
···
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}View
layout () 是 View 類中的方法,傳入的四個參數即我們熟知的 left、top、right、bottom,這四個值都是View 相對父容器 ViewGroup 的坐標值。
Get Voss’s stories in your inbox
Join Medium for free to get updates from this writer.
對於 DecorView 來說這四個值就分別是 0、0、screenWidth、screenHeight
public void layout(int l, int t, int r, int b) {
···
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
···
}
···
}
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}setFrame() 方法又會將 left、top、right、bottom 等四個值保存到 View 相應的幾個全局變量上,至此View 的 width 和 height 才真正確定下來,View 的 getWidth() 和 getHeight()方法都是依靠這四個值做減法運算得到的。
此外,這裡也會回調 onSizeChanged() 方法,在自定義 View 時我們往往就通過該方法來得到 View 的準確寬高大小,並在這裡接收寬高大小變化的通知。
protected boolean setFrame(int left, int top, int right, int bottom) {
···
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
···
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
···
}
return changed;
}
private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
···
}
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}ViewGroup
layout()方法會調用自身的 onLayout() 方法。onLayout()方法在View 類中是空實現,大部分情況下View 都無需重寫該方法。而ViewGroup 又將其改為了抽象方法,即每個ViewGroup 子類都需要通過實現該方法來管理自己的所有childView 的擺放位置,FrameLayout 和LinearLayout 等容器類就通過實現該方法來實現不同的佈局效果。
還是用 FrameLayout 為例子。此佈局特點就是會將所有的 childView 進行疊加覆蓋顯示,每個 childView 之間並不會形成相互約束,childView 主要是通過 layout_gravity 和 layout_margin 來聲明自己在 FrameLayout 中的位置。FrameLayout 的 padding 也會佔據一部分空間,從而影響childView 的可用空間。
FrameLayout 的 layoutChildren() 方法就需要考慮以上因素,計算得出 childView 相對FrameLayout 的 left、top、right、bottom 等值的大小,然後調用 childView 的 layout() 方法,使得 childView 能夠得到自己的真實寬高。如果 childView 也屬於 ViewGroup 類型的話,就又會層層調用重複以上步驟完成整個視圖樹的 layout 操作。
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}總結
ViewGroup 負責管理其子 View 的佈局過程。當 ViewGroup 觸發佈局過程時,它會遞歸地調用其每個子 View 的 measure() 方法和 layout() 方法。這些方法的調用順序通常是從子 View 到父視圖的遞歸調用。
測量(Measure)過程中,子 View 會計算自己的建議寬度和高度,然後將這些建議值返回給其父視圖。父視圖會根據這些建議值,以及父視圖的佈局參數(如 layout_width 和 layout_height),來確定子 View 的最終大小。
佈局(Layout)過程中,父視圖會根據子 View 的最終大小和父視圖的佈局參數,計算出每個子 View 在父視圖中的確切位置。這些位置信息將保存在子 View 的 Left、Top、Right 和 Bottom 屬性中。
最終,當所有子 View 的測量和佈局都完成後,父視圖就會得到每個子 View 的最終大小和位置信息,從而進行自身的繪製(Draw)過程。在繪製過程中,父視圖會根據子 View 的大小和位置,以及其他繪製相關的屬性 ( 如背景、邊框、文字等 ),將自己的內容繪製到屏幕上。





