一、前言
在前面的博文中我们已经学习了View的Measure过程,那么接下来就来学习一下View绘制的第二个步骤,即Layout。Layout的作用就是确定View在ViewGroup中的位置,ViewRoot的performTraversals()方法在measure结束后,会调用View的layout()方法来执行此过程,那么首先我们来看一下在这个layout()方法中到底做了哪些事情?
二、layout()过程
View的layout()方法的原型如下:
protected void layout(int mLeft, int mTop, int mRight, int mBottom);
layout()方法接收4个参数,分别代表这View相对于父视图的左、上、右、下位置,下面我们看看layout()方法中到底做了些什么?
/**
* Assign a size and position to a view and all of its
* descendants
*
* <p>This is the second phase of the layout mechanism.
* (The first is measuring). In this phase, each parent calls
* layout on all of its children to position them.
* This is typically done using the child measurements
* that were stored in the measure pass().
*
* Derived classes with children should override
* onLayout. In that method, they should
* call layout on each of their their children.
*
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
public final void layout(int l, int t, int r, int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~LAYOUT_REQUIRED;
if (mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>) 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);
}
}
}
mPrivateFlags &= ~FORCE_LAYOUT;
}
从上面代码中可以看到:
- layout()方法不可重写,因为其被final修饰,这意味着android不允许我们修改layout()的过程;
通过调用setFrame()方法判断当前View的大小是否发生过变化,以确定有没有必要对当前视图进行重绘,并且将(l, t, r, b)四个参数保存在View内部的变量(mLeft, mTop, mRight, mBottom)中。保存完变量前,会对比这些参数是否和原来的参数相同,如果相同,则什么都不做,如果不同则进行重新赋值,并在赋值前给mPrivateFlags添加DRAWN标志,同时调用invalidate()通知系统原来占用的位置需要重绘,setFrame()部分的代码如下:
protected boolean setFrame(int left, int top, int right, int bottom) { boolean changed = false; if (DBG) { Log.d("View", this + " View.setFrame(" + left + "," + top + "," + right + "," + bottom + ")"); } if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { changed = true; // Remember our drawn bit int drawn = mPrivateFlags & DRAWN; // Invalidate our old position invalidate(); int oldWidth = mRight - mLeft; int oldHeight = mBottom - mTop; mLeft = left; mTop = top; mRight = right; mBottom = bottom; mPrivateFlags |= HAS_BOUNDS; int newWidth = right - left; int newHeight = bottom - top; if (newWidth != oldWidth || newHeight != oldHeight) { onSizeChanged(newWidth, newHeight, oldWidth, oldHeight); } if ((mViewFlags & VISIBILITY_MASK) == VISIBLE) { // If we are visible, force the DRAWN bit to on so that // this invalidate will go through (at least to our parent). // This is because someone may have invalidated this view // before this call to setFrame came in, therby clearing // the DRAWN bit. mPrivateFlags |= DRAWN; invalidate(); } // Reset drawn bit to original value (invalidate turns it off) mPrivateFlags |= drawn; mBackgroundSizeChanged = true; } return changed; }
然后会调用View的onLayout()方法,继续追踪View的onLayout()方法,你会发现其实View的onLayout()方法是空的,其方法原型为:
protected void onLayout(boolean changed, int left, int top, int right, int bottom);
因为onLayout()方法是为了确定View在父View中的位置,因此这个操作应该是由父View来完成的,即父View决定子View的显示位置。View系统提供这个方法的目的是为了使系统包含的子视图的父视图能够在onLayout()方法对子视图进行位置分配。
既然如此,我们去ViewGroup中看看它的onLayout()方法都做了什么,可以发现,ViewGroup中的onLayout()方法其实是一个抽象方法,其方法原型如下:
@override
protected abstract void onLayout(boolean changed, int l , int t, int r, int b);
这也就意味着所有继承自ViewGroup的类都需要重写onLayout()方法,包括系统提供的LinearLayout、RelativeLayout等。这里我们不妨看下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的onLayout()方法中,通过判断orientation,来执行不同的操作,至于layoutVertical(l, t, r, b)和layoutHorizontal(l, t, r, b)的代码内容有兴趣的可以取参考相关源码。
其实分析源码不仅可以有助于我们理解底层的实现过程,还可以对我们自定义相关内容,比如自定义View和自定义ViewGroup很有帮助。
这里也给出一个自定义布局,借此来深刻理解onLayout()的过程,这里参考了郭霖大神博客的内容自定义Layout-郭霖。
public class SimpleLayout extends ViewGroup {
public SimpleLayout(Context context, AtrributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() > 0) {
View childView = getChildAt(0);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
}
}
在onLayout()过程结束后,就可以调用getWidth()方法和getHeight()方法来获取视图的宽高了,不知道大家是否注意到前面在onMeasure()结束后还有个方法叫getMeasuredWidth()和getMeasuredHeight()方法,那么这两者到底有什么区别和联系呢?
- 首先,getMeasuredWidth()方法在onMeasure()过程结束就可调用,而getWidth()方法在onLayout()过程结束后才可调用;
- 另外,getMeasuredWidth()方法得到的值是通过setMeasuredDimesion()方法设置的,而getWidth()方法得到的值是通过视图右边的左边减去左边的坐标计算出来的;
- 在simpleLayout中的onLayout()方法中,我们给子视图的layout()方法传入的4个参数是0,0,childView.getMeasuredWidth()和childView.getMeasuredHeight(),这样getWidth()方法得到的值就是childView.getMeasuredHeight() - 0 = childView.getMeasuredHeight(),所以通常情况下getMeasuredWidth()的值和getWidth()的值是相等的,getMeasuredHeight()的值和getHeight()的值是相等的。
三、总结
View的layout过程一般在进行自定义View的时候是不需要我们关心的,我们只需要管好measure和draw过程即可,而对于ViewGroup,则不得不重视layout()过程,管理好子View的layout布局,所以一般在自定义ViewGroup都需要重写onLayout()方法,从而对子View进行布局。
View的绘制过程还是有点复杂的,通过阅读相关源码对理解View的绘制过程很有帮助,目前View的绘制过程已经讲解了两部分,下面一篇博文将讲解View的onDraw()过程,这部分是View真正显示在屏幕上的关键部分,需要我们去认真对待。
大家有时间可以去看看大神们的博客,对于你学习Android很有帮助,我写的这些很多都参考了他们的博客,我目前主要关注两位大神的博客:郭霖大神的博客和鸿洋大神的博客,他们的博客都写的非常好,强烈推荐给大家!