目录
作用
根据 measure 测量出来的宽高,确定所有 View 的位置。
具体分析
View 本身的位置是通过它的四个点来控制的:
以下涉及到源码的部分都是版本27的,为方便理解观看,代码有所删减。
layout 的流程
先通过 measure 测量出 ViewGroup 宽高,ViewGroup 再通过 layout 方法根据自身宽高来确定自身位置。当 ViewGroup 的位置被确定后,就开始在 onLayout 方法中调用子元素的 layout 方法确定子元素的位置。子元素如果是 ViewGroup 的子类,又开始执行 onLayout,如此循环往复,直到所有子元素的位置都被确定,整个 View 树的 layout 过程就执行完了。
在上一节 《View的绘制-measure流程详解》中说过,View 的绘制流程是从 ViewRootViewImpl 中的 performMeasure()
、performLayout
、performDraw
开始的。在执行完 performMeasure()
后,开始执行 performLayout
方法:(以下源码有所删减)
//ViewRootViewImpl 类
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
final View host = mView;
/*代码省略*/
//开始执行 View 的 layout 方法
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
/*代码省略*/
}
复制代码
如此,就执行了 View 的 layout 方法:
//View 类
public void layout(int l, int t, int r, int b) {
/*代码省略*/
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//确定 View 的四个点后,调用 setOpticalFrame/setFrame 方法来控制 View 位置。
//方法 1--->setOpticalFrame
//方法 2--->setFrame
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//执行 onLayout 方法
onLayout(changed, l, t, r, b);
/*代码省略*/
}
/*代码省略*/
}
//方法 1---->setOpticalFrame
//View 类
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
Insets childInsets = getOpticalInsets();
//内部最终也是调用 setFrame 方法
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
//方法 2--->setFrame
//View 类
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
/*代码省略*/
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
//控件的大小和位置有没有改变
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
//对 View 的四个点赋值
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
if (sizeChanged) {
//这里 sizeChange 方法内部调用了 onSizeChanged 方法。
//所以当控件的大小和位置改变的时候会回调 onSizeChanged 方法
//方法 3--->sizeChange
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
/*代码省略*/
}
return changed;
}
//方法 3--->sizeChange
// View 类
private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
//执行 onSizeChanged 方法
onSizeChanged (newWidth, newHeight, oldWidth, oldHeight);
if (mOverlay != null) {
mOverlay.getOverlayView().setRight(newWidth);
mOverlay.getOverlayView().setBottom(newHeight);
}
rebuildOutline();
}
复制代码
接下来我们再看 onLayout
方法,在 View 中找到 onLayout
方法,会发现这是一个空实现的方法,里面什么也没有执行,那么我们就在 ViewGroup 中找 onLayout
的实现,发现只是一个抽象方法。
//View 类
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
复制代码
//ViewGroup 类
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
复制代码
在 View 类中 onLayout 是一个空实现不难理解,因为如果一个控件继承了 View ,它是没有子元素的,不需要确定子元素的位置,只需要确定自己的位置就够了。
在 ViewGroup 中是一个抽象方法,意思也很明显了,在控件继承自 ViewGroup 的时候,我们必须重写 onLayout
方法。因为如LinearLayout
、RelativeLayout
,他们的布局特性都是不一样的,需要各自根据自己的特性来进行制定确定子元素位置的规则。
下面以 LinearLayout
为例,分析 onLayout 里面的逻辑。
LinearLayout 的 onLayout 逻辑
//LinearLayout 类
@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
执行流程类似,就分析layoutVertical
//LinearLayout 类
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
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount();
/*省略代码*/
for (int i = 0; i < count; i++) {
//遍历子 View
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();
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
//加上 子元素的 topMargin 属性值
childTop += lp.topMargin;
//设置子元素的 位置
//方法 1 ----->setChildFrame
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
/*加上子元素的高度、bottomMargin、偏移量,就是下一个子元素的 初始 top。
如此,子元素就从上到下排列,符合我们所知的 LinearLayout 的特性*/
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
//方法 1 ----->setChildFrame
//LinearLayout 类
private void setChildFrame(View child, int left, int top, int width, int height) {
//最终又调用了子元素的 layout 方法.
child.layout(left, top, left + width, top + height);
}
复制代码
可以看到,在遍历子元素后,又调用了子元素的 layout 方法。子元素如果是继承 ViewGroup,还是会调用到子元素的 onLayout 方法,遍历自己的子元素,调用自己子元素的 layout 方法,如此循环递归,就完成了整个 View 树的 layout 流程。
getWidth、getMeasureWidth分析
getWidth 获取的值和 getMeasureWidth 获取的值有什么不同吗? 首先看 getWidth 源码:
//View 类
public final int getWidth() {
return mRight - mLeft;
}
复制代码
然后再看 mRight 和 mLeft 赋值(在我们分析 layout 的时就有展现):
以下为超精简代码,哈哈:
//View 类
public void layout(int l, int t, int r, int b) {
//setOpticalFrame 最终也是调用了 setFrame 方法
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
}
//View 类
protected boolean setFrame(int left, int top, int right, int bottom) {
//对四个值进行赋值
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
}
复制代码
然后我们再回顾调用 layout 的方法:
//LinearLayout 类
void layoutVertical(int left, int top, int right, int bottom) {
for (int i = 0; i < count; i++) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
//将获取的 childWidth 和 childHeight 传入进去
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
}
}
//LinearLayout 类
private void setChildFrame(View child, int left, int top, int width, int height) {
//子元素 layout 的方法中传入的 right = left + width
//子元素 layout 的方法中传入的 bottom = left + height
child.layout(left, top, left + width, top + height);
}
复制代码
这下就一目了然了,在 getWidth()
方法中 mRight - mLeft
其实就是等于 childWidth
,也就是 getWidth() = getMeasureWidth()
。
那么,他们两个有不相等的时候吗?有的:
//自定义 View
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//手动改变传入的值
super.onLayout(changed, left, top, right+100, bottom+100);
}
复制代码
如果我们在自定义 View 中重写 onLayout 方法,并手动改变传入的值, getWidth()
和 getMeasureWidth()
的值自然就不一样了。不过这样做好像没什么意义?
还有一种情况就是在某些情况下,View 需要多次 measure 才能确定自己的测量宽/高,在前几次测量的时候,其得出的测量宽/高 ( getMeasureWidth()/ getMeasureHeight()) 和最终宽/高 ( getWidth()/ getHeight()) 可能不一致,但是最终他们的值还是相等的。(这段话摘自刚哥的《Android 开发艺术探索》)
getHeight 和 getMeasuredHeight 过程类似,这里就不分析了。
所以我们在日常开发中,我们可以认为两者就是相等的。
另:可能还是会有疑惑,这里只分析了 LinearLayout,那么其他布局也适用这个结论么?
FrameLayout 类
//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();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
//从这里看出 FrameLayout 也是适用这个结论的
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
复制代码
RelativeLayout 类
//RelativeLayout 类
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
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();
//Relativelayout 这里传入的是 LayoutParams 中的属性
//st.mRight 和 st.mBottom 赋值很复杂,不过他们也是适用这个结论的,具体可以查看源码分析
child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
}
}
}
复制代码
其他我们日常开发使用的布局也是适用于这个结论的。
总结
参考文献
《Android开发艺术探索》第四章-View的工作原理