View的绘制原理
1.前言
View的绘制和View的事件分发是View的两个最为重要的知识点。
在上一篇中已经完整的分析过了View的事件分发机制,这一篇则是分析View的绘制原理。View的绘制原理是自定义View的基础知识,有了这个基础知识就可以写出五花八门的漂亮的自定义view了。
2.View的绘制流程
一个view要显示在界面上,需要经历一个view树的遍历过程,这个过程又可以分为三个过程,分别是:
- 测量measure 确定一个View的大小
- 布局layout 确定view在父节点上的位置
- 绘制draw 绘制view 的内容
这个过程的启动是由ViewRoot.performTraversals()函数发起的,子view也可以通过一些方法来请求重新遍历view树,但是在遍历过程view树时并不是所有的view都需要重新测量,布局和绘制,在view树的遍历过程中,系统会问view是否需要重新绘制,如果需要才会真的去绘制view。
其中ViewRoot.performTraversals会依次调用ViewRoot.performMeasure,ViewRoot.performLayout,ViewRoot.performDraw,这三个方法分别完成顶级view的measure、layout、draw三个流程。
performMeasure里面会对所有的子元素进行measure过程,这个时候measure就从父容器传递到子元素中。这样层层递进,measure过程就完成。ViewRoot.performLayout和ViewRoot.performDraw同理。
3.Measure
在看Measure源码之前我们需要了解一些基础知识
3.1 MeasureSpec
View的测量模式,测量规格等,这些东西都封装在MeasureSpec中,MeasureSpec封装了从父容器传递给子容器的布局要求,现在来学习一下这个类
MeasureSpec {
//MODE_SHIFT是位偏移数
private static final int MODE_SHIFT = 30;
//模式遮罩
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//三种模式
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 getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
//获取尺寸
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
......
}
这里面有用一个int型变量来表示一个测量规格,我们知道int型有32位,而MODE_SHIFT=30,三种模式分别是:
- UNSPECIFIED:表示父容器不对子view的大小做任何要求
- EXACTLY:父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间。
- AT_MOST:表示父容器不能确定子view大小,但是要求子view的大小不能超过测量规格中指定的尺寸。
3.2 LayoutParams
接着来看看LayoutParams。LayoutParams描述了View的大小,对其方式等信息,而每个ViewGroup都可以根据自身的layout特性来定制自己的LayoutParams。
之前提到系统内部是通过MeasureSpec来进行View的测量,但是我们也可以通过View设置LayoutParams来设置view的测量。在测量的时候系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽高。
需要注意的是MeasureSpec不是唯一由LayoutParams决定的,而是父View的MeasureSpec和子View自己的LayoutParams共同决定的,而子View的LayoutParams其实就是我们在xml写的时候设置的layout_width和layout_height 转化而来的。
对于顶级View则有些不同,其MeasureSpec是由窗口的尺寸和自身的LayoutParams决定的。不用在意父容器,毕竟没有父容器。
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
// 子View的LayoutParams,你在xml的layout_width和layout_height,
// layout_xxx的值最后都会封装到这个个LayoutParams。
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//根据父View的测量规格和父View自己的Padding,
//还有子View的Margin和已经用掉的空间大小(widthUsed),就能算出子View的MeasureSpec,具体计算过程看getChildMeasureSpec方法。
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);
//通过父View的MeasureSpec和子View的自己LayoutParams的计算,算出子View的MeasureSpec,然后父容器传递给子容器的
// 然后让子View用这个MeasureSpec(一个测量要求,比如不能超过多大)去测量自己,如果子View是ViewGroup 那还会递归往下测量。
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// spec参数 表示父View的MeasureSpec
// padding参数 父View的Padding+子View的Margin,父View的大小减去这些边距,才能精确算出
// 子View的MeasureSpec的size
// childDimension参数 表示该子View内部LayoutParams属性的值(lp.width或者lp.height)
// 可以是wrap_content、match_parent、一个精确指(an exactly size),
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec); //获得父View的mode
int specSize = MeasureSpec.getSize(spec); //获得父View的大小
//父View的大小-自己的Padding+子View的Margin,得到值才是子View的大小。
int size = Math.max(0, specSize - padding);
int resultSize = 0; //初始化值,最后通过这个两个值生成子View的MeasureSpec
int resultMode = 0; //初始化值,最后通过这个两个值生成子View的MeasureSpec
switch (specMode) {
// Parent has imposed an exact size on us
//1、父View是EXACTLY的 !
case MeasureSpec.EXACTLY:
//1.1、子View的width或height是个精确值 (an exactly size)
if (childDimension >= 0) {
resultSize = childDimension; //size为精确值
resultMode = MeasureSpec.EXACTLY; //mode为 EXACTLY 。
}
//1.2、子View的width或height为 MATCH_PARENT/FILL_PARENT
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size; //size为父视图大小
resultMode = MeasureSpec.EXACTLY; //mode为 EXACTLY 。
}
//1.3、子View的width或height为 WRAP_CONTENT
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size; //size为父视图大小
resultMode = MeasureSpec.AT_MOST; //mode为AT_MOST 。
}
break;
// Parent has imposed a maximum size on us
//2、父View是AT_MOST的 !
case MeasureSpec.AT_MOST:
//2.1、子View的width或height是个精确值 (an exactly size)
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension; //size为精确值
resultMode = MeasureSpec.EXACTLY; //mode为 EXACTLY 。
}
//2.2、子View的width或height为 MATCH_PARENT/FILL_PARENT
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; //size为父视图大小
resultMode = MeasureSpec.AT_MOST; //mode为AT_MOST
}
//2.3、子View的width或height为 WRAP_CONTENT
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size; //size为父视图大小
resultMode = MeasureSpec.AT_MOST; //mode为AT_MOST
}
break;
// Parent asked to see how big we want to be
//3、父View是UNSPECIFIED的 !
case MeasureSpec.UNSPECIFIED:
//3.1、子View的width或height是个精确值 (an exactly size)
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension; //size为精确值
resultMode = MeasureSpec.EXACTLY; //mode为 EXACTLY
}
//3.2、子View的width或height为 MATCH_PARENT/FILL_PARENT
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = 0; //size为0! ,其值未定
resultMode = MeasureSpec.UNSPECIFIED; //mode为 UNSPECIFIED
}
//3.3、子View的width或height为 WRAP_CONTENT
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0; //size为0! ,其值未定
resultMode = MeasureSpec.UNSPECIFIED; //mode为 UNSPECIFIED
}
break;
}
//根据上面逻辑条件获取的mode和size构建MeasureSpec对象。
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
其实计算原理很简单:
- 如果我们在xml 的layout_width或者layout_height 把值都写死,那么上述的测量完全就不需要了,之所以要上面的这步测量,是因为 match_parent 就是充满父容器,wrap_content 就是自己多大就多大, 我们写代码的时候特别爽,我们编码方便的时候,google就要帮我们计算你match_parent的时候是多大,wrap_content的是多大,这个计算过程,就是计算出来的父View的MeasureSpec不断往子View传递,结合子View的LayoutParams 一起再算出子View的MeasureSpec,然后继续传给子View,不断计算每个View的MeasureSpec,子View有了MeasureSpec才能更测量自己和自己的子View。
- 如果父View的MeasureSpec 是EXACTLY,说明父View的大小是确切的,(确切的意思很好理解,如果一个View的MeasureSpec 是EXACTLY,那么它的size 是多大,最后展示到屏幕就一定是那么大)。
-
- 如果子View 的layout_xxxx是MATCH_PARENT,父View的大小是确切,子View的大小又MATCH_PARENT(充满整个父View),那么子View的大小肯定是确切的,而且大小值就是父View的size。所以子View的size=父View的size,mode=EXACTLY
-
- 如果子View 的layout_xxxx是WRAP_CONTENT,也就是子View的大小是根据自己的content 来决定的,但是子View的毕竟是子View,大小不能超过父View的大小,但是子View的是WRAP_CONTENT,我们还不知道具体子View的大小是多少,要等到child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 调用的时候才去真正测量子View 自己content的大小(比如TextView wrap_content 的时候你要测量TextView content 的大小,也就是字符占用的大小,这个测量就是在child.measure(childWidthMeasureSpec, childHeightMeasureSpec)的时候,才能测出字符的大小,MeasureSpec 的意思就是假设你字符100px,但是MeasureSpec 要求最大的只能50px,这时候就要截掉了)。通过上述描述,子View MeasureSpec mode的应该是AT_MOST,而size 暂定父View的 size,表示的意思就是子View的大小没有不确切的值,子View的大小最大为父View的大小,不能超过父View的大小(这就是AT_MOST 的意思),然后这个MeasureSpec 做为子View measure方法 的参数,做为子View的大小的约束或者说是要求,有了这个MeasureSpec子View再实现自己的测量。
-
- 如果如果子View 的layout_xxxx是确定的值(200dp),那么就更简单了,不管你父View的mode和size是什么,我都写死了就是200dp,那么控件最后展示就是就是200dp,不管我的父View有多大,也不管我自己的content 有多大,反正我就是这么大,所以这种情况MeasureSpec 的mode = EXACTLY 大小size=你在layout_xxxx 填的那个值。
-
如果父View的MeasureSpec 是AT_MOST,说明父View的大小是不确定,最大的大小是MeasureSpec 的size值,不能超过这个值。
-
- 如果子View 的layout_xxxx是MATCH_PARENT,父View的大小是不确定(只知道最大只能多大),子View的大小MATCH_PARENT(充满整个父View),那么子View你即使充满父容器,你的大小也是不确定的,父View自己都确定不了自己的大小,你MATCH_PARENT你的大小肯定也不能确定的,所以子View的mode=AT_MOST,size=父View的size,也就是你在布局虽然写的是MATCH_PARENT,但是由于你的父容器自己的大小不确定,导致子View的大小也不确定,只知道最大就是父View的大小。
-
- 如果子View 的layout_xxxx是WRAP_CONTENT,父View的大小是不确定(只知道最大只能多大),子View又是WRAP_CONTENT,那么在子View的Content没算出大小之前,子View的大小最大就是父View的大小,所以子View MeasureSpec mode的就是AT_MOST,而size 暂定父View的 size。
-
- 如果如果子View 的layout_xxxx是确定的值(200dp),同上,写多少就是多少,改变不了的。
-
如果父View的MeasureSpec 是UNSPECIFIED(未指定),表示没有任何束缚和约束,不像AT_MOST表示最大只能多大,不也像EXACTLY表示父View确定的大小,子View可以得到任意想要的大小,不受约束
-
- 如果子View 的layout_xxxx是MATCH_PARENT,因为父View的MeasureSpec是UNSPECIFIED,父View自己的大小并没有任何约束和要求,那么对于子View来说无论是WRAP_CONTENT还是MATCH_PARENT,子View也是没有任何束缚的,想多大就多大,没有不能超过多少的要求,一旦没有任何要求和约束,size的值就没有任何意义了,所以一般都直接设置成0
-
- 同上…
-
- 如果如果子View 的layout_xxxx是确定的值(200dp),同上,写多少就是多少,改变不了的(记住,只有设置的确切的值,那么无论怎么测量,大小都是不变的,都是你写的那个值)
3.3 View.onMeasure()
从measure就可以直接进入onMeasure
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
onMeasure(widthMeasureSpec,heightMeasureSpec);
.....
}
再来看看onMeasure的实现
总结一下流程就是:
- 获取测量尺寸和模式,定义临时变量存储技术结果
- 判断测量模式:
-
- 模式是EXACTL的,就使用测量规格中的尺寸
-
- 模式是UNSPECIFIED,使用自身计算的尺寸
-
- 模式是AT_MOST的,使用自身计算的尺寸与规定尺寸中较小的一个
-
- 设置测量尺寸
3.4 ViewGroup.onMeasure
以FrameLayout为例
//FrameLayout 的测量
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
....
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) {
// 遍历自己的子View,只要不是GONE的都会参与测量,measureChildWithMargins方法在最上面
// 的源码已经讲过了,如果忘了回头去看看,基本思想就是父View把自己的MeasureSpec
// 传给子View结合子View自己的LayoutParams 算出子View 的MeasureSpec,然后继续往下传,
// 传递叶子节点,叶子节点没有子View,根据传下来的这个MeasureSpec测量自己就好了。
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);
....
....
}
}
.....
.....
//所有的孩子测量之后,经过一系类的计算之后通过setMeasuredDimension设置自己的宽高,
//对于FrameLayout 可能用最大的字View的大小,对于LinearLayout,可能是高度的累加,
//具体测量的原理去看看源码。总的来说,父View是等所有的子View测量结束之后,再来测量自己。
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
....
}
再来看看ViewGroup的onMeasure一般写法。
由于ViewGroup的布局卡变万化,根本没有统一的模板,只能根据业务来定,一般大概流程就是:
- 首先ViewGroup作为容器,有测量所有子view的职责,所以第一步是遍历测量所有的字view。一般我们会选用ViewGroup提供的measureChildWithMargins方法,这个方法已经考虑了子view需要的margin和ViewGroup自身需要的padding。
- 遍历测量完所有子view,那么子view的宽高基本可以确定了,如果有特殊的需求,可以对子view进行多次测量。
- 根据自身的布局特性,来确定自身的宽高,但是需要遵守基本的测量规则
4.Layout
接着就是第二个步骤layout了。
measure只能计算出来的只有view矩阵的大小,具体这个矩阵放在哪里,这就是layout 的工作了。layout的主要作用 :根据子视图的大小以及布局参数将View树放到合适的位置上。
先来看下ViewGroup的layout
4.1 ViewGroup.layout
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;
}
}
可以看出来ViewGroup会先处理动画。若没有动画则会先super.layout。否则就会等动画执行完成再来调用super.layout。
所以ViewGroup.layout的具体实现是在super.layout里面做的
4.2 View.layout
public final void layout(int l, int t, int r, int b) {
.....
//设置View位于父视图的坐标轴
boolean changed = setFrame(l, t, r, b);
//判断View的位置是否发生过变化,看有必要进行重新layout吗
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
//调用onLayout(changed, l, t, r, b); 函数
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~LAYOUT_REQUIRED;
}
mPrivateFlags &= ~FORCE_LAYOUT;
.....
}
setFrame可以理解为给mLeft 、mTop、mRight、mBottom赋值,然后基本就能确定View自己在父视图的位置了。然后就调用onLayout了。而view的onLayout是一个空实现,只需要重写ViewGroup的onLayout。
具体怎么onLayout就不分析了
5.Draw
绘制是View树遍历流程的最后一个,前面说的测量和布局只是确定View的大小和位置,如果不对view进行绘制,那么界面上依然不会有任何图形显示出来,draw也是从ViewRoot中的performTraversals发起的。然后会view的draw相关方法,但是并不是每个View都需要执行绘制,在执行绘制的过程中,只会重绘需要绘制的View。
draw方法的流程为:
- ViewRoot调用DecorView的draw方法:ViewRoot–>DecorView.draw(canvas)
- DecorView的draw方法调用自己的dispatchDraw(Canvas canvas)方法,然后在此方法中会调用子view的draw(Canvas canvas, ViewGroup parent, long drawtime)方法,此方法会调用单个参数的draw(Canvas canvas)方法。
5.1 draw方法
view有两个重载的draw方法,分别是:
draw(Canvas canvas, ViewGroup parent, long drawtime)
draw(Canvas canvas)
draw(Canvas canvas, ViewGroup parent, long drawtime)方法由父view调用,此方法比较重要的,在这里会判断View的一些内部标识,还会对canvas做一些调整,如绘制区域与绘图坐标系的调整,不一定会调用view的 draw(Canvas canvas)方法,如果不调用则绘制的是view的缓存。具体可以查看相关方法的源码。
public void draw(Canvas canvas) {
......
//通过内部标识,判断View的行为
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
/*
*draw的步骤
*
* 1. 画背景
* 2. 如果需要, 为显示渐变框做一些准备操作
* 3. 画内容(onDraw)
* 4. 画子view
* 5. 如果需要, 画一些渐变效果
* 6. 画装饰内容,如滚动条
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
final Drawable background = mBGDrawable;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
//如果条件不成立,跳过2-5步
if (!verticalEdges && !horizontalEdges) {
// Step 3,画内容
if (!dirtyOpaque) onDraw(canvas);
// Step 4,画孩子
dispatchDraw(canvas);
// Step 6, 画装饰(滚动条)
onDrawScrollBars(canvas);
// we're done...
return;
}
......
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
dispatchDraw&drawChild
protected void dispatchDraw(Canvas canvas) {
......
for (int i = 0; i < count; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
......
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
可以看到draw的过程分为:
1. 如果设置了,画背景
2. 如果需要, 为显示渐变框做一些准备操作
3. 调用onDraw画内容
4. 调用dispatchDraw画子view
5. 如果需要, 画渐变框
6. 画装饰内容,如前景与滚动条
- onDraw是每个view需要实现的,否则View默认只能显示背景,而实现onDraw就是为了画出View的内容,而ViewGroup一般不需要实现onDraw,因为它仅仅是作为View的容器没有需要绘制东西,
- dispatchDraw用来遍历ViewGrop的所有子view,执行draw方法
5.2 onDraw中如何绘制
在系统源码中onDraw是个空实现方法,仅仅提供了一个Canvas画板,到底如何来画View的内容呢?
如果需要熟练的绘制出各种效果的View,我们需要掌握很多知识:
- Canvas的是使用 绘制-变化-图层操作等等
- Paint 画笔
- Path 路径
- Bitmap Canvas是画布,但是我们需要画纸,Bitmap就是画纸
- ColorMatrix和Matrix的熟练运用
- PathMeasure
5.3 View的缓存优化
在Android的显示机制中,View的软件渲染都是基于bitmap图片进行的处理。并且刷新机制中只要是与脏数据区有交集的视图都将重绘,所以在View的设计中就有一个cache的概念存在,这个cache无疑就是一个bitmap对象。也就是说在绘制流程中View不一定会被重新绘制,有可能绘制的只是View的缓存。
参考:http://www.jianshu.com/p/5a71014e7b1b