View的绘制和View的事件分发是View的两个最为重要的知识点。
在上一篇中已经完整的分析过了View的事件分发机制,这一篇则是分析View的绘制原理。View的绘制原理是自定义View的基础知识,有了这个基础知识就可以写出五花八门的漂亮的自定义view了。
一个view要显示在界面上,需要经历一个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同理。
在看Measure源码之前我们需要了解一些基础知识
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,三种模式分别是:
接着来看看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 是AT_MOST,说明父View的大小是不确定,最大的大小是MeasureSpec 的size值,不能超过这个值。
如果父View的MeasureSpec 是UNSPECIFIED(未指定),表示没有任何束缚和约束,不像AT_MOST表示最大只能多大,不也像EXACTLY表示父View确定的大小,子View可以得到任意想要的大小,不受约束
从measure就可以直接进入onMeasure
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
onMeasure(widthMeasureSpec,heightMeasureSpec);
.....
}
总结一下流程就是:
以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的布局卡变万化,根本没有统一的模板,只能根据业务来定,一般大概流程就是:
接着就是第二个步骤layout了。
measure只能计算出来的只有view矩阵的大小,具体这个矩阵放在哪里,这就是layout 的工作了。layout的主要作用 :根据子视图的大小以及布局参数将View树放到合适的位置上。
先来看下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里面做的
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就不分析了
绘制是View树遍历流程的最后一个,前面说的测量和布局只是确定View的大小和位置,如果不对view进行绘制,那么界面上依然不会有任何图形显示出来,draw也是从ViewRoot中的performTraversals发起的。然后会view的draw相关方法,但是并不是每个View都需要执行绘制,在执行绘制的过程中,只会重绘需要绘制的View。
draw方法的流程为:
view有两个重载的draw方法,分别是:
draw(Canvas canvas, ViewGroup parent, long drawtime)
draw(Canvas canvas)
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是个空实现方法,仅仅提供了一个Canvas画板,到底如何来画View的内容呢?
如果需要熟练的绘制出各种效果的View,我们需要掌握很多知识:
在Android的显示机制中,View的软件渲染都是基于bitmap图片进行的处理。并且刷新机制中只要是与脏数据区有交集的视图都将重绘,所以在View的设计中就有一个cache的概念存在,这个cache无疑就是一个bitmap对象。也就是说在绘制流程中View不一定会被重新绘制,有可能绘制的只是View的缓存。
参考:http://www.jianshu.com/p/5a71014e7b1b