您的位置:首页 > 移动开发 > Android开发

从源码角度彻底分析layout_weight使用

2016-07-01 17:21 323 查看
转载请注明出处:http://blog.csdn.net/zhaodai11?viewmode=contents

layout_weight是线性布局特有的一个属性,这个属性可以按照比例设置控件的大小。线性布局中控件layout_weight默认值为0。

你想按照比例分配界面高度时:LinearLayout 的orientation属性设置为vertical,子控件高度建议0dp



计算方式 View1高度: 1/(1+2)=1/3 View2 高度: 2/(1+2)=2/3

当时当你把高度设置为wrap_content或者match_parent时,会是什么样子呢?

当设置为wrap_content时:



看起来好像没有区别,好多人因此得出wrap_content和0dp的效果相同,但是这就说明wrap_content和0dp效果一样吗?这个放在最后面讲。

当设置为match_parent时:



当设置为match_parent时,两个控件的高度比正好和上面相反。

这就涉及到了在线性布局中LinearLayout在onMeasure方法中对layout_weight属性的处理。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//判断线性布局的方向
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}


其实横向和纵向的原理差不多,这里我们这分析纵向的measureVertical(widthMeasureSpec, heightMeasureSpec);方法。在代码中已经添加相关注释,这里就不多加说明了。

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;//所有子控件高度之和
int maxWidth = 0;//子控件的最大宽度
int childState = 0;子控件的测量状态
int alternativeMaxWidth = 0; // 子控件中layout_weight<=0的View的最大宽度
int weightedMaxWidth = 0;// 子控件中layout_weight>0的View的最大宽度
boolean allFillParent = true;//所有子控件宽度是否全部为fillParent
float totalWeight = 0;//子控件所有layout_weight之和

final int count = getVirtualChildCount();//获取所有子控件的数量
//获取宽度和高度的测量模式
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

boolean matchWidth = false;
boolean skippedMeasure = 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;
}

if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}

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.
//如果LinearLayout测量规格为MeasureSpec.EXACTLY,说明,LinearLayout的高度已经确定,并不需要依赖于子控件的高度,并且子控件的高度为0,weight>0,说明子控件的高度,依赖于LinearLayout的剩余空间来计算的
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
//进入说明LinearLayout高度不确定,依赖于子控件高度
int oldHeight = Integer.MIN_VALUE;
//当LinearLayout高度不确定,子控件高度为0,weight>0,会强制设置子控件的高度计算模式为WRAP_CONTENT来配合LinearLayout高度计算。
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) {
// 测量完成之后,重新设置 LayoutParams.height
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);
childState = combineMeasuredStates(childState, child.getMeasuredState());

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 (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
mTotalLength += mDividerHeight;
}
// 这里是处理useLargestChild相关操作
if (useLargestChild &&
(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
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
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.
int delta = heightSize - mTotalLength;//计算剩余高度
if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
// 限定weight总和范围,假如我们给过weighSum范围,那么子控件的weight总和受此影响
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
// 计算 weight 属性分配的大小,可能为负值
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
//子控件高度子控件真是高度为weight分配后的高度+本身高度
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));
}

// Child may now not fit in vertical dimension.
childState = combineMeasuredStates(childState, child.getMeasuredState()
& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}

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);

// We have no limit, so make all weighted views as tall as the largest child.
// Children will have already been measured once.
if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);

if (child == null || child.getVisibility() == View.GONE) {
continue;
}

final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();

float childExtra = lp.weight;
if (childExtra > 0) {
child.measure(
MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(largestChildHeight,
MeasureSpec.EXACTLY));
}
}
}
}

if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;
}

maxWidth += mPaddingLeft + mPaddingRight;

// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);

if (matchWidth) {
forceUniformWidth(count, heightMeasureSpec);
}
}


结论:

由上面的源码分析可以得出下面的公式:

当View layout_height!=0时

delta = heightSize - mTotalLength;
share = (int) (childExtra * delta / weightSum);
height = share+child.getMeasuredHeight()
剩余空间高度 = LinearLayout高度-所有子控件高度之和
子控件真实高度 = (子控件所占比例)*(剩余空间高度)/总比例 + 子控件原本高度


当View layout_height=0时

delta = heightSize - mTotalLength;
share = (int) (childExtra * delta / weightSum);
height = share>0?share:0
剩余空间高度 = LinearLayout高度-所有子控件高度之和
子控件按比例分配的高度 share = (子控件所占比例)*(剩余空间高度)/总比例
子控件真实高度 h = 当share>0时 为share 当share<0时,直接为0


所以当上面两个子控件高度为match_parent时,

假设 LinearLayout 高度为H  //View1 weight = 1 View2 weight=2
剩余空间 delta = H-(H+H)=-H
//View1 高度h1  View2 高度h2
h1 = 1*(-H)/(1+2)+H=2/3*H
h2 = 2*(-H)/(1+2)+H = 1/3*H

h1/h2 = 2/1


其实通过上面两个公式,我们也能理解当为wrap_content时为什么看起来效果和为0时一样,但其实不一样。主要是因为View高度不为0时,View本身的高度也会影响最终高度的计算。

下面我们来验证一下:

图1



图2



通过上面TextView1设置padding =100dp在wrap_content时会对View本身高度测量产生影响,和mTotalLength的计算产生影响

//mTotalLength计算

final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +lp.bottomMargin + getNextLocationOffset(child));

View最终高度计算 相关代码

int childHeight = child.getMeasuredHeight() + share;
if (childHeight < 0) {
childHeight = 0;
}

child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));


此外例如TextView字体大小等会影响高度的属性,也会导致wrap_content和0dp结果不一样。

所以如果你想正常使用layout_weight属性的话,最好将对应的layout_width或者layout_height设置为0dp。这样是不会出现任何偏差的。如果你想有特殊的运用,建议将上面的源码读懂,注意各种细节。

最后在举一个有意思的例子:



上面这个为
android:layout_height="wrap_content"
时比例和正常相反了。

大家可以思考一下再看答案。

原因在View的源码中:

View的onMeasure方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

//注意在这个里面MeasureSpec.AT_MOST MeasureSpec.EXACTLY处理方式是一样的,所以wrap_content 按照match_partent的方式处理了。
public static int getDefaultSize(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:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}


有错误的地方欢迎反馈,大家一起进步。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  源码 Android Binder