)##介绍
在Android开发过程中,经常存在需要实现自定义控件的情况,对于比较简单的需求,通过组合系统提供的原生控件既可以完成,但是一旦碰到比较复杂的控件时候,这时候就需要我们亲自动手完成控件的设计,实现对控件的测量、布局、绘制等操作,而这一且操作的前提是你需要了解并掌握View的绘制流程。
在正式讲解View的绘制流程之前,我们有必要先来简单了解下Android的UI管理系统层级关系,如下图所示:
PhoneWindow 是Android系统中最基本的窗口系统,每一个Activity会创建一个。PhoneWindow是Activity和View系统交互的接口。DecorView本质上是一个FrameLayout,是Activity中所有View的祖先。
绘制整体流程
当一个应用启动的时候,会启动一个主Activity,Android系统会根据Activity的布局来对它进行绘制。绘制会从根视图ViewRootImpl的performTraversals()方法开始,从上到下遍历整个视图树,每一个View控件负责绘制自己,而ViewGroup还需要负责通知自己的子View进行绘制操作。整个流程如下图所示:
视图的绘制可以分为三个步骤,分别为测量(Measure)、布局(Layout)和绘制(Draw)。
private void performTraversals() {
int childWidthMeasureSpec=getRootMeasureSpec(mWidth,lp.width);
int childHeightMeasureSpec=getRootMeasureSpec(mHeight,lp.height);
.........
//执行测量流程
performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
.........
//执行布局流程
performLayout(lp,desiredWindowWidth,desiredWindowHeight);
.........
//执行绘制流程
performDraw();
}
其框架过程如下:
MeasureSpec
在介绍Measure过程之前,我们先了解一下MeasureSpec,这对之后理解Measure过程是十分重要的!
MeasureSpec表示的是一个32位整型值,它的高2位表示测量模式SpecMode,低30位表示某种测量模式下的规格大小SpecSize。MeasureSpec是View类的一个静态内部类,用来说明如何测量这个View,其核心代码如下
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;
//根据指定的大小和模式创建一个MeasureSpec
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);
}
//微调某一个MeasureSpec的大小
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);
}
}
小结(重点关注代码中的以下三种测量模式):
- 1、UNSPECIFIED:不指定测量模式,父视图没有限制子视图的大小,子视图可以是想要的任何尺寸,通常用于系统内部,应用开发中很少用到。
- 2、EXACTLY:精确测量模式,当该视图的layout_width或者layout_height指定为具体数值或者match_parent时生效,表示父视图已经决定了子视图的精确大小,这种模式下的View测量值就是SpecSize大小的值。
- 3、AT_MOST:最大值模式,当该视图的layout_width或者layout_height指定为wrap_content时生效,此时子视图的尺寸可以是不超过父视图允许的最大尺寸的任何尺寸。
注意:对DecorView而言,它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同决定;对于普通的View,它的MeasureSpec由父视图的MeasureSpec和其自身的LayoutParams共同决定。
Measure过程
主要作用:
- 为整个View树计算实际的大小,即设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:mMeasureWidth),每个View的控件的实际宽高都是由父视图和本身视图决定的。
由上面我们知道,页面的测量流程是从performMeasure()方法开始的,核心代码如下
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
..........
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
..........
}
从上面可以看出。具体的测量操作是分发给ViewGroup的,由ViewGroup在它的measureChild方法中传递
measureChildren主要是遍历ViewGroup中的所有View进行测量代码如下
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];
// 当View的可见性处于Gone状态时,不对其进行测量
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
measureChild是为了测量某一个指定的View 重要根据父容器的MeasureSpec和子View的LayoutParams等信息计算子View的MeasureSpec
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);
}
measure函数原型为 View.java 该函数不能被重载,而是通过回调onMeasure方法实现的。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
.........
onMeasure(widthMeasureSpec,heightMeasureSpec);
.........
}
onMeasure方法通常是由View特定子类自己实现的,开发者也可以通过重写这个方法实现自定义View。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
//如果View没有重写onMeasure方法,则会默认直接调用getDefaultSize来获取View的宽高
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;
}
小结:
具体的调用链如下:
ViewRoot根对象地属性mView(其类型一般为ViewGroup类型)调用measure()方法去计算View树的大小,回调View/ViewGroup对象的onMeasure()方法,该方法实现的功能如下:
- 1、设置本View视图的最终大小,该功能的实现通过调用setMeasuredDimension()方法去设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:mMeasureWidth);
- 2、如果该View对象是个ViewGroup类型,需要重写该onMeasure()方法,对其子视图进行遍历的measure()过程。
- 2.1 对每个子视图的measure()过程,是通过调用父类ViewGroup.java类里的measureChildWithMargins()方法去实现,该方法内部只是简单地调用了View对象的measure()方法。(由于measureChildWithMargins()方法只是一个过渡层更简单的做法是直接调用View对象的measure()方法)。
整个measure调用流程就是个树形的递归过程。
为了大家更好的理解采用下面的伪代码
//回调View视图里的onMeasure过程
private void onMeasure(int height , int width){
//设置该view的实际宽(mMeasuredWidth)高(mMeasuredHeight)
//1、该方法必须在onMeasure调用,否者报异常。
setMeasuredDimension(h , l) ;
//2、如果该View是ViewGroup类型,则对它的每个子View进行measure()过程
int childCount = getChildCount() ;
for(int i=0 ;i<childCount ;i++){
//2.1、获得每个子View对象引用
View child = getChildAt(i) ;
//整个measure()过程就是个递归过程
//该方法只是一个过滤器,最后会调用measure()过程 ;或者 measureChild(child , h, i)方法都
measureChildWithMargins(child , h, i) ;
//其实,对于我们自己写的应用来说,最好的办法是去掉框架里的该方法,直接调用view.measure(),如下:
//child.measure(h, l)
}
}
//该方法具体实现在ViewGroup.java里 。
protected void measureChildWithMargins(View v, int height , int width){
v.measure(h,l)
}
Layout过程
主要作用 :
为将整个根据子视图的大小以及布局参数将View树放到合适的位置上。该过程用来确定View在父容器中的布局位置。
由父容器获取子View的位置参数后,调用子View的layout方法并将位置参数传入实现的,ViewRootImpl的performLayout代码如下:
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
........
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
........
}
//View.java
public void layout(int l, int t, int r, int b) {
boolean changed = setFrame(l, t, r, b); //设置每个视图位于父视图的坐标轴
........
onLayout(changed, l, t, r, b);
........
}
// 空方法,子类如果是ViewGroup类型,则重写这个方法,实现ViewGroup中所有View控件布局流程
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
具体的调用链如下:
host.layout()开始View树的布局,继而回调给View/ViewGroup类中的layout()方法。具体流程如下
1、layout方法会设置该View视图位于父视图的坐标轴,即mLeft,mTop,mLeft,mBottom(调用setFrame()函数去实现)接下来回调onLayout()方法(如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局) ;
2、如果该View是个ViewGroup类型,需要遍历每个子视图chiildView,调用该子视图的layout()方法去设置它的坐标值。
同样地, 将上面layout调用流程,用伪代码描述如下:
// layout()过程 ViewRoot.java
// 发起layout()的"发号者"在ViewRoot.java里的performTraversals()方法, mView.layout()
private void performTraversals(){
//...
View mView ;
mView.layout(left,top,right,bottom) ;
//....
}
//回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现
private void onLayout(int left , int top , right , bottom){
//如果该View不是ViewGroup类型
//调用setFrame()方法设置该控件的在父视图上的坐标轴
setFrame(l ,t , r ,b) ;
//--------------------------
//如果该View是ViewGroup类型,则对它的每个子View进行layout()过程
int childCount = getChildCount() ;
for(int i=0 ;i<childCount ;i++){
//2.1、获得每个子View对象引用
View child = getChildAt(i) ;
//整个layout()过程就是个递归过程
child.layout(l, t, r, b) ;
}
}
Draw 过程
Draw操作用来将控件绘制出来,绘制的流程是从performDraw方法开始,核心代码如下。
private void performDraw(){
......
draw(fullRedrawNeeded);
......
}
private void draw(boolean fullRedrawNeeded) {
........
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
........
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
.......
mView.draw(canvas);
.......
}
可以看到最终调用到每一个View的Draw方法绘制每一个具体的View,绘制基本上可以分为六个步骤,代码如下
public void draw(Canvas canvas) {
........
//步骤一:绘制View的背景
drawBackground(canvas);
.........
//步骤二:如果需要的话,保存canvans的图层,为fading做准备
saveCount = canvas.getSaveCount();
........
canvas.saveLayer(left, top, right, top + length, null, flags);
//步骤三:绘制View的内容
onDraw(canvas);
.....
//步骤四:绘制View的子View
dispatchDraw(canvas);
//步骤五:如果需要的话,绘制View的fading边缘并恢复图层
canvas.drawRect(right - length, top, right, bottom, p);
......
canvas.restoreToCount(saveCount);
......
//步骤六:绘制View的装饰(比如滚动条)
onDrawScrollBars(canvas);
}
小结
由ViewRoot对象的performTraversals()方法调用draw()方法发起绘制该View树,值得注意的是每次发起绘图时,并不会重新绘制每个View树的视图,而只会重新绘制那些“需要重绘”的视图,View类内部变量包含了一个标志位DRAWN,当该视图需要重绘时,就会为该View添加该标志位。
调用流程:
mView.draw()开始绘制,draw()方法实现的功能如下:
- 1、绘制该View的背景
- 2、为显示渐变框做一些准备操作(见5,大多数情况下,不需要改渐变框)
- 3、调用onDraw()方法绘制视图本身 (每个View都需要重载该方法,ViewGroup不需要实现该方法)
- 4、调用dispatchDraw ()方法绘制子视图(如果该View类型不为ViewGroup,即不包含子视图,不需要重载该方法)值得说明的是,ViewGroup类已经为我们重写了dispatchDraw ()的功能实现,应用程序一般不需要重写该方法,但可以重载父类函数实现具体的功能。
- 4.1 dispatchDraw()方法内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法(注意,这个地方“需要重绘”的视图才会调用draw()方法)。
伪代码:
// draw()过程 ViewRoot.java
// 发起draw()的"发号者"在ViewRoot.java里的performTraversals()方法, 该方法会继续调用draw()方法开始绘图
private void draw(){
//...
View mView ;
mView.draw(canvas) ;
//....
}
//回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现
private void draw(Canvas canvas){
//该方法会做如下事情
//1 、绘制该View的背景
//2、为绘制渐变框做一些准备操作
//3、调用onDraw()方法绘制视图本身
//4、调用dispatchDraw()方法绘制每个子视图,dispatchDraw()已经在Android框架中实现了,在ViewGroup方法中。
// 应用程序程序一般不需要重写该方法,但可以捕获该方法的发生,做一些特别的事情。
//5、绘制渐变框
}
//ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法
@Override
protected void dispatchDraw(Canvas canvas) {
//
//其实现方法类似如下:
int childCount = getChildCount() ;
for(int i=0 ;i<childCount ;i++){
View child = getChildAt(i) ;
//调用drawChild完成
drawChild(child,canvas) ;
}
}
//ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法
protected void drawChild(View child,Canvas canvas) {
// ....
//简单的回调View对象的draw()方法,递归就这么产生了。
child.draw(canvas) ;
//.........
}
强调一点的就是,在这三个流程中,Google已经帮我们把draw()过程框架已经写好了,自定义的ViewGroup只需要实现measure()过程和layout()过程即可 。
这三种情况,最终会直接或间接调用到三个函数,分别为invalidate(),requsetLaytout()以及requestFocus() ,接着这三个函数最终会调用到ViewRoot中的schedulTraversale()方法,该函数然后发起一个异步消息,消息处理中调用performTraverser()方法对整个View进行遍历。
invalidate()方法
说明:请求重绘View树,即draw()过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些“需要重绘的”视图,即谁(View的话,只绘制该View ;ViewGroup,则绘制整个ViewGroup)请求invalidate()方法,就绘制该视图。
一般引起invalidate()操作的函数如下:
- 1、直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身。
- 2、setSelection()方法 :请求重新draw(),但只会绘制调用者本身。
- 3、setVisibility()方法 : 当View可视状态在INVISIBLE转换VISIBLE时,会间接调用invalidate()方法,继而绘制该View。
- 4、setEnabled()方法 : 请求重新draw(),但不会重新绘制任何视图包括该调用者本身。
requestLayout()方法
说明:只是对View树重新布局layout过程包括measure()和layout()过程,不会调用draw()过程,但不会重新绘制任何视图包括该调用者本身。
一般引起invalidate()操作的函数如下:
- 1、setVisibility()方法:当View的可视状态在INVISIBLE/ VISIBLE 转换为GONE状态时,会间接调用requestLayout() 和invalidate方法。同时,由于整个个View树大小发生了变化,会请求measure()过程以及draw()过程,同样地,只绘制需要“重新绘制”的视图。
requestFocus()方法
说明:请求View树的draw()过程,但只绘制“需要重绘”的视图。
总结
至此View绘制流程基本讲述完毕,为了更好的巩固这些知识,可以参考我的另一篇文章Android中自定义控件之流式布局实现方式