一些布局源码的分析
程序员文章站
2022-07-14 18:06:27
...
布局源码
每种布局都是继承自ViewGroup来实现的,本质上来说就是自定义View;并且它们的draw过程主要是交给子元素来实现的,因此下面仅仅简单分析各种布局的measure和layout过程(具体就是onMeasure和onLayout方法)。
LinearLayout
onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//根据横纵方向调用两个不同的方法进行测量
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
//下面以竖直方向来说
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
//...
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
//... child为空、Gone以及分界线的情况略去
//累计权重
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
//计算
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
//精确模式的情况下,子控件layout_height=0dp且weight大于0无法计算子控件的高度
//但是可以先把margin值合入到总值中,后面根据剩余空间及权值再重新计算对应的高度
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
} else {
if (lp.height == 0 && lp.weight > 0) {
//如果这个条件成立,就代表 heightMode不是精确测量以及wrap_conent模式
//也就是说布局是越小越好,你还想利用权值多分剩余空间是不可能的,只设为wrap_content模式
lp.height = LayoutParams.WRAP_CONTENT;
}
// 子控件测量
measureChildBeforeLayout(child, i, widthMeasureSpec,0, heightMeasureSpec,totalWeight== 0 ? mTotalLength :0);
//获取该子视图最终的高度,并将这个高度添加到mTotalLength中
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
//...
}
小结
需要注意的是在每次对child测量完毕后,都会调用child.getMeasuredHeight()获取该子视图最终的高度,并将这个高度添加到mTotalLength中。但是getMeasuredHeight暂时避开了lp.weight>0且高度为0子View,因为后面会将把剩余高度按weight分配给相应的子View。因此可以得出以下结论:
- 如果我们在LinearLayout中不使用weight属性,将只进行一次measure的过程。
- 如果使用了weight属性,LinearLayout在第一次测量时获取所有子View的高度,之后会再对所有子元素进行一次遍历,将剩余高度根据weight加到weight>0的子View上。
由此可见,weight属性对性能是有影响的。
其他说明
如果orientation是纵向的,那么LinearLayout的宽度为所有子View中最宽的宽度;高度为所有子元素高度的(子元素的高度为本身高度+上下margin)和加上LinearLayout自身上下的padding。
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);
}
}
//这里仍以纵向进行分析
void layoutVertical(int left, int top, int right, int bottom) {
...
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();
//当前子元素的LayoutParams
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
...
childTop += lp.topMargin;
//这个方法调用了child的layout方法,确定了它的四个点的位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
//更新下一个子View的上边界。从这里可以看到,从上到下依次放置子元素
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
小结:
- 先判断是横向还是纵向布局。
- 只会对所有子元素进行一次layout。如果是纵向,从上到下依次放置子元素;如果是横向,从左到右依次放置子元素。
RelativeLayout
onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mDirtyHierarchy) {
mDirtyHierarchy = false;
//它会根据子元素之间的依赖关系对横向和纵向进行两次排序,结果分别存储在mSortedHorizontalChildren和mSortedVerticalChildren中
sortChildren();
}
...
View[] views = mSortedHorizontalChildren;
int count = views.length;
for (int i = 0; i < count; i++) {
View child = views[i];
if (child.getVisibility() != GONE) {
LayoutParams params = (LayoutParams) child.getLayoutParams();
applyHorizontalSizeRules(params, myWidth);
measureChildHorizontal(child, params, myWidth, myHeight);
if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
offsetHorizontalAxis = true;
}
}
}
views = mSortedVerticalChildren;
count = views.length;
for (int i = 0; i < count; i++) {
View child = views[i];
if (child.getVisibility() != GONE) {
LayoutParams params = (LayoutParams) child.getLayoutParams();
applyVerticalSizeRules(params, myHeight);
measureChild(child, params, myWidth, myHeight);
if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
offsetVerticalAxis = true;
}
if (isWrapContentWidth) {
width = Math.max(width, params.mRight);
}
if (isWrapContentHeight) {
height = Math.max(height, params.mBottom);
}
if (child != ignore || verticalGravity) {
left = Math.min(left, params.mLeft - params.leftMargin);
top = Math.min(top, params.mTop - params.topMargin);
}
if (child != ignore || horizontalGravity) {
right = Math.max(right, params.mRight + params.rightMargin);
bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
}
}
}
//...
}
小结:
- 它先会在纵向和横向通过子元素之间的依赖关系对它们进行排序。例如:C依赖了A,A依赖了B,那么顺序就是B → A → C。
- 分别从横向和纵向对所有子元素进行measure,也就是调用它们的measure方法。
因为RelativeLayout允许ViewB在横向上依赖ViewA,ViewA在纵向上依赖B。所以需要横向纵向分别进行一次排序测量。
简单来说就是根据子元素之间的依赖关系对它们进行两次measure。
onLayout
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// The layout has actually already been performed and the positions
// cached. Apply the cached values to the children.
final int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
RelativeLayout.LayoutParams st =
(RelativeLayout.LayoutParams) child.getLayoutParams();
child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
}
}
}
很简单,就是通过一次遍历,调用所有子元素的layout来确定四个顶点的位置。
FrameLayout
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
小结
很简单,它先会根据子View的layout_gravity属性中水平和竖直方向的gravity确定它的左边界和上边界,接着会调用子元素的layout方法确定最终的位置。
ConstraintLayout
onMeasure
onMeasure方法看着不多,但是其中两个方法很复杂,看得有亿点头皮发麻。大概只会对所有子元素进行一次测量,其中有很多优化的代码,看得不是很懂。
onLayout
会对满足
//params是子元素的布局参数
((child.getVisibility() != 8 || params.isGuideline || params.isHelper || params.isVirtualGroup || isInEditMode) && !params.isInPlaceholder)
这些条件的子元素进行一次layout
下一篇: jQuery源码中的一些细节分析