View 的绘制 - Layout 流程
Layout 作用就是 ViewGroup 用来确定子元素的位置。当 ViewGroup 的位置确定后,他会在 onLayout() 中遍历所有子元素并调用其 layout() 方法,在 layout() 方法中执行我们熟悉的 onLayout() 方法。
子 View 具体的 layout 的位置都是相对于父容器而言的, view 的 layout 过程同 Measure 同理,也是从顶级 View 开始,递归的完成整个控件树的布局操作
经过前面的测量,控件树中的控件对于自己的尺寸显然已经了然于胸。而且父控件对于子控件的位置也有了眉目,所以经过测量过程后,布局阶段会把测量结果转化为控件的实际位置与尺寸。控件的实际位置与尺寸由 View 的 mLeft , mTop , mRight , mBottom 等 4 个成员变量存储的坐标值来表示。
需要注意的是: View的 mLeft , mTop , mRight , mBottom 这些坐标值是以父控件左上角为坐标原点进行计算的。倘若需要获取控件在窗口坐标系中的位置可以使用View.GetLocationWindow()或者是View.getRawX()/Y()。
先看 ViewRootImp 的 performLayout() 方法
ViewRootImpl #performLayout()
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth ,int desiredWindowHeight) {
...
final View host = mView;
host.layout(0, 0 , host.getMeasuredWidth(), host.getMeasuredHeight());
...
}
host 可以是一个 View ,也可以是一个 ViewGroup ,尽管 ViewGroup 也重写了 layout 方法,但是本质上还是通过super.layout(),调用 View 的 layout 方法
ViewGroup # layout()
public final void layout(int l, int t , int r , int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
//如果无动画,或者动画未运行
super.layout(l, t , r , b);
} else {
//等待动画完成时再调用requestLayout()
mLayoutCalledWhileSuppressed = true;
}
}
所以我们直接看 View 的 layout() 即可。
View # layout()
public void layout(int l, int t , int r , int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//如果布局有变化,通过 setFrame 重新布局
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t , r , b) : setFrame(l, t , r , b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//如果这是一个 ViewGroup ,还会遍历子 View 的 layout() 方法
//如果是普通 View ,通知具体实现类布局变更通知
onLayout(changed, l , t , r , b);
//清除 PFLAG_LAYOUT_REQUIRED 标记
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
...
//布局监听通知
}
//清除 PFLAG_FORCE_LAYOUT 标记
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
}
- 通过 setFrame() 将 l , t , r , b 分别设置到 mLeft , mTop , mRight ,和 mButton ,这样就可以确定子 View 在父容器上的位置了,也就是说这四个位置是相对于父容器的
- 调用 onLayout 方法,具体实现类接收到布局变更通知,如果此类是 ViewGoup ,还会遍历子 View 的 layout 方法,使其更新布局.
onLayout()
对于普通 View , onLayout 方法是一个空实现,主要是具体实现类重写该方法后能接受到布局坐标更新信息
protected void onLayout(boolean changed, int l , int t , int r , int b) {}
对于 ViewGroup 来说,和 measure 一样,不同的类有它不同的布局特性,在 ViewGroup 中 onLayout 方法中是 abstract 的,具体类必须重写该方法,以便接收布局坐标更新信息后,处理自己的子 View 的坐标信息。
protected abstract void onLayout(boolean changed, int l , int t , int r , int b);
还是以 LinearLayout 为例,看看 onLayout() 的实现
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);
}
}
这里面和 measure() 类似,这里选择 layoutVertical() 讲解
LinearLayout # layoutVertical()
void layoutVertical(int left, int top , int right , int bottom) {
...
final int count = getVirtualChildCount();
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();
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
...
childTop += lp.topMargin;
//遍历所有子元素并执行setChildFrame()来确定子元素的指定位置
setChildFrame(child, childLeft , childTop + getLocationOffset(child), childWidth , childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
private void setChildFrame(View child, int left , int top , int width , int height) {
child.layout(left, top , left + width, top + height);
}
- 遍历所有子元素并调用 setChildFrame() 来为子元素指定位置, setChildFrame() 内部调用子元素的 layout() 方法,
- childTop位置会逐渐增大,这就意味着后面的元素会放在靠下的位置,符合竖直方向的 LinearLayout 的特点, 这样父元素在 layout 完成自己的定位后,然后通过 onLayout() 调用子元素的 layout 方法,子元素又会通过 layout 方法定位自己的位置。这样一层层传递下来就完成了整个 View 树的 layout 过程。
这样 layout 流程就结束了。补充一份流程图吧
getMeasuredWidth()和 getWidth() 的区别
getMeasuredWidth(): 得到的是测量宽度。 形成与measure()过程中 getWidth(): 得到的是最终宽度。形成与 layout() 过程中 所以 getMeasuredWidth() 赋值时机更早一些。但是在 measure() 过程中, View 可能需要多次才能确定自己的测量高度。就导致第一次 getMeasureWidth() 的值可能和 getWidth() 不一直。但最终 measure() 之后,两个值还是相等的。
在 View 的默认实现中。getWidht() == getMeasuredWidth()
小结
- measure确定的是控件的尺寸,并在一定程度上确定了子控件的位置。而布局则是针对测量结果来实施,并最终确定子控件的位置。
- measure结果对布局过程没有约束力。虽说子控件在 onMeasure() 方法中计算出了自己应有的尺寸,但是由于 layout() 方法是由父控件调用,因此控件的位置尺寸的最终决定权掌握在父控件手中,测量结果仅仅只是一个参考。
- 因为 measure 过程是后根遍历(DecorView最后setMeasureDiemension()),所以子控件的测量结果影响父控件的测量结果。
- 而 Layout 过程是先根遍历(layout()一开始就调用 setFrame() 完成 DecorView 的布局),所以父控件的布局结果会影响子控件的布局结果。
- 完成 performLayout() 后,空间树的所有控件都已经确定了其最终位置,就剩下 Draw 绘制了。
相关文章:
View 的绘制 - 概览
View 的绘制 - Measure 流程
View 的绘制 - Layout 流程
View 的绘制 - Draw 流程, invalidate 的流程 以及 requestLayout 流程
搬运地址:
Android 开发艺术探索
既已览卷至此,何不品评一二: