前言
通过学习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调用测量。
今天的文章Android常用Layout源码总结—LinearLayout分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/14575.html