View体系——View的绘制流程

ViewRoot

ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的绘制流程开始于ViewRoot的performTraversals()方法,只有经过measure、layout、draw三个流程才能最终把View绘制出来。performTraversals()依次调用performMeasure()、performLayout()和performDraw()三个方法,分别完成顶级View的绘制。其中performMeasure()会调用measure(),measure()中又调用onMeasure(),实现对其所有子元素的measure过程,这样就完成了一次measure过程;接着子元素会重复父容器的measure过程,如此反复至完成整个View树的遍历(layout和draw同理)。 

View绘制流程图


三个重要方法

measure():测量View的宽高

layout():确定View的位置

draw():绘制View

View树的绘制流程就像一个递归过程:

View树结构


MeasureSpec测量规格

1、MeasureSpec的定义

MeasureSpec是一个32位的int值,前2位表示SpecMode测量模式,后30位表示SpecSize测量大小;在一个View控件的measure过程中,系统会将这个View的LayoutParams结合父容器的MeasureSpec生成一个MeasureSpec,这个MeasureSpec即规定好了如何去测量这个View的规格大小。

2、SpecMode有三种测量模式

UNSPECIFIED:不确定模式,父控件不会对子控件有任何约束;

EXACTLY:精确模式,父容器知道View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值;对应于LyaoutParams中的match_parent或具体数值。

AT_MOST:最多模式,父容器指定了一个可用的大小SpecSize,View的大小不能超过这个值,View的最终大小要看View的具体实现;对应于LyaoutParams中的wrap_content。

3、MeasureSpec与LyaoutParams的关系

子View的MeasureSpec  =  父View的MeasureSpec  +  子View的LyaoutParams

4、View的MeasureSpec创建规则

MeasureSpec创建规则

(1)当子View的LyaoutParams为固定宽高时,不管父View的SpecMode是什么,子View的MeasureSpec = EXACTLY模式 + LyaoutParams的实际大小。

(2)当子View的LyaoutParams为match_parent时:如果父View的SpecMode为EXACTLY精确模式时,子View的MeasureSpec = EXACTLY模式 + 父容器的剩余空间;如果父View的SpecMode为AT_MOST最大模式时,子View的MeasureSpec = AT_MOST模式 + 父容器的剩余空间(不允许超过)。

(3)当子View的LyaoutParams为wrap_content时:不管父View的SpecMode是EXACTLY精确模式还是AT_MOST最大模式,子View的MeasureSpec的SpecMode总是AT_MOST模式且SpecSize不会超过父容器的剩余空间(SpecSize具体大小由子View实现)。


Measure过程

measure过程分为两种情况:第一种情况是只有一个View,那么直接通过measure()方法完成测量;第二情况是ViewGroup,除了完成自身的测量之外,还要遍历去调用子元素的measure()方法。

1、View的measure过程

view的measure过程

View的测量过程首先会调用View的measure()方法,而measure()方法又会调用onMeasure()方法实现具体的测量。onMeasure()主要通过setMeasuredDimension()方法来设置View宽高的测量值(这里的测量值并不是最终的宽高大小,最终大小要在layout阶段确定的,不过一般来说View的测量大小和最终大小是一致的)。

而View的实际测量宽高是通过getDefaultSize()方法来获取的(返回值实际上就是View的SpecSize),该方法的主要逻辑是:根据传进来的View的MeasureSpec,获取对应的SpecMode值和SpecSize值,并判断SpecMode三种测量模式下对应的View的SpecSize的取值。在这里主要关注EXACTLY和AT_MOST两种模式,这两种模式下都是直接返回View的SpecSize值,这个SpecSize就是View的测量宽高大小。

如果是getDefaultSize()方法里面的UNSPECIFIED测量模式的话,则会使用getSuggestedMinimumWidth()和getSuggestedMinimumHeight()提供的默认大小,那么默认大小是多少呢?通过getSuggestedMinimumWidth()方法可以看到:如果View没有设置背景,那么View的测量宽度等于XML布局文件中android:minWidth属性指定的值,如果没有指定则默认为0;如果View设置了背景,那么View的测量宽度等于android:minWidth属性指定的值与背景图Drawable的原始宽度(若无原始宽度则默认为0)两者中的最大值。

measure()
getDefaultSize()

在单一View的测量过程中实际起主要作用的方法有两个:
getDefaultSize():获取View的实际测量宽高;
setMeasuredDimension():存储View的实际测量宽高;

2、ViewGroup的measure过程


ViewGroup的measure过程

ViewGroup的测量过程除了完成自身的测量之外,还会遍历去调用子View的measure()方法。ViewGroup是一个抽象类,没有重写View的onMeasure()方法,所以需要子类去实现onMeasure()方法规定具体的测量规则。ViewGroup子类复写onMeasure()方法一般有如下三个步骤:
(1)遍历所有子View并测量其宽高,直接调用ViewGroup的measureChildren()方法;
(2)合并计算所有子View测量的宽高,最终得到父View的实际测量宽高;
(3)存储父View实际测量宽高值;

ViewGroup中提供了measureChildren()方法,该方法主要遍历所有的子View并调用其measureChild()方法,measureChild()主要的逻辑是:取出子View的LayoutParams参数,结合传进来的父View的MeasureSpec参数,通过getChildMeasureSpec()来计算并创建子View的MeasureSpec,而getChildMeasureSpec()方法主要获取父View测量规格中的SpecMode值和SpecSize值,并根据三种SpecMode模式结合子View的LayoutParams参数计算出子View的SpecMode值和SpecSize值,并通过makeMeasureSpec()方法创建对应的MeasureSpec测量规格,然后再把MeasureSpec传递给子View的measure()方法进行测量。如此递归下去遍历所有的子View并测量子View的宽高从而得出ViewGroup的实际测量大小。

measureChild()
getChildMeasureSpec()

ViewGroup抽象类需要子类自己实现onMeasure()方法,因为不同的ViewGroup具有不同的布局特性(LinearLayout、RelativeLayout等),导致测量的细节也不一样,所以没有跟View测量过程一样做onMeasure()的统一处理。

如何正确获取View的测量宽高:最好的方法是在onLayout()中去获取View的测量宽高和最终宽高,getMeasureWidth()和getMeasureHeight()用来获取测量宽高,getWidth()和getHeight()用来获取最终宽高。  

在Activity启动时,如何正确获取一个View的宽高:由于View的measure过程和Activity的生命周期是不同步的,所以无法保证Activity的onCreate()或者onResume()方法执行时某个View已经测量完毕,可以通过以下方法来解决:
(1)在onWindowFocusChanged()方法中获取View的宽高,该方法可能会被频繁调用;
(2)通过ViewTreeObserver的OnGlobalLayoutListener监听接口,当View树的状态发生改变或者View树内部的View的可见性发生改变时,就会回调onGlobalLayout()方法,在该方法中可以准确获取View的实际宽高;


Layout过程

View的Layout过程主要是确定View的四个顶点位置,从而确定其在容器中的位置,具体的layout过程和measure过程大致相似。

1、View的layout过程

View的layout过程

对于单一View的layout过程,首先调用View的layout()方法,在该方法中通过setFrame()方法来设定View的四个顶点的位置,即 初始化mLeft、mTop、mRight、mBottom这四个值,View的四个顶点一旦确定,那么View在父容器的位置也就确定了。接着会调用onLayout()方法确定所有子View在父容器中的位置,由于是单一View的layout过程,所以 onLayout()方法为空实现,因为没有子View(如果是ViewGroup需要子类实现 onLayout()方法)。

layout()

2、ViewGroup的layout过程

ViewGroup的layout过程

ViewGroup的layout过程首先会调用自身layout()方法,但和View的layout过程不一样的是,ViewGroup需要子类实现onLayout()方法,循环变脸所有的子View并调用其layout()方法确定子View的位置,从而最终确定ViewGroup在父容器的位置。

那么如何实现onLayout()呢?这里我们以LinearLayout为例进行分析:在onLayout()中首先判断水平或者垂直方向进入相应的处理函数,查看垂直处理函数layoutVertical()主要遍历所有子View并调用setChildFrame(),在setChildFrame()中调用子View的layout()来确定每个子View的位置,从而最终确定自身的位置。 


Draw过程

Draw过程主要是绘制View的过程,也分为单一View的绘制和ViewGroup的绘制。

View的draw过程都是从调用draw()方法开始的,该方法主要完成如下工作流程:

(1) drawBackground():绘制背景;

(2) 保存当前的canvas层(不是必须的);

(3) onDraw(): 绘制View的内容,这是一个空实现,需要子View根据要绘制的颜色、线条等样式去具体实现,所以要在子View里重写该方法;

(4) dispatchDraw(): 对所有子View进行绘制;单一View的dispatchDraw()方法是一个空方法,因为单一View没有子View,不需要实现dispatchDraw ()方法,而ViewGroup就不一样了,它实现了dispatchDraw()方法去遍历所有子View进行绘制;

(5) onDrawForeground():绘制装饰,比如滚动条;

1、View的draw过程

Draw过程


2、ViewGroup的draw过程

ViewGroup的draw过程

draw两个容易混淆的方法,两者都是刷新View的方法:

invalidate(): 不会经过measure和layout过程,只会调用draw过程;

requestLayout() :会调用measure和layout过程重新测量大小和确定位置,不会调用draw过程;


参考

《Android进阶之光》 第3章 View体系与自定义View
《Android开发艺术探索》第4章 View的工作原理

推荐阅读

自定义View Measure过程 - 最易懂的自定义View原理系列

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

推荐阅读更多精彩内容