自定义View原理篇(2)- layout过程
1. 简介
-
View
的绘制过程分为三部分:measure
、layout
、draw
。
measure
用来测量View的宽和高。
layout
用来计算View的位置。
draw
用来绘制View。
-
经过
measure
之后就进入了layout
过程,measure
过程可以查看这篇文章:自定义View原理篇(1)- measure过程。 -
本章主要对
layout
过程进行详细的分析。 -
本文源码基于android 27。
2. layout的始点
跟measure
一样,layout
也是始于ViewRootImpl
的performTraversals()
:
2.1 ViewRootImpl的performTraversals
private void performTraversals() {
//...
//获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.widthhe和lp.height表示DecorView根布局宽和高
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//执行测量
//...
performLayout(lp, mWidth, mHeight);//执行布局
//...
performDraw();//执行绘制
//...
}
再来看看performLayout()
:
2.2 ViewRootImpl的performLayout
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
//...
//调用layout
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
//...
}
这里的host
就是DecorView
,如果不知道DecorView
,可以看看这篇文章:从setContentView揭开DecorView。
layout()
方法传入的0,0,host.getMeasuredWidth,host.getMeasuredHeight
就是一个View
的上下左右四个位置,可以看到,DecorView
都是从左上角位置(0,0)开始进行布局的,其宽高则为测量宽高。
下面重点来分析Layout
过程
3.layout过程分析
layout
用来计算View
的位置,即确定View
的Left
、Top
、Right
和 Bottom
这四个顶点的位置。如下图所示:
同样,layout
过程根据View
的类型也可以分为两种情况:
- 计算单一
View
位置时,只需计算其自身即可;- 计算
ViewGroup
位置时,需要计算ViewGroup
自身的位置以及其包含的子View
在ViewGroup
中的位置。
我们对这两种情况分别进行分析。
3.1 单一View的layout过程
单一View
的layout
过程是从View
的layout()
方法开始:
3.1.1 View的layout
public void layout(int l, int t, int r, int b) {
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;
//isLayoutModeOptical(mParent);//判断该view布局模式是否有一些特殊的边界
//有特殊边界则调用setOpticalFrame(l, t, r, b)
//无特殊边界则调用setFrame(l, t, r, b)
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 若View的大小或位置有变化
// 会重新确定该View所有的子View在父容器的位置,通过调用onLayout()来实现。
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
// ...
}
我们接下来分别看看setOpticalFrame()
,setFrame()
,onLayout()
这三个方法。
3.1.2 View的setOpticalFrame
先来看看setOpticalFrame()
:
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);
}
setOpticalFrame()
里面最终还是会调用到setFrame()
3.1.3 View的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;
//...
//赋值,保存View的四个位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
//...
}
return changed;
}
可以看到,View
的四个位置就在这里给确定下来了。
3.1.4 View的onLayout
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
onLayout()
在View
中就是个空实现,由于单一的View
没有子View
,因此不需要确定子View
的布局,所以onLayout()
也无需实现。
3.1.5 单一View的layout过程流程图
所以,单一View
的Layout
还是很简单的,来张流程图简单总结一下:
3.2 ViewGroup的layout过程
ViewGroup
的layout
过程除了需要计算ViewGroup
自身的位置外,还需要计算其包含的子View
在ViewGroup
中的位置。
计算ViewGroup
自身的位置实际上跟单一View
的过程是一样的,这里就不重述;唯一不同的就是单一View
的onLayout()
实现为空,ViewGroup
需要具体实现onLayout()
方法。
onLayout()
方法在ViewGroup
是一个抽象方法,需要其子类去重写,因为确定子View
的位置与具体的布局有关,所以ViewGroup
中没有办法统一实现。
我们在这里看看LinearLayout
的onLayout()
实现:
3.2.1 LinearLayout的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);
}
}
LinearLayout
会区分方向来进行不同的layout
方法,我们主要看下竖向的layoutVertical()
,横向的原理差不多这里就不看了。
3.2.2 LinearLayout的layoutVertical
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;//记录子View的Top位置
int childLeft;//记录子View的Left位置
// ...
// 子View的数量
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) {
//获取子View的测量宽 / 高值
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
//...
//childTop加上子View的topMargin的值
childTop += lp.topMargin;
//调用setChildFrame(),这里确定子View的位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
//childTop加上子View的高度、bottomMargin等值
//因此后面的子View就顺延往下放,这符合垂直方向的LinearLayout的特性
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
//...
}
}
}
layoutVertical()
通过遍历子View
,并调用setChildFrame()
方法来确定子View
的位置。
3.2.3 LinearLayout的setChildFrame
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
setChildFrame()
中就是调用子View
的layout()
方法来来确定子View
的位置。
3.2.4 ViewGroup的layout过程流程图
image4. 自定义View
4.1 自定义单一view
自定义单一view
一般无需重写onLayout()
方法。
4.2 自定义ViewGroup
由于ViewGroup
没实现onLayout()
,所以自定义ViewGroup
需要重写onLayout()
方法。这里给个简单的模板:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//遍历子View
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
//获取当前子View宽/高值
int width = child.getMeasuredWidth();
int height = child.getMeasuredHeight();
//计算当前子View的四个位置值
int mLeft = l + 100 * i;//具体逻辑请自行计算
int mTop = t + 100 * i;//具体逻辑请自行计算
int mRight = mLeft + width;//具体逻辑请自行计算
int mBottom = mTop + height;//具体逻辑请自行计算
//根据上面的计算结果设置子View的4个顶点
child.layout(mLeft, mTop, mRight, mBottom);
}
}
5. 其他
5.1 getWidth()与getMeasuredWidth()区别,getHeight()与getMeasuredHeight()同理
getWidth()
:获得View
最终的宽;getMeasuredWidth()
:获得View
测量的宽;
一般情况下,这两者获得的值是一样的,我们可以来看看他们的代码实现:
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getWidth() {
return mRight - mLeft;
}
结合源码中的各种赋值过程,getWidth()
的值就是测量出的宽度。
当然,我们可以通过重写layout()
来修改最终的宽度,但一般这没有任何的实际意义,如:
@Override
public void layout(int l, int t, int r, int b) {
// 修改传入的位置参数,这样一来,getWidth()获得的宽度就比测量出来的宽度大上100了
super.layout(l, t, r + 100, b + 100);
}