引言
在 Android 开发的世界里,布局是构建用户界面的基石,而 LinearLayout 作为最常用的布局之一,如同搭建房屋的基本框架,起着至关重要的作用。它能够按照水平或垂直方向排列子视图,以简洁直观的方式为我们呈现出各种界面结构 ,无论是简单的登录界面,还是复杂的主页面布局,都能看到 LinearLayout 的身影。
深入了解 LinearLayout 的工作原理,尤其是其 measure 过程,就像是掌握了框架搭建的核心技术,不仅可以帮助我们更高效地优化布局性能,避免因布局不合理导致的界面卡顿、加载缓慢等问题,还能让我们在面对复杂界面需求时,更加游刃有余地进行布局设计,实现更灵活、更美观的用户界面。所以,接下来就让我们一起深入到 LinearLayout 的 measure 源码中,一探究竟吧。
什么是 LinearLayout 的 measure
(一)Android 视图绘制流程概览
在 Android 的视图绘制体系中,视图的呈现需历经 measure(测量)、layout(布局)和 draw(绘制)这三大关键流程 。这就好比建造一座房子,measure 是测量土地面积和规划房屋大小的阶段,layout 是确定各个房间位置和布局的过程,而 draw 则是进行房屋内外装修和装饰的步骤。
其中,measure 过程处于最基础的地位,它决定了 View 及其子 View 的大小规格。在这个过程中,系统会根据 View 的 LayoutParams 以及父容器的约束条件,计算出每个 View 应该占据的空间大小。就像在规划房屋时,要根据土地的大小和建筑规范,确定每个房间的面积和尺寸。而 LinearLayout 的 measure 过程,作为 ViewGroup 测量的重要组成部分,不仅要测量自身的大小,还要负责测量其包含的所有子 View 的大小,这就如同在规划一个大型建筑综合体时,不仅要确定整个建筑的占地面积,还要规划每个店铺、办公室等内部空间的大小 。
(二)measure 的使命
measure 的核心使命是精准确定 View 及其子 View 的大小。在这个过程中,它会综合考虑多方面因素。首先是 LayoutParams,这是开发者在布局文件中为 View 设置的布局参数,比如设置一个 Button 的宽度为 match_parent,高度为 wrap_content,这些参数会直接影响 measure 对 Button 大小的计算 。其次是父容器的约束,父容器会为子 View 提供一个可用的空间范围和测量模式。例如,一个 LinearLayout 作为父容器,它会根据自身的大小和布局方向,限制子 View 的大小和排列方式。
准确的测量结果是后续 layout 和 draw 流程的重要前提。在 layout 过程中,系统会根据 measure 得到的 View 大小,将 View 放置在父容器中的合适位置。而 draw 过程则是在确定好的位置和大小范围内,将 View 的内容绘制到屏幕上。如果 measure 过程出现偏差,比如测量的 View 大小不准确,那么在 layout 时就可能导致 View 的位置错乱,draw 时也会出现显示异常,就像房子的设计图纸尺寸错误,会导致施工过程中房间位置不对,装修效果也会大打折扣 。
深入 LinearLayout 的 measureVertical 方法
(一)方法入口与准备阶段
1. 源码呈现与初印象
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
int maxWidth = 0;
int childState = 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;
boolean skippedMeasure = false;
final int baselineChildIndex = mBaselineAlignedChildIndex;
final boolean useLargestChild = mUseLargestChild;
int largestChildHeight = Integer.MIN_VALUE;
int consumedExcessSpace = 0;
int nonSkippedChildCount = 0;
// 后续测量逻辑...
}
从这段源码可以看出,measureVertical方法用于处理 LinearLayout 在垂直方向上的测量。它接收widthMeasureSpec和heightMeasureSpec两个参数,这两个参数是由父容器传递下来的测量规范,包含了测量模式和尺寸大小等重要信息 。在方法开始时,初始化了一系列变量,这些变量将在后续的测量过程中发挥关键作用,就像是搭建房屋前准备好各种工具和材料。
测量模式
Android 定义了三种测量模式,存储在 MeasureSpec 类中:
-
MeasureSpec.EXACTLY- 含义:表示父容器已经确定了当前
View的精确大小,当前View应该按照这个大小来布局。通常当View的布局参数为固定数值(如100dp)或者MATCH_PARENT时,会使用这种测量模式。 - 示例:如果一个
View的宽度布局参数设置为200dp,父容器传递给它的widthMeasureSpec的测量模式就是EXACTLY,尺寸值就是200dp对应的像素值。
- 含义:表示父容器已经确定了当前
-
MeasureSpec.AT_MOST- 含义:表示当前
View的大小最大不能超过父容器指定的某个值,View可以根据自身内容的大小在这个范围内进行调整。通常当View的布局参数为WRAP_CONTENT时,会使用这种测量模式。 - 示例:如果一个
View的宽度布局参数设置为WRAP_CONTENT,父容器传递给它的widthMeasureSpec的测量模式就是AT_MOST,尺寸值就是父容器剩余的可用宽度。
- 含义:表示当前
-
MeasureSpec.UNSPECIFIED- 含义:表示父容器没有对当前
View的大小进行限制,View可以根据自身的需求来决定大小。这种模式比较少见,通常用于一些特殊的场景,如滚动视图中的子View,滚动视图不会限制子View的大小,子View可以自由扩展。
- 含义:表示父容器没有对当前
2. 关键变量剖析
- mTotalLength:用于记录 LinearLayout 在垂直方向上已使用的总长度,包括子 View 的高度、子 View 的 margin 以及分割线的高度等,就像是记录建筑中已经使用的垂直空间高度。
- maxWidth:表示所有子 View 中宽度的最大值,它将影响 LinearLayout 最终的宽度确定,类似于在规划建筑时,确定整个建筑在水平方向上的最大跨度。
- totalWeight:统计所有子 View 的权重总和,在后续处理权重分配时起到关键作用,比如在分配建筑资源时,根据不同区域的重要性(权重)来分配空间和材料。
- widthMode 和 heightMode:分别表示 LinearLayout 的宽度和高度测量模式,取值可能是MeasureSpec.EXACTLY(精确模式)、MeasureSpec.AT_MOST(最大模式)或MeasureSpec.UNSPECIFIED(未指定模式),这些模式决定了 LinearLayout 及其子 View 的大小确定方式,就像不同的建筑规划要求决定了房屋的建设规格 。
(二)第一次遍历:初步测量
1. 遍历逻辑与子 View 筛选
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;
}
nonSkippedChildCount++;
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
// 后续测量逻辑...
}
在这个循环中,通过getVirtualChildAt(i)方法依次获取每个子 View。如果子 View 为null,则调用measureNullChild(i)方法进行处理,并将结果累加到mTotalLength中。如果子 View 的可见性为GONE,则跳过该子 View 的测量,因为GONE状态的子 View 不会在界面上显示,也就不需要分配空间 。如果子 View 之前有分割线(通过hasDividerBeforeChildAt(i)判断),则将分割线的高度mDividerHeight累加到mTotalLength中。
2. 测量模式与权重考量
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
// 优化:不必费心测量仅使用多余空间布局的子视图。这些视图将在稍后进行测量
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
if (useExcessSpace) {
// 将子View的高度设置为WRAP_CONTENT,以便测量其最佳高度
lp.height = LayoutParams.WRAP_CONTENT;
}
// 后续测量逻辑...
}
获取子 View 的布局参数lp,并累加其权重到totalWeight。如果 LinearLayout 的高度模式为MeasureSpec.EXACTLY(精确模式),并且子 View 的高度为 0 且权重 > 0,这意味着子 View 仅在剩余空间中布局,此时会进行优化,跳过对子 View 的测量,仅将其上下 margin 累加到mTotalLength中,并标记skippedMeasure为true,表示跳过了测量 。否则,就剩下 AT_MOST 和 UNSPECIFIED 两个了,如果子 View 满足useExcessSpace条件,即高度为 0 且权重 > 0,会将子 View 的高度临时设置为LayoutParams.WRAP_CONTENT,以便测量其最佳高度,后续再恢复为 0。
3. 测量子 View 的具体步骤
final int usedHeight = totalWeight == 0? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
// 恢复原始高度并记录分配给多余子View的空间
lp.height = 0;
consumedExcessSpace += childHeight;
}
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
计算已使用的高度usedHeight,如果总权重totalWeight为 0,则usedHeight为当前的mTotalLength,否则为 0。然后调用measureChildBeforeLayout方法测量子 View,该方法内部会根据父容器的测量规范和子 View 的布局参数,为子 View 创建合适的测量规范,并调用子 View 的measure方法进行测量 。测量完成后,获取子 View 的测量高度childHeight。如果子 View 是使用多余空间布局的(useExcessSpace为true),则恢复其原始高度为 0,并记录分配给它的空间大小到consumedExcessSpace中。接着更新mTotalLength,使其包含子 View 的高度、margin 等。如果设置了useLargestChild,则更新largestChildHeight为当前子 View 高度和largestChildHeight中的较大值 。
(三)第二次遍历:特殊情况处理
1. 触发条件解析
if (useLargestChild && (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
mTotalLength = 0;
// 后续遍历逻辑...
}
第二次遍历的触发条件是useLargestChild为true,并且 LinearLayout 的高度模式为MeasureSpec.AT_MOST(最大模式)或MeasureSpec.UNSPECIFIED(未指定模式)。当满足这些条件时,说明 LinearLayout 的高度不是精确确定的,可能需要根据最大子 View 的高度来重新计算总高度 。
2. 遍历操作与目的
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();
// 考虑负margin
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + largestChildHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
在这个遍历中,首先将mTotalLength重置为 0,然后再次遍历每个子 View。对于非null且可见性不为GONE的子 View,获取其布局参数lp,并根据largestChildHeight(即最大子 View 的高度)来更新mTotalLength。这里的目的是确保所有子 View 在垂直方向上的高度都以最大子 View 的高度为基准,从而统一子 View 的高度,使布局更加整齐美观。
示例
可以看到设置了measureWithLargestChild="true"的 LinearLayout 中的子 View,如果同时设置了 weight,会使用 weight 最大的子 View 的高度为实际的高度,而不设置 weight 的子 view 不受影响,但整体的高度会受到子 view 的数量的影响也变高了。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/linearLayoutFalse"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFA6A6"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#FF0000"
android:text="Short Text"
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#6C82FD"
android:text="This is a much\n1\n2\n3\n4\n5"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/linearLayoutTrue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#918581"
android:measureWithLargestChild="true"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#6C82FD"
android:text="不设置 weight"
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="#FF0000"
android:text="Short Text,weight 为 1"
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="20"
android:background="#6C82FD"
android:text="This is a much\n1\n2\n3\n4\n5,weight 为 20"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
(四)第三次遍历:权重分配与调整
1. 再次测量的必要性
int remainingExcess = heightSize - mTotalLength + (mAllowInconsistentMeasurement? 0 : consumedExcessSpace);
if (skippedMeasure || ((sRemeasureWeightedChildren || remainingExcess!= 0) && totalWeight > 0.0f)) {
// 后续遍历逻辑...
}
计算剩余的可分配空间remainingExcess,它是 LinearLayout 的最终高度heightSize减去当前已使用的总长度mTotalLength,再加上之前分配给多余子 View 的空间(如果允许不一致的测量,这部分空间为 0)。如果之前跳过了测量(skippedMeasure为true),或者需要重新测量带权重的子 View(sRemeasureWeightedChildren为true),或者存在剩余可分配空间且总权重 > 0,就需要进行第三次遍历,以确保带权重的子 View 能够正确分配剩余空间 。
2. 权重分配的计算逻辑
float remainingWeightSum = mWeightSum > 0.0f? mWeightSum : totalWeight;
mTotalLength = 0;
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;
if (childWeight > 0) {
final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
remainingExcess -= share;
remainingWeightSum -= childWeight;
// 后续子View测量逻辑...
}
}
计算剩余的权重总和remainingWeightSum,如果设置了mWeightSum(通过android:weightSum属性设置),则使用mWeightSum,否则使用之前统计的总权重totalWeight。将mTotalLength重置为 0,然后遍历每个子 View。对于权重 > 0 的子 View,根据其权重childWeight计算它应分配到的剩余空间份额share,计算公式为childWeight * remainingExcess / remainingWeightSum。然后更新剩余可分配空间remainingExcess和剩余权重总和remainingWeightSum 。
3. 子 View 的重新测量与布局更新
final int childHeight;
if (mUseLargestChild && heightMode!= MeasureSpec.EXACTLY) {
childHeight = largestChildHeight;
} else if (lp.height == 0 && (!mAllowInconsistentMeasurement || heightMode == MeasureSpec.EXACTLY)) {
// 子View仅使用其分配的剩余空间进行布局
childHeight = share;
} else {
// 子View在其固有高度基础上加上分配的剩余空间
childHeight = child.getMeasuredHeight() + share;
}
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(0, childHeight), MeasureSpec.EXACTLY);
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin, lp.width);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
// 更新相关布局参数
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
// 后续参数更新逻辑...
根据不同条件确定子 View 的最终高度childHeight。如果设置了mUseLargestChild且高度模式不是MeasureSpec.EXACTLY,则子 View 的高度为largestChildHeight;如果子 View 高度为 0 且满足一定条件(不允许不一致测量或高度模式为精确模式),则子 View 高度为分配到的空间份额share;否则,子 View 高度为其固有测量高度加上分配的空间份额share 。然后根据childHeight创建精确模式的高度测量规范childHeightMeasureSpec,并根据父容器的测量规范、padding 和子 View 的 margin、布局宽度等创建宽度测量规范childWidthMeasureSpec。最后调用子 View 的measure方法重新测量子 View 。测量完成后,更新相关布局参数,如最大宽度maxWidth等。
(五)其他关键步骤与细节
1. 宽度相关的计算与处理
在整个测量过程中,除了关注高度,宽度的计算也很重要。maxWidth用于记录所有子 View 中宽度的最大值,在第一次遍历中,通过maxWidth = Math.max(maxWidth, measuredWidth);不断更新maxWidth,其中measuredWidth是子 View 的测量宽度加上其左右 margin 。对于带权重的子 View 和不带权重的子 View,分别使用weightedMaxWidth和alternativeMaxWidth来记录它们的最大宽度,这在后续确定 LinearLayout 的最终宽度时起到关键作用。例如,在最后确定 LinearLayout 的宽度时,如果不是所有子 View 都填充父容器且宽度模式不是精确模式,会使用alternativeMaxWidth来确定最终宽度 。
2. 与 padding、margin 的交互
LinearLayout 的padding和子 View 的margin对测量有直接影响。在测量子 View 时,会考虑 LinearLayout 的padding和子 View 的margin来创建子 View 的测量规范。例如,在getChildrenMeasureSpec方法中,会将 LinearLayout 的padding和子 View 的margin累加到已使用的宽度和高度中,从而影响子 View 的测量范围 。在更新mTotalLength时,也会加上子 View 的topMargin和bottomMargin,确保总长度包含了子 View 的所有布局空间。
3. 测量结果的确定与返回
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState);
在完成所有测量和计算后,通过setMeasuredDimension方法设置 LinearLayout 的测量尺寸。其中,宽度通过resolveSizeAndState(maxWidth, widthMeasureSpec, childState)方法来确定,它会根据maxWidth、宽度测量规范widthMeasureSpec和子 View 的测量状态childState来综合计算出最终的宽度。高度则通过之前计算得到的heightSizeAndState来确定,这个值包含了高度大小和测量状态等信息 。这样,LinearLayout 的测量过程就完成了,确定的尺寸将用于后续的布局和绘制流程 。
实例分析与应用场景
(一)简单布局示例
1. 布局代码展示
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户名:"
android:textSize="18sp" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入用户名"
android:padding="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="密码:"
android:textSize="18sp" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入密码"
android:inputType="textPassword"
android:padding="8dp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="登录" />
</LinearLayout>
这是一个简单的登录界面布局,使用了 LinearLayout 作为根布局,并设置为垂直方向排列。其中包含了多个 TextView、EditText 和 Button 等子 View ,每个子 View 都有各自的布局参数和属性设置。
2. measure 过程模拟
在这个布局中,LinearLayout 的measureVertical方法开始执行。首先,初始化各种变量,如mTotalLength初始化为 0,用于记录 LinearLayout 在垂直方向上已使用的总长度。
然后,依次遍历每个子 View。对于第一个 TextView“用户名:”,它的可见性为 VISIBLE,不是 GONE 状态。由于它没有设置权重,并且宽度为wrap_content,高度为wrap_content,所以它会根据自身的文本内容和字体大小等因素,测量出自己的宽度和高度。假设测量出的宽度为 80dp,高度为 30dp ,此时mTotalLength更新为 30dp(TextView 的高度)。
接着处理 EditText,它的宽度为match_parent,高度为wrap_content。在测量时,它会根据父容器 LinearLayout 的宽度约束(这里是match_parent,即屏幕宽度减去左右 padding)和自身的wrap_content属性,计算出自己的宽度和高度。假设它的高度测量为 40dp,此时mTotalLength更新为 30dp + 40dp = 70dp 。
以此类推,后续的 TextView“密码:” 和 EditText 以及 Button 都会按照各自的布局参数和属性进行测量,并更新mTotalLength。当所有子 View 测量完成后,mTotalLength就记录了整个 LinearLayout 在垂直方向上的总高度,再加上 LinearLayout 自身的 padding(这里上下 padding 假设各为 16dp),最终确定 LinearLayout 的高度为mTotalLength + 16dp + 16dp 。而宽度则是根据子 View 中宽度最大的那个来确定,再加上左右 padding。
在这个过程中,由于没有子 View 设置权重,所以不存在权重分配和调整的步骤。每个子 View 都是根据自身的属性和父容器的约束进行独立测量 ,最终形成了这样一个简单而有序的登录界面布局。
(二)复杂布局与权重应用
1. 复杂布局结构解析
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="3">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="第一部分"
android:gravity="center"
android:background="#FF9900" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:text="第二部分,内容可能会比较长,需要占用更多空间"
android:gravity="center"
android:background="#00CCFF" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="16dp">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="按钮1" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:text="按钮2" />
</LinearLayout>
</LinearLayout>
这个布局相对复杂一些,包含了嵌套的 LinearLayout。外层的 LinearLayout 是垂直方向排列,内部又有两个水平方向排列的 LinearLayout。第一个内部 LinearLayout 设置了weightSum为 3 ,并包含两个 TextView,它们分别设置了layout_weight为 1 和 2,用于按比例分配水平空间。第二个内部 LinearLayout 包含两个按钮,它们没有设置权重,按照正常的布局方式排列 。
2. 权重在实际中的作用与效果
在第一个内部 LinearLayout 中,权重起到了关键作用。两个 TextView 的宽度都设置为 0dp,这是使用权重的一个重要条件。当 LinearLayout 的宽度确定后(这里是match_parent,即父容器的宽度减去左右 padding),剩余的空间会按照权重比例分配给这两个 TextView 。
假设 LinearLayout 的宽度为 300dp(减去左右 padding 后的可用宽度),由于weightSum为 3,第一个 TextView 的layout_weight为 1,第二个 TextView 的layout_weight为 2 ,那么第一个 TextView 将获得 300dp * (1 / 3) = 100dp 的宽度,第二个 TextView 将获得 300dp * (2 / 3) = 200dp 的宽度。这样就实现了根据权重来动态分配空间的效果,使得布局更加灵活,能够适应不同屏幕尺寸和内容需求 。
在实际应用中,这种权重分配可以用于很多场景。比如在一个新闻列表界面中,标题和摘要可以使用权重来分配空间,标题权重较小,占用较少空间,摘要权重大,占用较多空间,以展示更多内容。在电商商品展示界面中,商品图片和商品描述也可以通过权重来合理分配空间,确保界面布局美观且信息展示合理 。通过合理使用权重,能够有效提升界面的布局效果和用户体验。
总结与拓展
(一)回顾 LinearLayout 的 measure 要点
LinearLayout 的measureVertical方法是其垂直方向测量的核心。在这个过程中,通过多次遍历子 View,全面且细致地完成了测量工作。首先,在第一次遍历中,会初步测量每个子 View ,同时统计关键信息,如总权重totalWeight、已使用的总长度mTotalLength等。这个过程就像是对建筑材料进行初步的清点和测量,为后续的布局和分配做好准备。
对于可见性为GONE的子 View,会直接跳过测量,因为它们不会在界面上显示,也就无需占用布局空间,这体现了测量过程的高效性和针对性 。在测量过程中,还会根据子 View 的布局参数和 LinearLayout 的测量模式,灵活调整测量策略。例如,当 LinearLayout 的高度模式为MeasureSpec.EXACTLY且子 View 的高度为 0 且权重 > 0 时,会先跳过对这些子 View 的测量,仅记录其上下 margin,待后续再进行处理 ,这是一种优化策略,避免了不必要的测量操作,提高了测量效率。
第二次遍历主要是在特定条件下,如useLargestChild为true且高度模式为MeasureSpec.AT_MOST或MeasureSpec.UNSPECIFIED时,根据最大子 View 的高度来重新计算 LinearLayout 的总高度,以确保所有子 View 在垂直方向上的高度一致性 ,就像在建筑设计中,为了保证整体的美观和协调,会统一某些部分的高度标准。
第三次遍历则是在存在剩余可分配空间且有带权重的子 View 时,进行权重分配和子 View 的重新测量。通过精确计算每个带权重子 View 应分配到的空间份额,实现了对剩余空间的合理利用,使布局更加灵活和自适应 ,这类似于在资源分配中,根据不同的需求和权重,合理分配有限的资源。
(二)与其他布局的对比思考
与 RelativeLayout 相比,LinearLayout 的测量过程相对简洁直接 。RelativeLayout 由于子 View 之间存在复杂的相对位置关系,在测量时需要进行两次测量,分别处理水平和垂直方向的布局关系,这就好比在规划一个复杂的建筑群时,需要同时考虑各个建筑之间在水平和垂直方向上的位置关系,增加了测量的复杂性和计算量。而 LinearLayout 只需根据方向进行一次测量(不考虑权重时),当使用权重时最多进行两次测量 ,大大减少了测量的次数和时间开销。例如,在一个简单的列表布局中,使用 LinearLayout 可以快速地按照垂直方向排列子 View 并完成测量,而如果使用 RelativeLayout,可能需要花费更多的时间来确定每个子 View 的相对位置和大小。
与 FrameLayout 相比,FrameLayout 的测量过程更为简单 。FrameLayout 的子 View 默认是重叠放置的,在测量时只需找到子 View 中宽高的最大值作为自己的宽高即可,这就像在一个展示台上放置物品,只需要找到最大物品的尺寸来确定展示台的大小。而 LinearLayout 需要依次测量每个子 View,并根据布局参数和权重等因素来确定整体的大小和每个子 View 的位置,考虑的因素更加全面和复杂 。比如在一个包含多个图片的布局中,如果使用 FrameLayout,图片会重叠显示,测量过程简单;而使用 LinearLayout,可以按照水平或垂直方向排列图片,测量过程需要考虑图片之间的间距、权重等因素。
(三)对开发者的启示与建议
在使用 LinearLayout 时,合理设置布局参数至关重要 。避免过度使用权重,因为权重的使用会增加测量的复杂性和时间开销,可能导致性能下降。例如,在一个简单的布局中,如果所有子 View 都不需要按比例分配空间,就不要设置权重,以减少测量次数。
尽量减少嵌套的层数 。过多的嵌套会使布局结构变得复杂,增加测量和布局的时间,影响界面的加载速度和流畅性。可以通过优化布局结构,使用合适的布局组合来减少嵌套。比如,在一个复杂的界面中,可以将一些相关的子 View 组合在一个 LinearLayout 中,再将这个 LinearLayout 作为一个整体放置在其他布局中,避免过多的 LinearLayout 嵌套。
对于性能要求较高的界面,可以优先考虑使用 LinearLayout 代替 RelativeLayout ,尤其是在子 View 之间没有复杂相对位置关系的情况下。同时,在布局设计阶段,要充分考虑不同屏幕尺寸和分辨率的兼容性,确保 LinearLayout 在各种设备上都能正确地测量和布局,为用户提供良好的视觉体验 。例如,在设计一个通用的应用界面时,要测试 LinearLayout 在不同尺寸的手机和平板上的显示效果,根据测试结果调整布局参数和子 View 的大小,以保证界面的美观和可用性。