LinearLayout線性佈局measure流程原始碼分析
1. 介紹
LinearLayout線性佈局在Android開發中使用頻率很高,它包含的子控制元件將會按照橫向或者豎向的方向順序排列。線性佈局的使用在不涉及weight的情況下比較簡單,此篇主要通過分析LinearLayout原始碼理清線性佈局的measure流程以及weight所發揮的作用!
2.原始碼分析
LinearLayout線性佈局的measure操作按照不同的orientation呼叫不同的方法進行,但是原理是一樣的。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
我們這裡只分析豎直方向的measure操作,在這之前我們先看一個簡單的事例
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_weight="1"
android:background="#FF0000"
android:gravity="center"
android:text="第一個" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00FF00"
android:gravity="center"
android:text="第二個" />
</LinearLayout>
像這種佈局結構,第二個TextView宣告自己填充整個高度,雖然不知道最後怎麼顯示,但是第一個TextView因該至少為顯示20dp的高度,但是結果是第一個TextView不會顯示
為什麼會這樣呢,我們來看下measureVertical的原始碼
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
int maxWidth = 0;
int alternativeMaxWidth = 0;
int weightedMaxWidth = 0;
boolean allFillParent = true;
float totalWeight = 0;
final int count = getVirtualChildCount();
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
boolean matchWidth = false;
final int baselineChildIndex = mBaselineAlignedChildIndex;
final boolean useLargestChild = mUseLargestChild;
int largestChildHeight = Integer.MIN_VALUE;
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
// Optimization: don't bother measuring children who are going to use
// leftover space. These views will get measured again down below if
// there is any leftover space.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
} else {
int oldHeight = Integer.MIN_VALUE;
if (lp.height == 0 && lp.weight > 0) {
// heightMode is either UNSPECIFIED or AT_MOST, and this
// child wanted to stretch to fill available space.
// Translate that to WRAP_CONTENT so that it does not end up
// with a height of 0
oldHeight = 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).
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
/**
* If applicable, compute the additional offset to the child's baseline
* we'll need later when asked {@link #getBaseline}.
*/
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
// if we are trying to use a child index for our baseline, the above
// book keeping only works if there are no children above it with
// weight. fail fast to aid the developer.
if (i < baselineChildIndex && lp.weight > 0) {
throw new RuntimeException("A child of LinearLayout with index "
+ "less than mBaselineAlignedChildIndex has weight > 0, which "
+ "won't work. Either remove the weight, or don't set "
+ "mBaselineAlignedChildIndex.");
}
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.
matchWidth = true;
matchWidthLocally = true;
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
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.
*/
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
i += getChildrenSkipCount(child, i);
}
if (useLargestChild && heightMode == MeasureSpec.AT_MOST) {
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
child.getLayoutParams();
// Account for negative margins
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
}
// 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
heightSize = resolveSize(heightSize, heightMeasureSpec);
// Either expand children with weight to take up available space or
// shrink them if they extend beyond our current bounds
int delta = heightSize - mTotalLength;
if (delta != 0 && totalWeight > 0.0f) {
float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
float childExtra = lp.weight;
if (childExtra > 0) {
// Child said it could absorb extra space -- give him his share
int share = (int) (childExtra * delta / weightSum);
weightSum -= childExtra;
delta -= share;
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight +
lp.leftMargin + lp.rightMargin, lp.width);
// TODO: Use a field like lp.isMeasured to figure out if this
// child has been previously measured
if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
// child was measured once already above...
// base new measurement on stored values
int childHeight = child.getMeasuredHeight() + share;
if (childHeight < 0) {
childHeight = 0;
}
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
} else {
// child was skipped in the loop above.
// Measure for this first time here
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
MeasureSpec.EXACTLY));
}
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
lp.width == LayoutParams.MATCH_PARENT;
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
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);
}
if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;
}
maxWidth += mPaddingLeft + mPaddingRight;
// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), heightSize);
if (matchWidth) {
forceUniformWidth(count, heightMeasureSpec);
}
}
在這個方法裡面有幾個比較重要的變數
@mTotalLength 代表測量過的子佈局的高度和
@totalWeight 代表所有字佈局weight和
@heightMode線性佈局父佈局傳遞給線性佈局的測量Mode
@mWeightSum 代表線性佈局weightsum屬性,預設為-1
其中2-20行:初始化變數
其中22 - 150行:在這個迴圈裡面遍歷子View,有選擇性對某個view進行measure或者不進行measure操作
@其中39-44行:針對heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0的情況,符合這個條件的子view在這裡不被測量,當最後高度還有結餘的時候,才有可能分享結餘高度被顯示
@其中46-72行: 這裡會把符合lp.height == 0 && lp.weight > 0條件的子View的lp.height = LayoutParams.WRAP_CONTENT;因為下面對weight進行處理的時候,會判斷lp.height來區分該View是否已經測量過,在這裡你可能會有一個疑問,為什麼符合heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0不需要測量,而除了符合heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0的view都需要測量呢,這個在下面會解釋
然後對子View進行測量,只要這個子View或者之前的view的weight屬性不是0就會按照0為引數作為使用使用的高度測量該view,具體細節請檢視measureChildWithMargins方法,最後將測量後的高度累加到mTotalLength 中。
其中155-161行:這裡通過resolveSize方法確定heightSize
public static int resolveSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
result = Math.min(size, specSize);
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
通過這個方法可以看出,假如heightMode為EXACTLY的時候,最後的result是一個確定的值,而那些
heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0的子View明確是想分享剩餘的高度,因為最後剩餘多少未知,所以在上面那個疑問中是沒有必要對這些View進行測量的,最後真的有剩餘高度的情況下才進行測量,但是當heightMode為AT_MOST的時候,result為Math.min(size, specSize)所有都要測量。
其中165-210行:比較heightSize和mTotalLength的大小差值,對於符合(lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)的子View如果還有高度結餘則會按照weight比例分享高度結餘,如果高度還不夠,那麼這些view需要按照weight比例奉獻出指定的高度,所以對於weight屬性也算是一把雙刃劍
最後246行:通過setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), heightSize)以heightsize為引數,確定LinearLayout的高度和寬度
3. 結束語
通過上面的原始碼分析,可以解釋上面的例項,在第一次迴圈,TextView獲得20dp高度,TextView2獲得父佈局最大高度,這樣mTotalLength = 父佈局最大高度 + 20dp
heightSize = 父佈局最大高度,所以TextView要貢獻出int delta = heightSize - mTotalLength= -20dp
所以TextView沒有被顯示。