一、layout 过程
类似 measure 过程,layout 过程根据 View 的类型也分为 2 种情况:
1.1 View 的 layout 过程
layout() 的作用是 ViewGroup 用来确定子元素的位置,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素并调用其 layout() 方法,在 layout() 方法中 onLayout() 方法又会被调用。layout 过程和 measure 过程相比就简单多了,layout() 方法确定 View 本身的位置,而 onLayout() 方法则会确定所有子元素的位置。先来看看 View 的 layout 方法:
/**
* 源码分析:layout()
* 作用:确定View本身的位置,即设置View本身的四个顶点位置
*/
public void layout(int l, int t, int r, int b) {
// 当前视图的四个顶点
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
// 1. 确定View的位置:setFrame() / setOpticalFrame()
// 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化 & 返回
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 2. 若视图的大小 & 位置发生变化
// 会重新确定该View所有的子View在父容器的位置:onLayout()
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
// 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现->>分析3
// 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现
...
}
layout 方法的大致流程如下:首先会通过 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop、mBottom 这四个值,View 的四个顶点一旦确定,那么 View 在父容器中的位置也就确定了。接着会调用 onLayout 方法,这个方法的用途是确定子元素的位置,由于单一 View 是没有子元素的,所以 View 的 onLayout() 是一个空实现。
1.2 ViewGroup 的 layout 过程
而 ViewGroup 的 layout 过程确定位置与具体的布局有关,所以在 ViewGroup 中是一个抽象方法,需要重写实现。
根据自身需求的布局逻辑复写 onLayout(),步骤分为 3 步:
- 遍历所有子 View
- 根据自身需求计算当前子 View 的四个位置值(需自身实现)
- 根据上述 4 个位置的计算值,设置子 View 的 4 个顶点:调用子 View 的 layout 方法,即确定了子 View 在父容器里的位置
/**
* 作用:计算该ViewGroup包含所有的子View在父容器的位置()
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 参数说明
// changed 当前View的大小和位置改变了
// left 左部位置 top 顶部位置 right 右部位置 bottom 底部位置
// 1. 遍历子View:循环所有子View
for (int i=0; i<getChildCount(); i++) {
View child = getChildAt(i);
// 2. 计算当前子View的四个位置值
// 2.1 位置的计算逻辑需自己实现,也是自定义View的关键
calculate();
// 2.2 对计算后的位置值进行赋值
int mLeft = Left
int mTop = Top
int mRight = Right
int mBottom = Bottom
// 3. 根据上述4个位置的计算值设置子View的4个顶点:调用子view的layout() & 传递计算过的参数
// 即确定了子View在父容器的位置
child.layout(mLeft, mTop, mRight, mBottom);
// 该过程类似于单一View的layout过程中的layout()和onLayout()
}
}
1.3 ViewGroup 子类(LinearLayout)的 layout 过程分析
我们直接看它的 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 的方向(vertical、horizontal)进入不同的布局过程,这里我们只选垂直方向的布局过程,即layoutVertical()。
void layoutVertical(int left, int top, int right, int bottom) {
// 子View的数量
final int count = getVirtualChildCount();
// 1. 遍历子View
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
// 2. 计算子View的测量宽 / 高值
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
// 3. 确定自身子View的位置
// 即:递归调用子View的setChildFrame(),实际上是调用了子View的layout() ->>分析2
setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
// childTop逐渐增大,即后面的子元素会被放置在靠下的位置
// 这符合垂直方向的LinearLayout的特性
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
private void setChildFrame( View child, int left, int top, int width, int height){
// setChildFrame()仅仅只是调用了子View的layout()而已
child.layout(left, top, left ++ width, top + height);
// 在子View的layout()又通过调用setFrame()确定View的四个顶点
// 即确定了子View的位置
// 如此不断循环确定所有子View的位置,最终确定ViewGroup的位置
}
这里分析一下 layoutVertical 的代码逻辑,可以看到此方法会遍历所有子元素并调用 setChildFrame() 方法来为子元素指定对应的位置,其中 childTop 会逐渐增大,这就意味着后面的子元素会被放置在考下的位置,这刚好符合竖直方向的 LinearLayout 的特性。至于 setChildFrame,它仅仅是调用子元素的 layout 方法而已,这样父元素在 layout 方法中完成自己的定位以后,就通过 onLayout() 方法去调用子元素的 layout() 方法,子元素又通过自己的 layout() 方法来确定自己的位置,这样一层一层的传递下去就完成了整个 View 树的 layout 过程。
我们可以看到,setChildFrame 中的 width 和 height 实际上就是子元素的测量宽/高,而在 layout() 方法中会通过 setFrame() 去设置子元素的四个顶点的位置,这样一来子元素的位置就确定了。
二、getMeasureWidth 和 getWidth 区别
首先明确定义:
- getWidth() / getHeight():获得View最终的宽 / 高
- getMeasuredWidth() / getMeasuredHeight():获得 View测量的宽 / 高
// 获得View测量的宽 / 高
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
// measure过程中返回的mMeasuredWidth
}
// 获得View最终的宽 / 高
public final int getWidth() {
return mRight - mLeft;
// View最终的宽 = 子View的右边界 - 子view的左边界。
}
从 getWidth 和 getHeight 的源码再结合 mLeft、mRight、mTop、mBottom 这四个变量的赋值过程来看,getWidth、getHeight 方法的返回值刚好就是 View 的测量宽/高。因此,在 View 的默认实现中,View 的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于 View 的 measure 过程,而最终宽/高形成于 View 的 layout 过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。
因此在日常开发中,我们可以认为 View 的测量宽/高就等于最终宽/高,但是的确存在某些特殊情况会导致两者不一致,比如重写 View 的 layout 方法:
@Override
public void layout( int l , int t, int r , int b){
// 改变传入的顶点位置参数
super.layout(l,t,r+100,b+100);
}
如此一来,在任何情况下 View 的最终宽高(getWidth()、getHight())总是比测量宽高(getMeasuredWidth()、getMeasuredHeight())大 100px,虽然这样做会导致 View 的显示不正常并且没有实际意义,但证明了测量宽高的确是可以不等于最终宽高的。
另一种情况是在某些情况下,View 需要多次 measure 才能确定自己的测量宽高,在前几次的测量过程中,其得出的测量宽高有可能和最终宽高不一致,但最终来说,测量宽高还是和最终宽高相同。