前言
本篇文章来说测量、布局和绘制的三大步骤中的第二步:布局,简单来说经过前面介绍的测量流程后,每个View它的大小就基本确定了,但是每个View需要如何放置,这就是布局的作用了。
正文
由于布局的过程比测量要简单一些,这里就直接看源码分析一遍流程。
ViewRootImpl开始
和前面文章里介绍的测量流程一样,在一个页面你一旦调用requestLayout()方法后,就会执行ViewRootImpl中的requestLayout()方法,然后执行View的绘制。下面代码简单介绍一下流程,可以不用细看:
//ViewRootImpl中的requestLayout方法
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
//执行遍历
scheduleTraversals();
}
}
复制代码
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//利用handler,发送绘制Runnable
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
复制代码
//真正的绘制方法
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
复制代码
void doTraversal() {
if (mTraversalScheduled) {
...
//执行遍历
performTraversals();
...
}
}
复制代码
private void performTraversals() {
...
//这里的host就是这个View链的根View
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}
复制代码
到这里就把布局这个操作给传递到了View上。
layout过程
Layout的作用就是ViewGroup来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中遍历所有子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。
比如上面的流程从ViewRootImapl到View的布局事件调用,那个host一般就是mDecorView,即是一个FrameLayout,所以mDecorView的位置是确定的。
确定mDecorView的方法是调用layout方法,所以我们先分析一下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方法,设定View的4个顶点位置
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
...
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//抽象方法,用户可以自己定义
onLayout(changed, l, t, r, b);
...
}
复制代码
这里主要可以确定2点:
-
View是通过setFrame方法来真正确定4个顶点的位置。
-
在layout方法内会调用onLayout方法来让用户自定义ViewGroup子View的布局方式。
所以当mDecorView是FrameLayout时,就会调用其对应的FrameLayout的onLayout方法。
ViewGroup的onLayout方法
View和ViewGroup都没有真正实现onLayout方法,这个需要ViewGroup去实现,我们就看一个简单的例子,看一下线性布局的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);
}
}
复制代码
//竖向排列,这里的4个参数就是LinearLayout本身的位置
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
//计算具体的padding
...
//遍历所有View
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();
//结合LinearLayout的LayoutParams计算
...
//得到该View的4个顶点数据
childTop += lp.topMargin;
//调用子View的方法
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
复制代码
通过上面的方法,我们就可以按照自己的需求设定子View的位置,看一下这个setChildFrame方法:
//就是调用子View的layout方法
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
复制代码
果不其然,这里又是调用其layout方法。
getWidth和getMeasureWidth的区别
这是一个很经典的问题,这2者的区别,其实看了布局的源码就非常容易理解了。
其中getWidth的方法是:
public final int getWidth() {
return mRight - mLeft;
}
复制代码
而getMeasureWidth方法是:
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
复制代码
后者是测量宽高,我们很好理解,而前者其实正常情况下也就是测量宽高,看源码可知传入给layout函数的几个参数就是测量宽高。
但是也没有让这俩者不一致呢,是有办法的,但是不建议,也就是重写View的layout方法:
public void layout(int l,int t,int r,int b){
super.layout(l,t,r + 100,b)
}
复制代码
这里就会导致原来代码传入的测量宽高会不一样,但是一般不会这么写。
总结
View的布局还是蛮简单的,一句话总结一下就是调用layout方法传入的参数用来确定自己的位置,setFrame方法是真正确定View的4个顶点,然后重新onLayout方法来确定子View的位置。