Android view的绘制流程

96
凯玲之恋
2017.07.16 22:04* 字数 2370

Android的UI管理系统层级关系

无标题.png

PhoneWindow是Adroid系统中最基本的窗口系统,每个Activity会创建一个PhoneWindow是Activity和view的系统交互接口。DecorView本质上是一个framlayout。

View的绘制流程

View的绘制时从ViewRoot的performTraversals方法开始的,经过measure、layout和draw三个过程最终将一个view绘制出来。
performTraversals会一次调用performMeasure、performLayout、performDraw三个方法,分别完成顶级View的measure、layout、draw三大流程。
其中在performMeasure会调用measure方法,measure方法又会调用onMeasure方法,在onMeasure方法中会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中,这样就完成了一次measure过程。
接着子元素会重复父容器的measure过程,如此反复完成整个View树的遍历。
同理,performLayout和performDraw的传递流程与其类似,唯一不同的是performDraw在draw方法中通过dispatchDraw来实现。

注意

measure决定了View的宽/高,measure完成后,可以通过下面方法来获得View测量后的View的款和高

    getMeasuredWidth();
    getMeasuredHeight();

layout决定了View的四个顶点的坐标和实际的View的宽/高完

    //通过此方法数去View的四个顶点
    getTop();
    getLeft();
    getRight();
    getBottom();
    //layout后获取最终的宽高
    getWidth();
    getHeight();
//view的getWidth()方法
public final int getWidth() {
    return mRight - mLeft;
}

draw过程决定了View的显示,只有draw方法完成后View的内容才能呈现在屏幕上。

View的工作流程

Measure过程

1.如果只是一个原始的View,那么measure方法就完成其测量过程、
2.如果是一个viewgroup,除了要完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程。

1view的measure过程

View的measure过程由其measure方法来完成,measure方法是一个final类型的方法,子类不能重写。在measure方法中会去调用View的onMeasure方法。
(view的大小最终是在layout阶段确定的)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 获取值的模式
MeasureSpec.getMode(widthMeasureSpec);
// 获取值的大小
MeasureSpec.getSize(widthMeasureSpec);

    getDefaultSize()
    //setMeasuredDimension方法测量view是多宽多高的
    // 测量出最终view的值(传入多少就是多少)
    // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // onMeasure方法调用了setMeasuredDimension方法
    setMeasuredDimension(200, 200);
    // 通过这个方法来测量view是多宽多高的
    // 这个方法中的参数就代表了测量之后的宽和高--对成员方法的赋值--
}

**View的onMeasure方法

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

setMeasuredDimension设置view的宽高测量值,来看getDefaultSize方法

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;
}

可以看出在EXACTLY和AT_MOST情况下,返回的其实就是measureSpec中的specSize
如果view没有设置背景,那么view的宽度为mMinWindth,而mMinWindth对应于Android:minWidth这个属性的值。如果这个属性不指定,mMinWindth的值为0.
如果View指定了背景则View的宽度为Android:minWidth和背景的最小宽度这两者中的最大值
max(mMinWidth,mBackGroud.getMinimumWidth())

ViewGroup的measure过程

除了完成自己的measure过程外,还会遍历去调用所有子元素的measure方法,各个子元素再去递归执行这个过程。Viewgroup是一个抽象类,没有重写View的onMeasure方法,但是提供了measureChildren的方法
viewgroup的测量过程的onMeasure方法需要各个子类去具体实现

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];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

measurechild的思想就是取出子元素的LayoutParams,然后通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的Measure方法来进行测量

linearlayout的measure过程

系统会遍历元素对每个子元素与执行measureChildBeforeLyout,这个方法来调用view子元素的measure过程,这样各个子元素开始因此进入measure过程
每测量一个子元素,mTotalLength就会增加,增加的部分主要包括了子元素的高度以及元素在竖直方向的margin。
当子元素测量完毕后,LinearLayout会测量自己的大小
针对竖直的LinearLayout,它在水平方向的测量过程遵循View的测量过程
竖直方向:若采用的是matchparent或具体数值,测量与View过程。
若采用wrap_content,那么就是子元素的总和,但人不能超过它的父容器的剩余空间(当然还要考虑其padding值)
注意
在某些极端情况下,系统可能需要多次measure才能确定最终的测量宽高,在这种情况下,在onMeasure方法中拿到的测量宽高时不准确的。因此较好的习惯是在onlayout方法中取获取测量宽和高。
在activity的onCreate、onStart、onResume中均无法正确的得到某个View的宽高信息。因为View的measure与activity的生命周期不是同步的。如果View没有测量完则,获取到的View是0.
获取测量宽高的时机。
(1)Activity/View的onWindowFocusChanged
这个方法的含义是View已经初始化完毕,宽高已经准备好了。
onWindowFocusChanged会被调用多次,当activity获得焦点并失去焦点均会被调用,如果onResume和onPause方法被频繁调用,那么onWindowFocusChanged也会被调用。

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if (hasFocus){
        view.getMeasuredWidth();
        view.getMeasuredHeight();
    }
}

(2)view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View此时已经初始化好了

@Override
protected void onStart() {
    super.onStart();
    view.post(new Runnable(){

        @Override
        public void run() {
            view.getMeasuredWidth();
            view.getMeasuredHeight();
        }
    });
}

(3)ViewTreeObserver
使用ViewTreeObserver的众多接口可以完成此功能,如addOnGlobalLayoutListener,当view树的状态发生改变或者View树内部的可见性发生改变时onGlobalLayout会被调用。

    view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            view.getMeasuredWidth();
            view.getMeasuredHeight();
        }
    });

(4)手动对view进行measure来得到View的宽和高
根据View的layoutparams来分
matchparent
无法得出具体结果,根据View的measure过程,构造此种Measure需要知道parentSize,即父容器的剩余空间,而这个时候无法知道父容器的剩余空间,因此理论上无法测量出。
具体的数值

    int width = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
    int heght = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
    view.measure(width,heght);

wrapcontent
view的尺寸最大是1 << 30) - 1==2^30-1

    int width = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
    int height = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
    view.measure(width,heght);

理解MeasureSpec

在测量过程中系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后更具这个measureSpec来测量出View的宽和高
widthMeasureSpec :宽测量规格 :父容器对view的宽度的期望
heightMeasureSpec:高的测量规格:父容器对view高度期望
他两的值实际上是由两部分组成(32位):前30位表示大小+后两位来表示mode(模式) **
SpecMode和SpecSize打包成一个MeasureSpec,而SpecMode可以通过解包的形式来得出原始的SpecMode和SpecSize。
SpecMode有三个类
1)
UNSPECIFIED** 未指定(爹不管模式), 对子View的最大值没有要求 ,一般用于系统内部。
2)EXACTLY:精确的 父容器已经检测出View所需要的精确大小,这个时候最终大小就是SpecSize所指定的值。对应于LayoutParams中的match_parent 和具体的数值。
3)AT_MOST :至多 父容器指定了一个可用大小即SpecSize,View的值不能大于这个值,具体指要看不同View的具体实现。它对应于LayoutParams中的wrap_content。
在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec来确定View测量后的宽/高
1.对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定。
2.对于普通的View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,MeasureSpec一旦确定,onMeasure中就可以确定View测量的宽/高。
在对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec()方法来获得子元素的MeasureSpec。

普通view的MeasureSpec的构建规则.png

Layout过程

Layout的作用是ViewGroup用来确定子元素的位置
**layout方法确定view本身的位置,而onLayout方法确定所有子元素的位置。
当 ViewGroup的位置被确定后它在onLayout中会遍历所有的子元素并调用其Layout方法,在layout方法中onLayout方法又会被调用。

View的layout方法

layout方法的大致流程:
首先通过setFram方法来设定View的四个顶点的位置,即初始化mLeft,mRight,mTop,mBottom四个值,这四个值已确定,那么View在父容器的位置就确定了。接着会调用onLayout方法。

View和ViewGroup中的onlayout方法。

这个方法的用途是确定子元素的位置。
和onMeasure方法一样onlayout实现同具体的布局有关,
view和Viewgroup中均没有真真的实现onLayout方法
**LinearLayout的layoutVertical
遍历所有子元素并调用setChildFrame方法来为子元素指定对应的位置,childTop的值越拉越大,后面的子元素会越靠下方。
setChildFrame仅仅是调用子元素的layout方法而已。

这样在layout方法中完成自己的定位以后,就通过onLayout方法去调用子元素的layout方法,这样就会一层一层的传递下去。

draw过程

它的作用是将View绘制到屏幕上面。
View的绘制过程遵循如下几步
(1)绘制背景drawBackground.(canvas)
(2)绘制自己(ondraw)
(3)绘制children(dispatchDraw)
(4)绘制装饰(onDrawScrollBars)
view的绘制过程是通过dispatchDraw来实现的,dispatchDraw会遍历所有子元素的draw方法,如此draw事件就传递下去了

参考

《Android高级进阶》
《Android开发艺术探索》

Android
Web note ad 1