LinearLayout是Android App开发中最常用的控件之一。特别是当我们要实现几个控件平均分割一定的区域的时候,一般都会通过LinearLayout的layout_weight和weightsum组合实现。要理解layout_weight和weightsum这2个属性对LinearLayout布局大小的影响,最好还是结合源码进行分析。
源码分析
LinearLayout有水平和竖直两种方向,在源码中也有对应的2种测量方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
这里以水平方向为例做个分析:
void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;//占用长度:所有明确指定大小元素长度总和+margin+padding
float totalWeight = 0;//所有子元素的权重和
final int count = getVirtualChildCount();
//一般地,如果LinearLayout的layout_width为match_parent或者指定值, widthmode为EXACTLY;wrap_content则为AT_MOST
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//是否是精确模式
final boolean isExactly = widthMode == MeasureSpec.EXACTLY;
int usedExcessSpace = 0;
for (int i = 0; i < count; ++i) {//第一次遍历测量子元素
final View child = getVirtualChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;//元素权重总和
final boolean useExcessSpace = lp.width == 0 && lp.weight > 0;//我们暂且称为“0宽权重模式”
//LinearLayout为精确模式并且“0宽权重模式”
if (widthMode == MeasureSpec.EXACTLY && useExcessSpace) {
if (isExactly) {
mTotalLength += lp.leftMargin + lp.rightMargin;
} else {
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength +
lp.leftMargin + lp.rightMargin);
}
} else {//wrap_content情况或非“0宽权重模式”
if (useExcessSpace) {//“0宽权重模式”
lp.width = LayoutParams.WRAP_CONTENT;
}
final int usedWidth = totalWeight == 0 ? mTotalLength : 0;
//第一次预测量子元素
measureChildBeforeLayout(child, i, widthMeasureSpec, usedWidth,
heightMeasureSpec, 0);
final int childWidth = child.getMeasuredWidth();
if (useExcessSpace) {//“0宽权重模式”
lp.width = 0;
usedExcessSpace += childWidth;
}
//计算占用长度
if (isExactly) {//精确模式
mTotalLength += childWidth + lp.leftMargin + lp.rightMargin
+ getNextLocationOffset(child);
} else {//wrap_content情况
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childWidth + lp.leftMargin
+ lp.rightMargin + getNextLocationOffset(child));
}
}
// Add in our padding
mTotalLength += mPaddingLeft + mPaddingRight;
int widthSize = mTotalLength;
// Check against our minimum width
widthSize = Math.max(widthSize, getSuggestedMinimumWidth());
// Reconcile our calculated size with the widthMeasureSpec
int widthSizeAndState = resolveSizeAndState(widthSize, widthMeasureSpec, 0);
widthSize = widthSizeAndState & 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 remainingExcess = widthSize - mTotalLength
+ (mAllowInconsistentMeasurement ? 0 : usedExcessSpace);//注意这里1,计算“剩余空间”
if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;//注意这里2:如果指定了android:weightSum属性,权重和=android:weightSum指定的值
mTotalLength = 0;
//开始第二次测量子元素
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final float childWeight = lp.weight;
if (childWeight > 0) {
final int share = (int) (childWeight * remainingExcess / remainingWeightSum);//注意这里3:计算子元素在剩余空间占用的份额
remainingExcess -= share;
remainingWeightSum -= childWeight;
final int childWidth;
if (lp.width == 0 && (!mAllowInconsistentMeasurement
|| widthMode == MeasureSpec.EXACTLY)) {
childWidth = share;
} else {
childWidth = child.getMeasuredWidth() + share;
}
//根据childWidth创建childWidthMeasureSpec
final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
Math.max(0, childWidth), MeasureSpec.EXACTLY);
final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin,
lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);//注意这里4:真正测量子元素
}
if (isExactly) {
mTotalLength += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin +
getNextLocationOffset(child);
} else {
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredWidth() +
lp.leftMargin + lp.rightMargin + getNextLocationOffset(child));
}
}
// Add in our padding
mTotalLength += mPaddingLeft + mPaddingRight;
}
}
从以上代码,我们可以得出:LinearLayout会对所有子元素进行2次测量:第一次预测量是为了计算“剩余空间”;第二次测量是根据weight、weightsum计算在“剩余空间”所占的份额,进而创建measurespec进行测量。
关于weightsum
从上面注意这里2 源码:
float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
我们可以得出:
- 未指定android:weightSum属性时,权重和=所有子控件的weight之和;
- 如果指定了android:weightSum属性,权重和=android:weightSum指定的值,而不是子控件weight和;
关于weight
- weight是针对剩余空间的分配,而不是针对LinearLayout总空间的分配;
- 剩余空间=总宽度-padding-(每个元素的原始宽度+margin+padding)
- 元素的长度=原始宽度+权重*父视图剩余空间/权重和
例子
测试手机屏幕宽度1440px, dp=4
例1
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<View
android:id="@+id/view1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@android:color/holo_blue_light" />
<View
android:id="@+id/view2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@android:color/holo_green_light" />
<View
android:id="@+id/view3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@android:color/holo_orange_light" />
</LinearLayout>
这是最常用的情况:
剩余空间=LinearLayout的总宽度,3个view均分
view1Width=view2Width=view3Width=1440/3=480
例2
view1的宽度改为100dp,其他不变
...
<View
android:id="@+id/view1"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@android:color/holo_blue_light" />
...
剩余空间=1440-100*4=1040
view1Width=400+1040/3=746
view2Width=view3Width=1040/3=346
例3
view1的宽度改为100dp,view2/view3宽度为match_parent或wrap_content
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<View
android:id="@+id/view1"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@android:color/holo_blue_light" />
<View
android:id="@+id/view2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@android:color/holo_green_light" />
<View
android:id="@+id/view3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@android:color/holo_orange_light" />
</LinearLayout>
剩余空间=1440-100*4-1440-1440=-1840
view1Width=400-1840/3<0 => 0
view2Width=view3Width=1440-1840/3=827
例4
view1/view2/view3宽度为match_parent或wrap_content,view1的layout_weight=2
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<View
android:id="@+id/view1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="2"
android:background="@android:color/holo_blue_light" />
<View
android:id="@+id/view2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@android:color/holo_green_light" />
<View
android:id="@+id/view3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@android:color/holo_orange_light" />
</LinearLayout>
剩余空间=1440-1440-1440-1440=-2880
view1Width=1440-2880*2/4 = 0
view2Width=view3Width=1440-2880/4=720
另外,提示大家一点的是:可以通过Android Studio的Layout Inspector查看控件的大小