您的位置:首页 > 移动开发 > Android开发

Android 中View的绘制流程(结合图解及伪代码说明)

2018-04-03 15:50 579 查看
)##介绍

在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 M
10b5b
easureSpecMode {}
//不指定测量模式
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中自定义控件之流式布局实现方式
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: