androidView的绘制流程整理和记录

view是我们作为android开发,平时最常接触的东西了。但是view的绘制流程和实现原理大多数人都是不知其所以然。所以我想整理一下View的绘制流程,加深印象,提升自己

View绘制的流程框架

所有View的绘制都是从ViewRoot的performTraversals方法开始的。ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带。
如图所示,performTraversals会依次调用performMeasure、performLayout、performDraw三个方法,这3个方法分别完成顶级View的measure、layout、draw这3个流程。其中performMeasure会调用measure方法,measure方法中又会调用onMeasure方法,这样层层调用,最终遍历整个View树。

image

View的绘制是从上往下一层层迭代下来的。DecorView–> ViewGroup(—>ViewGroup)–> View,按照这个流程从上往下,依次measure(测量),layout(布局),draw(绘制)。

image

Measure流程

View的measure流程是通过调用View的measure()方法进行的,measure方法又在内部调用了onMeasure()方法。因为measure方法是一个final方法,子类无法重写,我们只需要关注onMeasure()方法即可。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

onMeasure() 中调用setMeasuredDimension()设定View的宽高信息,完成View的测量操作。其中参数widthMeasureSpec,heightMeasureSpec表示View的宽高的一些信息。

MeasureSpec的确定

先介绍下什么是MeasureSpec?

image
MeasureSpec由两部分组成,一部分是测量模式,另一部分是测量的尺寸大小。用这种格式主要是为了避免更多的内存分配。

其中,Mode模式共分为三类
UNSPECIFIED :不对View进行任何限制,要多大给多大,一般用于系统内部

EXACTLY:对应LayoutParams中的match_parent和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,

AT_MOST :对应LayoutParams中的wrap_content。View的大小不能大于父容器的大小。

那么widthMeasureSpec和heightMeasureSpec这两个参数是怎么获得的呢?

对于DecorView,其确定是通过屏幕的大小,和自身的布局参数LayoutParams。
这部分很简单,根据LayoutParams的布局格式(match_parent,wrap_content或指定大小),将自身大小,和屏幕大小相比,设置一个不超过屏幕大小的宽高,以及对应模式。

对于其他View(包括ViewGroup),其确定是通过父布局的MeasureSpec和自身的布局参数LayoutParams。
这部分比较复杂。以下列图表表示不同的情况:

image

View的测量流程如下:
image

Layout流程

知道了View的大小,就可以将View布局在window中,view的布局主要通过上下左右四个点来确定。

和measure过程不同的是ViewGroup现在layout()方法中确定自己的布局,然后在onLayout()中再调用子View的layout()方法。而在Measure过程中,ViewGroup一般是先测量子View的大小,然后再确认自身的大小。

public void layout(int l, int t, int r, int b) {  

    // 当前视图的四个顶点
    int oldL = mLeft;  
    int oldT = mTop;  
    int oldB = mBottom;  
    int oldR = mRight;  

    // setFrame() / setOpticalFrame():确定View自身的位置
    // 即初始化四个顶点的值,然后判断当前View大小和位置是否发生了变化并返回  
 boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    //如果视图的大小和位置发生变化,会调用onLayout()
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {  

        // onLayout():确定该View所有的子View在父容器的位置     
        onLayout(changed, l, t, r, b);      
  ...

}  

上面看出通过 setFrame() / setOpticalFrame():确定View自身的位置,通过onLayout()确定子View的布局。
setOpticalFrame()内部也是调用了setFrame(),所以具体看setFrame()怎么确定自身的位置布局。

protected boolean setFrame(int left, int top, int right, int bottom) {
    ...
// 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
// 即确定了视图的位置
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;

    mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
}

确定了自身的位置后,就要通过onLayout()确定子View的布局。onLayout()是一个可继承的空方法。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

如果当前View就是一个单一的View,没有子View,就不需要实现该方法。

如果当前View是一个ViewGroup,就需要实现onLayout方法,该方法的实现个自定义ViewGroup时其特性有关,必须自己实现。

由此便完成了一层层的的布局工作。

View的布局流程:

image

Draw流程

View的绘制过程遵循如下几步:
①绘制背景 background.draw(canvas)

②绘制自己(onDraw)

③绘制Children(dispatchDraw)

④绘制装饰(onDrawScrollBars)

从源码中可以清楚地看出绘制的顺序。

public void draw(Canvas canvas) {
// 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
// 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制。
// 如果自定义的视图确实要复写该方法,那么需要先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制。
    ...
    int saveCount;
    if (!dirtyOpaque) {
          // 步骤1: 绘制本身View背景
        drawBackground(canvas);
    }

        // 如果有必要,就保存图层(还有一个复原图层)
        // 优化技巧:
        // 当不需要绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过
        // 因此在绘制的时候,节省 layer 可以提高绘制效率
        final int viewFlags = mViewFlags;
        if (!verticalEdges && !horizontalEdges) {

        if (!dirtyOpaque) 
             // 步骤2:绘制本身View内容  默认为空实现,  自定义View时需要进行复写
            onDraw(canvas);
    
        ......
        // 步骤3:绘制子View   默认为空实现 单一View中不需要实现,ViewGroup中已经实现该方法
        dispatchDraw(canvas);
  
        ........

        // 步骤4:绘制滑动条和前景色等等
        onDrawScrollBars(canvas);

       ..........
        return;
    }
    ...    
}

无论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了 dispatchDraw()方法,而在单一子View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。
View绘制流程:
image

总结

通过绘制流程的分析,可以看出,androidView的绘制是符合直觉的。我们画画的时候是不是现在脑子里想好要画多大,再决定从哪里开始绘制,最后拿起笔进行绘制呢。

如果要实现自定义View,根据需求的不同,我们可以自定义实现不同的方法来达到想要的效果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章