概要
Activity
界面的根布局其实是一个名为 DecorView
的 FrameLayout
, 在创建 DecorView
的时候,会相应的创建一个 ViewRootImpl
类来控制 UI 的绘制,其实也就是控制 DecorView
。绘制的过程分为了 measure
, layout
, draw
三个过程。 以下分别对 View
和 ViewGroup
三个过程进行说明。
View 绘制
measure
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
两个参数 widthMeasureSpec
,heightMeasureSpec
是 parent
提供过来的,parent
用这两个参数告诉 View
,我希望你的宽高是多大。这两个 参数可以由 View
中的一个静态类 MeasureSpec
来解析,MeasureSpec
是由 size
和 mode
组成,可由下面方法获得。
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
size
很好理解 ,就是 parent
给 Child View
的大小。 而 mode
呢,有三种形式。
UNSPECIFIED
:parent
对Child View
没有任何限制,Child View
想多大就多大。EXACTLY
:parent
对child view
说,我只希望是这个固定的大小,也就是上面代码解析出来的specSize
。AT_MOST
:parent
告诉child view
,你最大绘制范围就是specSize
。
那么 View.onMeasure()
就是利用 paren
t 传过来的关于宽高限制的参数来决定自己的宽度 ,然而可以看到这个方法是 final
,不允许重写的。 当然,实际的测量是在 View.onMeasure()
方法中。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
系统调用了 setMeasuredDimension()
,这是测量之后必须要调用的方法,它用来存储的测量的宽高,也就是 getMeasuredWidth()
和 getMeasuredHeight()
的值。
系统的实现中还用到了 getSuggestedMinimumWidth()
和 getSuggestedMinimumHeight()
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
为何要调用这个?有时候,我可能设置了 android:backgound
或者 android:minWidth
, android:minHeight
,所以我们自定义的 View
不应该小于这个值吧。
当然我们还想知道,系统的默认实现中设置的测量宽高是什么呢,这个是要看 getDefaultSize()
方法了。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
如果解析出来的 mode
为 UNSPECIFIED
,也就说 parent
对 Child View
没有限制,那就用 getSuggestedMinimumXXX()
的值。
如果解析出来的 mode
为 AT_MOST
或 EXACTLY
, 就用 parent
提供过来的大小。 那也就是说,如果调用系统的方法super.onMeasure()
进行测试,默认就是填充整个父布局。 我们自定义 View
或 ViewGroup
的时候,有时候并不想这样,因此需要自己测量,最后调用 setMeasuredDimension()
方法。
draw
@CallSuper
public void draw(Canvas canvas) {
// ...
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// Step 2, save the canvas' layers
// ...
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
// ...
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
View.draw()
方法并不是 final
的,因此我们可以重写它,不过一定记得调用 super.draw(canvas)
, 因为系统帮我们绘制了 background
,scrollBar
,而且最重要的是,如果是 ViewGroup
的话,会调用 dispatchDraw()
来让 Child View
进行绘制。这也正是 @CallSuper
注解的意思。
从代码中的 step 3
可以看出,onDraw()
才是绘制自己的内容区域。而 View
的 onDraw()
方法是一个空方法
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {
}
layout
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
}
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);
}
}
View
的 layout()
是 parent
(ViewGroup
) 调用,用来确定 Child View
的位置,顺便也确定了实际大小。如果是自定义 View 的话,完全没必要在意这个方法,当然也不用在意在 View.layout()
调用的 View.onLayout()
方法。而在实际中,只需要注意 onSizeChange()
方法,这个方法告诉你,你的 View
的大小发生了改变。而 onSizeChange()
就是在 setOpticalFrame()
或者 setFrame()
中调用的,而这个方法是 layout
过程的关键所在 ,它们决定了 View
的实际宽高,我们取其中一个方法看看样子
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;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
// 检测大小是否已经改变
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
// 更新坐标值
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
if (sizeChanged) {
// 调用了 onSizeChanged()
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
}
return changed;
}
setFrame()
检测了 View
的大小是否改变了,然后更新了坐标值。 坐标值的改变也意味着 getWidth()
和 getHeight()
的值可以确定了。
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
如果大小改变了,就调用 sizeChange()
方法
private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
// ...
}
在 sizeChange()
方法中首先就调用了 onSizeChanged()
方法。
那么我们梳理下 layout
过程,layout()
-> onSizeChanged()
-> onLayout()
。
ViewGroup 绘制
measure
View
的 measure()
方法为 final
类型,因此子类不能复写这个方法,只能复写 onMeasure()
方法,那么 ViewGroup
作为 View
的子类,也不例外。
我们还可以知道,onMeasure()
是要完成自身的测量,并通过调用 setMeasuredDimension()
方法来存储测量的结果。 然而对于 ViewGroup
来说,它需要提供一个 MeasureSpec
给它的每个 Child View
用来完成测量,然后根据 Child View
测量结果来决定自身的测量值。
ViewGroup
用来测量 Child View
的方法有三个,分别是 measureChild()
, measureChildren()
, measureChildWithMargins()
。
measureChild()
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
首先整体观看 measureChild()
方法,它只考虑了 padding
,而并没有考虑经常会出现的 margin
,因此,如果自定义 View
的时候没有 margin
可以考虑用这个方法完成 Child View
的测量。
measureChild()
是调用 getChildMeasureSpec()
来为 Child View
生成相应的 MeasureSpec
,它的第二个参数虽然名为 padding
,但是在实际使用的时候,它其实在更严格的意义上讲,是指已经使用的长度或宽度。
mesureChildren()
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
measureChildren()
在原理还是调用了 measureChild()
方法,只是有一点需要注意,如果 Child View
的可见性为 GONE
,是不会测量的。
measureChildWithMargins()
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
measureChildWithMargins()
这个方法就比较全面,首先考虑了 padding
和 margin
,其次它需要传入两个参数,分为代表已经使用过的宽度和高度,这样我们就很方便计算并排的 Child View
的宽高。
FrameLayout.onMeasure()
说了这么多很空泛,举个例子。 我们知道,根布局其实就是一个名为 DecorView
的 FrameLayout
,它的 onMeasure()
方法的部分源码如下
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
//...
}
}
// ...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
// ...
}
在第 17
行,它使用的是 measureChildWithMargins()
方法来完成 Child View
的测量,由于 FrameLayout
设计为层层覆盖的特性,因此 measureChildWithMargins()
的第三个参数和第五个参数都为0,代表永远没有使用过的宽度来高度,只考虑 padding
和 margin
即可。
layout
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
ViewGroup
的 layout()
方法其实也是调用 View
的 layout()
完成布局的。 然而我们注意到 ViewGroup
的 onLayout
是一个抽象方法,子类必须实现,这也是提示我们,自定义 ViewGroup
就有责任为 Child View
布局。
draw
ViewGroup
如果自身没有特别的绘制需要,就不需要复写 draw()
或者 onDraw()
方法,它的重点在布局。draw()
方法会绘制一些基本的东西,例如背景,滚动条等等,因此如果想复写 draw()
方法记得调用 super.draw()
,而如果复写 onDraw()
只需要完成自己的绘制即可 。
参考文章
https://developer.android.google.cn/training/custom-views/create-view.html
https://realm.io/news/360andev-huyen-tue-dao-measure-layout-draw-repeat-custom-views-and-viewgroups-android/
http://blog.csdn.net/yanbober/article/details/46128379