目录
前言
上文最全的View绘制流程(上)— Window、DecorView、ViewRootImp的关系说到了,我们打开一个Activity
,经过Window
和DecorView
的创建过程后,绘制会从 ViewRootImp
的 performTraversals()
方法开始,从上到下遍历整个视图树进行View
的绘制
ViewRootImp.performTraversals()
performTraversals()
的源码非常的长,但是核心代码就是下面三个步骤。
private void performTraversals() {
......
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
//这里的mView就是DecorView
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
mView.draw(canvas);
......
}
所以,一个完整的绘制流程包括measure
、layout
、draw
三个步骤,其中:
-
measure
:测量。系统会先根据xml布局文件和代码中对控件属性的设置,来获取或者计算出每个View和ViewGrop的尺寸,并将这些尺寸保存下来 -
layout
:布局。根据测量出的结果以及对应的参数,来确定每一个控件应该显示的位置 -
draw
:绘制。确定好位置后,就将这些控件绘制到屏幕上
每个 View
负责绘制自己,而 ViewGroup
还需要负责通知自己的子 View
进行绘制操作
Measure — 测量
MeasureSpec
从上面的源码我们可以发现,系统会用一个int
型的值(childWidthMeasureSpec
和childHeightMeasureSpec
)来存储View
的宽高的信息
上文中用于生成childWidthMeasureSpec
和childHeightMeasureSpec
的getRootMeasureSpec()
方法
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
这个方法实际上调用的就是MeasureSpec.makeMeasureSpec()
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}
}
那么这个MeasureSpec
到底是什么意思呢?MeasureSpec
概括了从父布局传递给子view的布局要求,包括了测量模式和测量大小。分析代码可知,int长度为32位,高2位表示mode
(模式),后30位用于表示size
(大小)
有三种mode
:
UNSPECIFIED
:不对View大小做限制,如:ListView,ScrollView
EXACTLY
:确切的大小,如:100dp或者march_parent
AT_MOST
:大小不可超过某数值,如:wrap_content
子View的LayoutParams / 父view的MeasureSpec | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
具体大小(如100dp) | EXACTLY | EXACTLY | EXACTLY |
match_parent | EXACTLY | AT_MOST | UNSPECIFIED |
wrap_content | AT_MOST | AT_MOST | UNSPECIFIED |
当View采用固定宽高时(即设置固定的dp/px),不管父容器是什么模式,View
都是EXACTLY
模式,并且大小遵循我们设置的值
当View的宽高是match_parent时,如果父容器的是EXACTLY
模式,那么View也是EXACTLY
模式且其大小是父容器的剩余空间;如果父容器是AT_MOST
模式那么View
也是AT_MOST
模式并且其大小不会超过父容器的剩余空间
当View的宽高是wrap_content时,View
都是AT_MOST
模式并且其大小不能超过父容器的剩余空间
只要提供父容器的
MeasureSpec
和子元素的LayoutParams
,就可以确定出子元素的MeasureSpec
,进一步便可以确定出测量后的大小
onMeasure
mView.measure()
内部会调用onMeasure()
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//仅列出关键代码
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
一个View
的实际测量工作是在onMeasure()
中实现的,onMeasure()
已经默认为我们的控件测量了宽高
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
/**
* 为宽度获取一个建议最小值
*/
protected int getSuggestedMinimumWidth () {
return (mBackground == null) ? mMinWidth : max(mMinWidth , mBackground.getMinimumWidth());
}
/**
* 获取默认的宽高值
*/
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;
}
在自定义ViewGroup
时,默认的onMeasure()
往往不能满足我们的需求,这时候就要重写该方法,在该方法内测量子View
的尺寸。当重写onMeasure()
时,必须调用setMeasuredDimension(width,height)
来存储该View
测量出的宽和高。如果不这样做将会触发IllegalStateException
ViewGroup
提供了三个方法测量子View
的宽高
/**
*遍历ViewGroup中所有的子控件,调用measuireChild测量宽高
*/
protected void measureChildren (int widthMeasureSpec, int heightMeasureSpec) {
...
}
/**
* 测量某一个child的宽高
*/
protected void measureChild (View child, int parentWidthMeasureSpec,
...
}
/**
* 测量某一个child的宽高,考虑margin值
*/
protected void measureChildWithMargins (View child,
...
}
View和ViewGroup重写onMeasure的差异
下面用两个例子分别来展示一下View
和ViewGroup
重写onMeasure
的差异
View
View
一般只关心自身尺寸的测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = measureWidth(widthMeasureSpec);
int height = measureHeight(heightMeasureSpec);
setMeasuredDimension(width, height);
}
private int measureWidth(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
Log.e("CustomTextViewWidth", "---speSize = " + specSize + "");
switch (specMode) {
case MeasureSpec.AT_MOST:
result = (int) mPaint.measureText(mTextStr) + getPaddingLeft() + getPaddingRight();
Log.e("CustomTextViewWidth", "---speMode = AT_MOST");
break;
case MeasureSpec.EXACTLY:
Log.e("CustomTextViewWidth", "---speMode = EXACTLY");
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
Log.e("CustomTextViewWidth", "---speMode = UNSPECIFIED");
result = Math.max(result, specSize);
}
Log.e("CustomTextViewWidth", "---result = "+result);
return result;
}
private int measureHeight(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
Log.e("CustomTextViewHeight", "---speSize = " + specSize + "");
switch (specMode) {
case MeasureSpec.AT_MOST:
result =
(int) (-mPaint.ascent() + mPaint.descent()) + getPaddingTop() + getPaddingBottom();
Log.e("CustomTextViewHeight", "---speMode = AT_MOST");
break;
case MeasureSpec.EXACTLY:
result = specSize;
Log.e("CustomTextViewHeight", "---speSize = EXACTLY");
break;
case MeasureSpec.UNSPECIFIED:
result = Math.max(result, specSize);
Log.e("CustomTextViewHeight", "---speSize = UNSPECIFIED");
break;
}
Log.e("CustomTextViewHeight", "---result = "+result);
return result;
}
ViewGroup
ViewGroup
一般会先遍历子View
,调用子View
的测量方法,然后在再结合子View的尺寸来确定自身的大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
// 如果是wrap_content,定义width,height统计FlowLayout的宽高
int width = 0;
int height = 0;
// 记录每一行的宽度与高度
int lineWidth = 0;
int lineHeight = 0;
/**
* 1、通过getChildCount,获取子View的个数view个数
*/
int childCount = getChildCount();
/**
* 2、遍历childCount,通过getChildAt获取到对应的view
*/
for (int i = 0; i < childCount; i++) {
//获取i对应的子View,通过获取他的宽高,确定
View childView = getChildAt(i);
/**
* 3、对childView进行测量
*/
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
// 判断是否换行,如果换行则高度累加,如果不换行则宽度累加
if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) {
// 对比得到最大的宽度
width = Math.max(width, lineWidth);
// 重置lineWidth
lineWidth = childWidth;
// 记录行高
height += lineHeight;
// 重置lineHeight
lineHeight = childHeight;
} else {
// 宽度累加
lineWidth += childWidth;
// 得到当前行最大的高度
lineHeight = Math.max(lineHeight, childHeight);
}
// 最后一个的时候,不管是换行,还是未换行,前面都没有处理
if (i == childCount - 1) {
width = Math.max(width, lineWidth);
height += lineHeight;
}
/**
* 4、确定父布局(FlowLayout)的宽高
*/
setMeasuredDimension(modeWidth == MeasureSpec.EXACTLY ? sizeWidth :
width + getPaddingLeft() + getPaddingRight(),
modeHeight == MeasureSpec.EXACTLY ? sizeHeight :
height + getPaddingTop() + getPaddingBottom());
}
}
Layout - 布局
前面measure
的作用是测量每个View
的尺寸,而layout
的作用是根据前面测量的尺寸以及设置的其它属性值,共同来确定View
的位置
ViewGroup的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);
}
//内部实际上是调用View的layout()
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
从源码中可以看出实际上调用的还是View的layout()方法
View的layout()
/**
* 作用:确定View本身的位置,即设置View本身的四个顶点位置
*/
public void layout(int l, int t, int r, int b) {
//根据一些flag,如果有需要则进一步measure
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 & = ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
//暂存旧的位置信息
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) {
// 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现->>分析3
// 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现(后面会详细说)
onLayout(changed, l, t, r, b);
//回调layoutChange事件
......
}
//标记为已经执行过layout;
......
}
/**
* 作用:确定View本身的位置,即设置View本身的四个顶点位置
* @return 如果新的尺寸和位置和之前的不同,返回true
*/
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;
......
}
return changed;
}
从源码可以看出layout()最终通过setFrame()
方法对view的四个属性(mLeft、mTop、mRight、mBottom)进行了赋值,从而去确定View
的大小和位置,如果发生改变则调用onLayout()
方法
onLayout
我们先来看看ViewGroup
的onLayout()
方法,该方法是一个抽象方法。因为layout
过程是父布局容器布局子View
的过程,onLayout()
方法对子View
没有意义,只有ViewGroup
才有用,所以ViewGroup
应该重写该方法并为每一个子View
调用layout()
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);
我们再来看看顶层ViewGroup,也就是DecorView
的onLayout()
方法。DecerView
继承自FrameLayout
,所以我们直接看FrameLayout
的onLayout()
方法
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
......
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
......
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
我们可以看到,这里面会对每一个child
调用layout()
方法。如果该child
仍然是ViewGroup
,会继续递归下去;如果是叶子View
,则会走到View
的onLayout
空方法,该叶子View
布局流程就走完了。另外,width
和height
分别来源于measure
阶段存储的测量值
Draw - 绘制
当layout
完成后,就进入到draw
阶段了,在这个阶段,会根据layout
中确定的各个view
的位置将它们画出来
前面说过,mView
就是DecorView
,所以我们直接来看DecorView
的draw()
方法
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (mMenuBackground != null) {
mMenuBackground.draw(canvas);
}
}
调用完super.draw()
后,还画了菜单背景。我们继续关注super.draw()
方法,会发现FrameLayout和ViewGroup都没有重写该方法,直接进到了View
的draw()
方法
@CallSuper
public void draw(Canvas canvas) {
......
int saveCount;
// Step 1, draw the background, if needed
if (!dirtyOpaque) {
drawBackground(canvas);
}
// Step 2, If necessary, save the canvas' layers to prepare for fading
......
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
//Step 5, If necessary, draw the fading edges and restore layers
......
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);45 ......
}
主要是就是如下几步,其中最重要的就是画内容和画子View
画背景。对应我我们在xml布局文件中设置的
android:background
属性画内容。通过重写
onDraw()
方法画子
View
。dispatchDraw()
方法用于帮助ViewGroup
来递归画它的子View
画装饰。这里指画滚动条和前景。其实平时的每一个
View
都有滚动条,只是没有显示而已
onDraw()
当自定义View需要进行绘制的时候,我们往往会重写onDraw()方法,这里放一个简单的例子感受一下
Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.YELLOW);
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
mPaint.setColor(Color.BLUE);
mPaint.setTextSize(20);
String text = "Hello View";
canvas.drawText(text, 0, getHeight() / 2, mPaint);
}
总结
本文主要讲解View的绘制流程,让读者们从大的方向上对View的绘制有一个了解。具体自定义View如何进行measure
、layout
、draw
这些细节,以后有时间会专门出一个自定义View系列进行讲解