一 概述
在上篇文章中我们分析了 View 的 Measure 过程,本篇文章我们分析 Layout 过程,通过本篇文章,你将了解到:
- 关于 Layout 简单类比
- 一个简单 Demo
- View Layout 过程
- ViewGroup Layout 过程
- View/ViewGroup 常用方法分析
- 为什么说 Layout 是承上启下的作用
二 关于Layout简单类比
在上篇文章的比喻里,我们说过:
老王给三个儿子,大王(大王儿子:小小王)、二王、三王分配了具体的良田面积,三个儿子(小小王)也都确认了自己的需要的良田面积。这就是:Measure 过程
既然知道了分配给各个儿孙的良田大小,那他们到底分到哪一块呢,是靠边、还是中间、还是其它位置呢?先分给谁呢?
老王想按到这个家的时间先后顺序来分 (对应 addView 顺序),大王是自己的长子,先分配给他,于是从最左侧开始,划出3亩田给大王。现在轮到二王了,由于大王已经分配了左侧的3亩,那么给二王的5亩地只能从大王右侧开始划分,最后剩下的就分给三王。这就是:ViewGroup onLayout 过程。
大王拿到老王给自己指定的良田的边界,将这个边界 (左、上、右、下) 坐标记录下来。这就是:View Layout 过程。
接着大王告诉自己的儿子小小王:你爹有点私心啊,从爷爷那继承的5亩田地不能全分给你,我留一些养老。这就是设置:padding 过程。
如果二王在最开始测量的时候就想:我不想和大王、三王的田离得太近,那么老王就会给大王、三王与二王的土地之间留点缝隙。这就是设置:margin 过程
三 一个简单Demo
自定义 ViewGroup
public class MyViewGroup extends ViewGroup {
public MyViewGroup(Context context) {
super(context);
}
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int usedWidth = 0;
int maxHeight = 0;
int childState = 0;
//测量子布局
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
MarginLayoutParams layoutParams =
(MarginLayoutParams) childView.getLayoutParams();
measureChildWithMargins(childView, widthMeasureSpec, usedWidth,
heightMeasureSpec, 0);
usedWidth += layoutParams.leftMargin + layoutParams.rightMargin +
childView.getMeasuredWidth();
maxHeight = Math.max(maxHeight, layoutParams.topMargin +
layoutParams.bottomMargin + childView.getMeasuredHeight());
childState = combineMeasuredStates(childState, childView.getMeasuredState());
}
//统计子布局水平,记录尺寸值
usedWidth += getPaddingLeft() + getPaddingRight();
maxHeight += getPaddingTop() + getPaddingBottom();
setMeasuredDimension(
resolveSizeAndState(usedWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//父布局传递进来的位置信息
int parentLeft = getPaddingLeft();
int left = 0;
int top = 0;
int right = 0;
int bottom = 0;
//遍历子布局
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
MarginLayoutParams layoutParams =
(MarginLayoutParams) childView.getLayoutParams();
left = parentLeft + layoutParams.leftMargin;
right = left + childView.getMeasuredWidth();
top = getPaddingTop() + layoutParams.topMargin;
bottom = top + childView.getMeasuredHeight();
//子布局摆放
childView.layout(left, top, right, bottom);
//横向摆放
parentLeft += right;
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MyLayoutParam(getContext(), attrs);
}
//自定义LayoutParams
static class MyLayoutParam extends MarginLayoutParams {
public MyLayoutParam(Context c, AttributeSet attrs) {
super(c, attrs);
}
}
该 ViewGroup 重写了onMeasure() 和 onLayout() 方法:
- onMeasure() 测量子布局大小,并根据子布局测算结果来决定自己的尺寸
- onLayout() 摆放子布局位置
同时,当 layout 执行结束,清除 PFLAG_FORCE_LAYOUT 标记,该标记会影响 Measure 过程是否需要执行 onMeasure。
自定义View
public class MyView extends View {
public MyView(Context context) {
super(context);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int defaultSize = 100;
setMeasuredDimension(
resolveSize(defaultSize, widthMeasureSpec),
resolveSize(defaultSize, heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.GREEN);
}
}
该 View 重写了 onMeasure() 和 onLayout() 方法:
- onMeasure() 测量自身大小,并记录尺寸值
- onLayout() 什么都没做
MyViewGroup 添加子布局
<?xml version="1.0" encoding="utf-8"?>
<com.fish.myapplication.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/myviewgroup"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_gravity="center_vertical"
android:background="#000000"
android:paddingLeft="10dp"
tools:context=".MainActivity">
<com.fish.myapplication.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</com.fish.myapplication.MyView>
<Button
android:layout_marginLeft="10dp"
android:text="hello Button"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</Button>
</com.fish.myapplication.MyViewGroup>
MyViewGroup 里添加了 MyView、Button 两个控件,最终运行的效果如下:
可以看出,MyViewGroup 里子布局的是横向摆放的。我们重点关注 Layout 过程。实际上,MyViewGroup 里我们只重写了 onLayout() 方法,MyView 也是重写了 onLayout() 方法。接下来,分析 View Layout 过程。
四 Layout过程
与 Measure 过程类似,连接 ViewGroup onLayout() 和 View onLayout() 之间的桥梁是 View layout()。
View.java
public void layout(int l, int t, int r, int b) {
//PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT 在measure时候可能会设置
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
//记录当前的坐标值
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//新(父布局给的)的坐标值与当前坐标值不一致,则认为有改变
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//坐标改变或者是需要重新layout
//PFLAG_LAYOUT_REQUIRED 是Measure结束后设置的标记
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) ==
PFLAG_LAYOUT_REQUIRED) {
//调用onLayout方法,传入父布局传入的坐标
onLayout(changed, l, t, r, b);
......
//清空请求layout标记
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
//监听的onLayoutChange回调,通过addOnLayoutChangeListener设置
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(
this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
final boolean wasLayoutValid = isLayoutValid();
//清空强制布局标记,该标记在measure时判断是否需要onMeasure;
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
......
}
public static boolean isLayoutModeOptical(Object o) {
//设置了阴影,发光等属性
//只有ViewGroup有这属性
//设置 android:layoutMode="opticalBounds" 或者 android:layoutMode="clipBounds"
//则返回true,默认没设置以上属性
return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical();
}
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(xx)
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
可以看出,最终都调用了 setFrame() 方法。
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;
//记录PFLAG_DRAWN标记位
int drawn = mPrivateFlags & PFLAG_DRAWN;
//记录新、旧宽高
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
//新、旧宽高是否一样
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
//不一样,走inValidate,最终执行Draw流程
invalidate(sizeChanged);
//将新的坐标值记录
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
//设置坐标值给RenderNode
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
//标记已经layout过
mPrivateFlags |= PFLAG_HAS_BOUNDS;
if (sizeChanged) {
//调用sizeChange,在该方法里,我们已经能够拿到View宽、高值
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
......
}
return changed;
}
对于 Measure 过程在 onMeasure() 里记录了尺寸的值,而对于 Layout 过程则在 layout() 里记录了坐标值,具体来说是在 setFrame() 里,该方法有两个地方需要重点关注:
- 将新的坐标值记录到成员变量 mLeft、mTop、mRight、mBottom 里
- 将新的坐标值记录到 RenderNode 里,当调用 Draw 过程的时候,Canvas 绘制起点就是 RenderNode 里的位置
View.onLayout()
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
View.onLayout() 是空实现,从 layout() 和 onLayout() 声明可知,这两个方法都是可以被重写的,接下来看看 ViewGroup 是否重写了它们。
ViewGroup Layout过程
ViewGroup.java
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
//没有被延迟,或者动画没在改变坐标
if (mTransition != null) {
mTransition.layoutChange(this);
}
//父类方法,其实就是View.layout()
super.layout(l, t, r, b);
} else {
//被延迟,那么设置标记位,动画完成后根据标志位requestLayout,重新发起layout过程
mLayoutCalledWhileSuppressed = true;
}
}
ViewGroup.layout() 虽然重写了 layout(),但仅仅只是做了简单判断,最后还是调用了 View.layout()。
ViewGroup.onLayout()
ViewGroup.java
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
重写后将 onLayout 变为抽象方法,也就是说继承自 ViewGroup 的类必须重写 onLayout() 方法。我们以 FrameLayout 为例,分析其 onLayout() 做了什么。
FrameLayout.onLayout()
FrameLayout.java
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();
//前景padding,意思是子布局摆放的时候不要侵占该位置
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);
//GONE状态下无需layout
if (child.getVisibility() != GONE) {
//获取LayoutParams
final FrameLayout.LayoutParams lp =
(FrameLayout.LayoutParams) child.getLayoutParams();
//获取之前在Measure过程确定的测量值
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();
//水平反向的Gravity
final int absoluteGravity =
Gravity.getAbsoluteGravity(gravity, layoutDirection);
//垂直方向的Gravity
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
//若子布局水平居中,则它的水平方向起始点
//扣除父布局padding剩下的位置
//结合子布局宽度,使得子布局在剩下位置里居中
//再将子布局margin考虑进去
//从这里可以看出,若是xml里有居中,也有margin,先考虑居中,然后再考虑margin
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的坐标位置
//传递给child
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
FrameLayout 布局 Layout 的时候,起始坐标都是以 FrameLayout 为基准,并没有记录上一个子布局占了哪块位置,因此子布局的摆放位置可能会重叠,这也是 FrameLayout 布局特性的由来。而我们之前的 Demo 在水平方向上记录了上一个子布局的摆放位置,下一个摆放时只能在它之后,因此就形成了水平摆放的功能。由此可以知道,我们常说的某个子布局在父布局里的哪个位置,决定这个位置的即是 ViewGroup.onLayout()。
4.1 Layout调用关系
上面我们分析了 View.layout()、View.onLayout()、ViewGroup.layout()、ViewGroup.onLayout(),那么这四者是什么关系呢?
View.layout()
更新位置坐标,并根据条件调用 onLayout,同时将摆放位置坐标记录到成员变量里并给 RenderNode 设值
View.onLayout()
空实现
ViewGroup.layout()
调用 View.layout()
ViewGroup.onLayout()
抽象方法,子类必须重写。子类重写时候需要为每一个子布局计算出摆放位置,并传递给子布局
View/ViewGroup 子类需要重写哪些方法:
- 继承自 ViewGroup 必须重写 onLayout(),为子布局计算位置坐标
- 继承自 View 无需重写 layout() 和 onLayout(),因为它已经没有子布局可以摆放
4.2 Layout的承上启下作用
通过以上的分析,我们发现 Measure 过程和 Layout 过程里定义的方法比较类似:
measure() <-----> layout()
onMeasure() <-----> onLayout()
他们的实现流程比较类似:measure()、layout() 一般不需要我们重写,measure() 里调用 onMeasure(),layout() 里调用 onLayout()。
ViewGroup.onMeasure() 里遍历子布局,并测量每个子布局,最后将结果汇总,设置自己测量的尺寸;onLayout() 里遍历子布局,并设置每个子布局的坐标。
View.onMeasure() 则测量自身,并存储测量尺寸;onLayout() 不需要做什么。
承上
Measure 过程虽然比 Layout 过程复杂,但仔细分析后就会发现其本质就是为了设置两个成员变量:
- 设置 mMeasuredWidth 和 mMeasuredHeight
而 Layout 过程虽然比较简单,其本质是为了设置坐标值
- 设置 mLeft、mRight、mTop、mBottom 这四个值确定一个矩形区域
- mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom) 给 RnederNode 设置坐标
将 Measure 设置的变量和 Layout 设置的变量联系起来:
- mRight、mBottom 是根据 mLeft、mTop 结合 mMeasuredWidth、mMeasuredHeight 计算而得的
此外,Measure 过程通过设置 PFLAG_LAYOUT_REQUIRED 标记来告诉需要进行 onLayout,而 Layout 过程通过清除 PFLAG_FORCE_LAYOUT 来告诉 Measure 过程不需要执行 onMeasure 了。
以上就是 Layout 的承上作用。
启下
我们知道 View 的绘制需要依靠 Canvas 绘制,而 Canvas 是有作用区域限制的。例如我们使用:
canvas.drawColor(Color.GREEN);
Cavas 绘制的起点是哪呢?对于硬件绘制加速来说是通过 Layout 过程中设置的 RenderNode 坐标,而对于软件绘制来说:
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
canvas.translate(mLeft - sx, mTop - sy);
}
关于硬件绘制加速/软件绘制后续文章会分析。
这就是 Layout 的启下作用,以上即是 Measure、Layout、Draw 三者的内在联系。当然 Layout 的"承上"还需要考虑 margin、gravity 等参数的影响。具体用法参见最开始的 Demo。
五 经典问题
getMeasuredWidth() / getMeasuredHeight 与 getWidth/getHeight 区别
我们以获取 width 为例,分别来看看其方法:
View.java
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getWidth() {
return mRight - mLeft;
}
- getMeasuredWidth():获取测量的宽,属于"临时值",measure 后才能获取
- getWidth():获取 View 真实的宽,layout 后才能获取
在 Layout 过程之前,getWidth() 默认为 0
何时可以获取真实的宽、高?
- 重写 View.onSizeChanged() 方法获取
- 注册 View.addOnLayoutChangeListener(),在 onLayoutChange() 里获取
- 重写 View.onLayout() 方法获取
下篇将分析 Draw() 过程,如有问题欢迎留言评论交流。