文章目錄
前言
通過學習Android官方Layout的源碼,可以幫助自己更好的理解Android的UI框架系統,瞭解內部便捷的封裝好的API調用,有助於進行佈局優化和自定義view實現等工作。這裏把學習結果通過寫博客進行總結,便於記憶,不至於將來遺忘。
本篇博客中源碼基於Android 8.1
LinearLayout特點
LinearLayout是Android開發中最常用的Layout之一,它支持水平或垂直線性佈局,並且支持child設置權重weight,使child能夠在主軸按一定比例填充LinearLayout。
源碼探究
佈局屬性
首先查看LinearLayout的構造函數源碼,在其中獲取LinearLayout特有的佈局屬性:
public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.LinearLayout, defStyleAttr, defStyleRes);
// 水平或垂直方向(0:HORIZONTAL,1:VERTICAL)
int index = a.getInt(com.android.internal.R.styleable.LinearLayout_orientation, -1);
if (index >= 0) {
setOrientation(index);
}
// child的對齊方式
index = a.getInt(com.android.internal.R.styleable.LinearLayout_gravity, -1);
if (index >= 0) {
setGravity(index);
}
// 所有文本child以文字基線對齊
boolean baselineAligned = a.getBoolean(R.styleable.LinearLayout_baselineAligned, true);
if (!baselineAligned) {
setBaselineAligned(baselineAligned);
}
// 總權重(若沒有設置,則計算child的權重之和)
mWeightSum = a.getFloat(R.styleable.LinearLayout_weightSum, -1.0f);
// 指定以某個child的文字基線作爲基準線對齊
mBaselineAlignedChildIndex =
a.getInt(com.android.internal.R.styleable.LinearLayout_baselineAlignedChildIndex, -1);
// 所有設置權重且無精確尺寸的child,修改他們的尺寸和最大的child的尺寸一致
mUseLargestChild = a.getBoolean(R.styleable.LinearLayout_measureWithLargestChild, false);
// 顯示分割線
mShowDividers = a.getInt(R.styleable.LinearLayout_showDividers, SHOW_DIVIDER_NONE);
// 分割線距兩端的間距
mDividerPadding = a.getDimensionPixelSize(R.styleable.LinearLayout_dividerPadding, 0);
// 保存分割線的Drawable和寬高
setDividerDrawable(a.getDrawable(R.styleable.LinearLayout_divider));
final int version = context.getApplicationInfo().targetSdkVersion;
mAllowInconsistentMeasurement = version <= Build.VERSION_CODES.M;
a.recycle();
}
屬性說明:
-
orientation
child在LinearLayout中的水平或垂直排列方式。 -
gravity
child在LinearLayout的對齊方式。 -
baselineAligned
child以文字基線對齊。
例:
-
weightSum
權重總和,不設置的話將計算各child的weight之和。
例:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:weightSum="1"
android:orientation="vertical"
android:background="#dcdcdc">
<TextView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.6"
android:background="#3399cc"/>
</LinearLayout>
-
baselineAlignedChildIndex
以某一個child的文字基線作爲基準對齊。
例:
根佈局爲水平LinearLayout,紫塊、藍塊、綠塊是三個垂直LinearLayout,分別設置baselineAlignedChildIndex屬性值爲2、0、1,意味着紫塊以索引2即第三個child的文字基線作爲基準線,藍塊以第一個child爲基準,綠塊以第二個child爲基準,進行對齊。 -
measureWithLargestChild
使所有設置了權重且爲設置精確尺寸的child的尺寸統一成最大的那個child的尺寸。
例:
-
showDividers
顯示分割線,支持設置beginning(分割線位於第一個child前面)、end(分割線位於最後一個child後面)、middle(分割線位於每個child之間)、none(默認,不顯示分割線)。 -
dividerPadding
若顯示分割線,設置分割線距兩端的間距。 -
divider
設置分割線的圖案。
例:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@drawable/shape_line"
android:dividerPadding="30dp"
android:showDividers="middle"
android:orientation="vertical" >
<View
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#e8eaf6"/>
<View
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#e0f2f1"/>
<View
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#e8f5e9"/>
</LinearLayout>
LayoutParams
LinearLayout中定義了靜態內部類LayoutParams繼承自MarginLayoutParams,定義了兩個成員weight、gravity:
public float weight;
public int gravity = -1;
因此支持child設置權重和對齊方式。
onMeasure測量
LinearLayout在測量過程中會根據child的LayoutParams進行多次測量,測量流程較長,這裏把測量分爲預測量和補充測量。
開始測量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 根據排列方向,執行不同的測量方法。
if (mOrientation == VERTICAL) {
// 垂直排列
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
// 水平排列
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
measureVertical和measureHorizontal方法內的邏輯類似
這裏以垂直排列爲例。
準備初始參數階段
進入measureVertical方法,首先初始一些變量,用作輔助測量計算:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// 內容總高度(所有child的測量高度總和+divider高度+邊距)
mTotalLength = 0;
// 最大寬度(最大child寬度+邊距)
int maxWidth = 0;
// child測量狀態(可設置MEASURED_STATE_TOO_SMALL標識位,用於向父佈局請求加大寬高)
int childState = 0;
// 備選最大寬度(記錄非權重的child最大寬度)
int alternativeMaxWidth = 0;
// 權重最大寬度(記錄含權重的child最大寬度)
int weightedMaxWidth = 0;
// 標記是否所有child的LayoutParams.width爲MATCH_PARENT
boolean allFillParent = true;
// 計算child的權重之和
float totalWeight = 0;
// child數量(該方法內直接調用getChildCount,子類可重寫該方法進行)
final int count = getVirtualChildCount();
// 取出父佈局傳入的測量規格模式。
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 標記是否在LinearLayout的寬度確定後,對LayoutParams.width爲MATCH_PARENT的child進行再次測量。
boolean matchWidth = false;
// 標記是否有對某個child暫不測量。
boolean skippedMeasure = false;
// baselineAlignedChildIndex屬性值,默認爲-1。
final int baselineChildIndex = mBaselineAlignedChildIndex;
// measureWithLargestChild屬性值,默認爲false。
final boolean useLargestChild = mUseLargestChild;
// 最大child的高度,當useLargestChild爲true時有用。
int largestChildHeight = Integer.MIN_VALUE;
// 記錄設置了LayoutParams.height爲0像素且權重大於0的child佔用的總高度
int consumedExcessSpace = 0;
// 記錄在第一輪遍歷child時,有效child個數。
int nonSkippedChildCount = 0;
// 省略其餘部分
// ···
}
預測量階段
在準備完參數變量後,接下來開始遍歷子view:
void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
// 省略上面部分
// ···
// See how tall everyone is. Also remember max width.
// 遍歷測量子view,同時比較出最寬的子view的寬度。
for (int i = 0; i < count; ++i) {
// 根據索引返回子view(該方法內直接調用getChildAt,子類可重新該方法擴展,因此有可能返回null)。
final View child = getVirtualChildAt(i);
if (child == null) {
// 跳過空的child,measureNullChild固定返回0。
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
// 跳過GONE的child,getChildrenSkipCount固定返回0,子類可重寫使之跳過後續的child。
i += getChildrenSkipCount(child, i);
continue;
}
// 完成非null和非GONE檢查後,計數加一,該計數用於後續divider的判斷
nonSkippedChildCount++;
// 首先判斷是否計算divider高度
// hasDividerBeforeChildAt根據當前索引和showDividers屬性值
// 判斷當前位置是否有divider(可能存在beginning和middle兩個位置)。
if (hasDividerBeforeChildAt(i)) {
// 若有divider,需要增加divider的高度。
mTotalLength += mDividerHeight;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 累加child總權重
totalWeight += lp.weight;
// 標記該child是否使用剩餘空間(當child設置高度爲0像素且設置大於0的權重,則LinearLayout先分配空間給其他child,
// 之後再用剩餘空間進行權重分配)。
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
// 若高度模式爲EXACTLY,表示LinearLayout自身的高度大小是可以確定的(LinearLayout自身的LayoutParams.height
// 設置了精確的像素尺寸。或者是MATCH_PARENT,但LinearLayout的父佈局的高度也是確定的)。
// 且同時該child被標記使用剩餘空間,則先不測量和獲取child的測量高度,僅計算margin。
// Optimization: don't bother measuring children who are only
// laid out using excess space. These views will get measured
// later if we have space to distribute.
// 這裏可以優化的原因是因爲LinearLayout的LayoutParams.height是可以明確的,也不會是WRAP_CONTENT,
// 無需知道child自身內容高度來計算LinearLayout自身高度。而child的LayoutParams.height爲0像素,
// 完全依賴LinearLayout剩餘空間分配權重,若剩餘空間爲0,則child也不會顯示。
final int totalLength = mTotalLength;
// 這裏取大值是爲了避免margin爲負數。
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
// 標記skippedMeasure,用於後續判斷補充測量。
skippedMeasure = true;
} else {
// 常規測量流程
if (useExcessSpace) {
// The heightMode is either UNSPECIFIED or AT_MOST, and
// this child is only laid out using excess space. Measure
// using WRAP_CONTENT so that we can find out the view's
// optimal height. We'll restore the original height of 0
// after measurement.
// 進入這個條件的話,高度模式只能是UNSPECIFIED或AT_MOST,且child被標記使用剩餘空間。
// 在child測量前把LayoutParams.height臨時改爲WRAP_CONTENT,使child能夠測量自身內容需要的高度,測量完成後再改回0。
lp.height = LayoutParams.WRAP_CONTENT;
}
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
// 已佔用的高度,如果截至到當前,都沒有child設置大於0的權重,賦值爲總內容高度,否則賦值爲0。
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
// 第一次調用child測量。
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
// 獲取child測量後的高度。
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
// Restore the original height and record how much space
// we've allocated to excess-only children so that we can
// match the behavior of EXACTLY measurement.
// 恢復LayoutParams.height爲0。
lp.height = 0;
// 記錄消耗的高度,用於後續計算剩餘空間。(爲什麼這裏單獨記錄消耗高度?是因爲下面累加內容總高度時,
// 也會加進這個child高度,而這個child實際是需要等待後續有剩餘空間後再分配高度,因此後續計算時會加上這個值)
consumedExcessSpace += childHeight;
}
final int totalLength = mTotalLength;
// 累加總內容高度。
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
// 若設置了measureWithLargestChild屬性,記錄最大的child高度。(默認不執行,可忽略)
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
// 省略baselineChildIndex屬性邏輯部分,默認不執行,不影響主測量流程。
// ···
// 調用child測量結束,下面是處理寬度相關的邏輯。
// 用於標記該child的測量寬度是否有效。
boolean matchWidthLocally = false;
if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
// The width of the linear layout will scale, and at least one
// child said it wanted to match our width. Set a flag
// indicating that we need to remeasure at least that view when
// we know our width.
// 此時child爲MATCH_PARENT需要依賴父佈局的寬度,但父佈局自身寬度也不確定。所以標記matchWidth、matchWidthLocally爲true,
// 後面將再次調用child測量。
matchWidth = true;
matchWidthLocally = true;
}
// 計算child的margin
final int margin = lp.leftMargin + lp.rightMargin;
// child的測量寬度加上margin
final int measuredWidth = child.getMeasuredWidth() + margin;
// 記錄最大的child寬度
maxWidth = Math.max(maxWidth, measuredWidth);
// 合併child的測量狀態
childState = combineMeasuredStates(childState, child.getMeasuredState());
// 記錄是否所有child的寬度都爲MATCH_PARENT
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
// 區分設置權重的情況,記錄不同的最大寬度。
if (lp.weight > 0) {
/*
* Widths of weighted Views are bogus if we end up
* remeasuring, so keep them separate.
*/
// 若上面標記該child的測量寬度無效,則僅用margin參與比較。
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
// 若上面標記該child的測量寬度無效,則僅用margin參與比較。
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
// getChildrenSkipCount方法內直接返回0,子類可重寫使之跳過後續的child。
i += getChildrenSkipCount(child, i);
}
// 省略剩餘部分
// ···
}
這部分邏輯主要是遍歷child測量,同時記錄總高度和最大child寬度。對於設置了權重的child,這次遍歷測量並未真正根據權重分配空間,並且對於滿足特點條件的child先暫時不調用child的測量方法。
補充測量階段
接下來會根據第一輪的測量情況,對子view進行補充測量:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// 省略第一輪遍歷測量部分
// ···
// 省略判斷divider部分(通過hasDividerBeforeChildAt方法判斷是否有end位置的divider,若有,內容總高度需要加上dividerHeight)
// ···
// 省略measureWithLargestChild屬性部分(重新計算內容總高度,每個child的測量高度都用前面部分記錄的最大child高度替代)
// ···
// 內容總高度再加上LinearLayout自身的上下內邊距。
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// 保證高度不小於最小高度。
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
// 根據規格模式調整高度值。
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
// Either expand children with weight to take up available space or
// shrink them if they extend beyond our current bounds. If we skipped
// measurement on any children, we need to measure them now.
// 計算剩餘空間(mAllowInconsistentMeasurement在高於M的版本上爲false)
// 注意:remainingExcess有可能爲負數,因爲經過resolveSizeAndState的調整,heightSize可能遠小於mTotalLength。
int remainingExcess = heightSize - mTotalLength
+ (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
// 需要根據權重分配空間
// 計算剩餘總權重(若設置了weightSum屬性,以weightSum爲準,否則計算出來的child總權重爲準)
float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
// 內容總高度清零
mTotalLength = 0;
// 第二次遍歷子view
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null || child.getVisibility() == View.GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final float childWeight = lp.weight;
// 對權重大於0的child,計算權重分配空間。
if (childWeight > 0) {
// 根據權重比例從剩餘空間求出可分配高度(有可能爲負值,因此有可能出現權重大的child,實際高度反而小的情況)。
final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
// 計算剩餘高度和剩餘總權重
remainingExcess -= share;
remainingWeightSum -= childWeight;
// 計算child的高度,後面會以此值參與生成child的高度測量規格。
final int childHeight;
if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
// 使用最大的child高度,默認爲false,可忽略。
childHeight = largestChildHeight;
} else if (lp.height == 0 && (!mAllowInconsistentMeasurement
|| heightMode == MeasureSpec.EXACTLY)) {
// This child needs to be laid out from scratch using
// only its share of excess space.
// 若child的高度爲0像素,且LinearLayout自身可確定高度,child高度爲按權重分配的高度。
childHeight = share;
} else {
// This child had some intrinsic height to which we
// need to add its share of excess space.
// child高度爲child測量後高度加上按權重分配高度(因此child並不是完全按照權重比例高)。
childHeight = child.getMeasuredHeight() + share;
}
// 使用childHeight直接生成高度測量規格,此時指定明確高度,並且保證高度不爲負數。
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
Math.max(0, childHeight), MeasureSpec.EXACTLY);
// 通過getChildMeasureSpec方法按照系統默認規則生成寬度測量規格。
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
lp.width);
// 調用child測量。此時高度是明確清楚的了。
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
// Child may now not fit in vertical dimension.
// 組合child測量狀態。
childState = combineMeasuredStates(childState, child.getMeasuredState()
& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}
// 記錄最大child寬度
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
// 標記child的測量寬度是否有用
boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
lp.width == LayoutParams.MATCH_PARENT;
// 記錄最大備選寬度
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
// 記錄是否所有child的是MATCH_PARENT
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
// 累計內容總高度
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
// TODO: Should we recompute the heightSpec based on the new total length?
} else {
// 比較最大寬度
alternativeMaxWidth = Math.max(alternativeMaxWidth,
weightedMaxWidth);
// 省略measureWithLargestChild屬性部分(統一以最大child高度對子view進行測量)
// ···
}
// 省略測量尾聲部分
// ···
}
這部分主要是針對權重進行分配空間補充測量,若預測量階段有未測量的child,或child總權重大於0且有剩餘空間,則執行。
從源碼中可以看到,用於權重分配的高度,是由LinearLayout自身高度減去在預測量階段確定的總內容高度求出剩餘空間高度。各個child的高度不是嚴格和權重比例一致,而是child自身高度加上權重高度。當child的總高度超過LinearLayout高度時,權重高度會出現負數,因此會出現權重越大的child,高度反而越小。
測量尾聲階段
前面部分依次對child進行了測量,並且在過程中記錄了最大child寬度和生成了高度規格,接下來便要設置LinearLayout自身的尺寸:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// 省略前面測量部分
// ···
if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
// 若不是所有child都爲MATCH_PARENT且LinearLayout寬度未明確
maxWidth = alternativeMaxWidth;
}
// 最大寬度加上內邊距
maxWidth += mPaddingLeft + mPaddingRight;
// Check against our minimum width
// 確保寬度不小於最小寬度
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// 設置LinearLayout自身尺寸
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
if (matchWidth) {
// 若在前面測量階段中,標記有child的寬度需要依賴LinearLayout的寬度,則在最後需要再進行一次測量。
forceUniformWidth(count, heightMeasureSpec);
}
}
這部分就是給LinearLayout自身設置了尺寸,但是在最後,還要處理下先前階段中LayoutParams.width爲MATCH_PARENT的child,因爲其依賴LinearLayout的寬度而沒能準確測算寬度,對他們再次測量:
private void forceUniformWidth(int count, int heightMeasureSpec) {
// Pretend that the linear layout has an exact size.
// 獲取LinearLayout自身的寬度,生成寬度規格。
int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(),
MeasureSpec.EXACTLY);
// 遍歷子view
for (int i = 0; i< count; ++i) {
final View child = getVirtualChildAt(i);
if (child != null && child.getVisibility() != GONE) {
LinearLayout.LayoutParams lp = ((LinearLayout.LayoutParams)child.getLayoutParams());
// 測量MATCH_PARENT的child
if (lp.width == LayoutParams.MATCH_PARENT) {
// Temporarily force children to reuse their old measured height
// FIXME: this may not be right for something like wrapping text?
int oldHeight = lp.height;
// 臨時修改child的LayoutParams.height爲child的測量高度,因爲當前child的高度已經測量完成。
lp.height = child.getMeasuredHeight();
// Remeasue with new dimensions
// 調用child測量
measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0);
lp.height = oldHeight;
}
}
}
}
onLayout佈局
佈局也是根據排列方向執行不同的方法:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
layoutVertical和layoutHorizontal佈局邏輯相似,這裏以垂直排列爲例:
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight;
// Space available for child
// child可用寬度
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount();
// 垂直方向對齊方式
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
// 水平方向對齊方式
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
// 根據垂直方向對齊方式,先對上邊界做偏移。
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
// 靠底部,上邊界需向下偏移,偏移距離即爲LinearLayout高度減去內容高度的空白區域高度。
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
// 垂直居中,上邊界向下偏移空白區域一半的高度。
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
// 默認即爲靠頂部
childTop = mPaddingTop;
break;
}
// 遍歷子view,一次調用child.layout
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
// 獲取child設置的LayoutParams的對齊方式
int gravity = lp.gravity;
if (gravity < 0) {
// 若沒有設置,則以LinearLayout的水平方向對齊方式爲準。
gravity = minorGravity;
}
// 獲取內容佈局方向(RTL或LTR)
final int layoutDirection = getLayoutDirection();
// 轉換相對對齊方式(將START、END轉換成LEFT、RIGHT)
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
// 處理水平方向對齊
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
// 判斷該索引位置是否有divider,若有,上邊界需要偏移DividerHeight。
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
// setChildFrame方法內直接調用child的layout進行佈局。
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
// getChildrenSkipCount方法返回0
i += getChildrenSkipCount(child, i);
}
}
}
LinearLayout的佈局方法較簡單,按照對齊方式,對邊界進行偏移。併線性對child進行排布,不斷向下偏移上邊界。
onDraw繪製
@Override
protected void onDraw(Canvas canvas) {
// 若沒有設置分割線,則直接返回
if (mDivider == null) {
return;
}
if (mOrientation == VERTICAL) {
drawDividersVertical(canvas);
} else {
drawDividersHorizontal(canvas);
}
}
看源碼可知,LinearLayout的繪製只針對分割線。繪製同樣分爲不同方向,以垂直方向爲例:
void drawDividersVertical(Canvas canvas) {
final int count = getVirtualChildCount();
// 遍歷子view
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child != null && child.getVisibility() != GONE) {
// 判斷該索引位置是否有divider
if (hasDividerBeforeChildAt(i)) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int top = child.getTop() - lp.topMargin - mDividerHeight;
// 給divider設置Bounds,繪製在canvas上。
drawHorizontalDivider(canvas, top);
}
}
}
// 遍歷完子view後,再判斷是有有end位置的divider。
if (hasDividerBeforeChildAt(count)) {
final View child = getLastNonGoneChild();
int bottom = 0;
if (child == null) {
bottom = getHeight() - getPaddingBottom() - mDividerHeight;
} else {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
bottom = child.getBottom() + lp.bottomMargin;
}
drawHorizontalDivider(canvas, bottom);
}
}
總結
LinearLayout核心、複雜的邏輯主要在測量流程中,因爲權重的出現,可能導致對child進行多次測量。(這裏對垂直方向進行總結,水平方向邏輯相似)
child不設置權重的情況下,LinearLayout只需按照ViewGroup常規的測量流程,依次調用child測量,再計算內容高度和寬度,最後結合測量規格設置自身尺寸即可。
在含有權重child後,情況變得複雜。首先進行一次預測量,期間求出內容高度。之後進行補充測量,用LinearLayout的測量規格高度減去內容總高度後求出剩餘高度,剩餘高度再按照權重比例分配高度,讓child的高度再加上這部分高度。最後設置LinearLayout自身尺寸。
注意,不管LinearLayout有沒有權重child。若在測量期間,有child的LayoutParams.width爲MATCH_PARENT,且LinearLayout的寬度測量規格不爲EXACTLY。意味着child需要依賴父佈局的寬度,但父佈局此時寬度尚不明確。因此在LinearLayout設置完自身尺寸後,還會對這些child調用測量。