Android view的绘制流程

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开发艺术探索》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 148,637评论 1 318
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 63,443评论 1 266
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 99,164评论 0 218
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,075评论 0 188
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,080评论 1 266
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,365评论 1 184
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,901评论 2 283
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,649评论 0 176
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,122评论 0 223
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,734评论 2 225
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,093评论 1 236
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,548评论 2 222
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,028评论 3 216
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,765评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,291评论 0 178
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,162评论 2 239
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,293评论 2 242

推荐阅读更多精彩内容