Android 07--View的绘制机制简介

    从开发人员角度,我们可以知道,整个App主要有各个活动、服务等相关组成,而用户交互的基础来自于界面。存在用户交互就离不开界面,界面又依赖于各个View和ViewGroup所组成,也因此,对于View的绘制流程,对于我们自定义View也好,活动界面的创建流程等都是十分重要的知识。在这里我们将其简单的梳理一遍。本节的章节目录如下:

1视图结构(又称:android视图构成

2View绘制主要流

3View绘制流程之大小测量——Measure

4View绘制流程之布局(位置)测量——Layout

5View绘制流程之内容绘制——Draw

6View提供的API控制视图的部分常用方法

    从上述的步骤,我们逐步讲解一下,构建一个大致的知识网络。

1视图结构(又称:android视图构成

Activity窗口简要示意图

    从上图来看,我们可以看到一个比较清晰的构成,我们来对上图做出简单解释:

    DecorView是一个应用窗口的根容器,它本质上是一个FrameLayout。DecorView有唯一一个子View,它是一个垂直LinearLayout,包含两个子元素,一个是TitleView(ActionBar的容器),另一个是ContentView(窗口内容的容器)。关于ContentView,它是一个FrameLayout(android.R.id.content),我们平常用的setContentView就是设置它的子View。上图还表达了每个Activity都与一个Window(具体来说是PhoneWindow)相关联,用户界面则由Window所承载。

在这里我们先讲解一下涉及到的部分比较陌生却又重要的角色(如觉繁琐,可自行跳过):

    Window:窗口的概念上来看是一个比较宏大的思想,指代屏幕上用于绘制各种UI元素及响应用户输入事件的一个矩形区域。通常具备以下两个特点:

    1、独立绘制,不与其它界面相互影响;

    2、不会触发其它界面的输入事件;

    在Android系统中,窗口是独占一个Surface实例的显示区域,每个窗口的Surface由WindowManagerService分配。我们可以把Surface看作一块画布,应用可以通过Canvas或OpenGL在其上面作画。画好之后,通过SurfaceFlinger将多块Surface按照特定的顺序(即Z-order/Z 轴)进行混合,而后输出到FrameBuffer中,这样用户界面就得以显示。

    其中,窗口在Android Framework中的实现为android.view.Window这个抽象类,这个抽象类是对Android系统中的窗口的抽象。它有三个核心组件构成:

    1、WindowManager.LayoutParams:窗口的布局参数;

    2、Callback: 窗口的回调接口,通常由Activity实现;

    3、ViewTree: 窗口所承载的控件树。


    PhoneWindow:类android.view.Window的一个实现类,也是其唯一实现类。

    我们平时调用setContentView()方法设置Activity的用户界面时,实际上就完成了对所关联的PhoneWindow的ViewTree的设置。我们还可以通过Activity类的requestWindowFeature()方法来定制Activity关联PhoneWindow的外观,这个方法实际上做的是把我们所请求的窗口外观特性存储到了PhoneWindow的mFeatures成员中,在窗口绘制阶段生成外观模板时,会根据mFeatures的值绘制特定外观。

    内部包含了一个DecorView对象,该DectorView对象是所有应用窗口(Activity界面)的根View。 简而言之,PhoneWindow类是把一个FrameLayout类即DecorView对象进行一定的包装,将它作为应用窗口的根View,并提供一组通用的窗口操作接口。


    DecorView:是PhoneWindow类的内部类(后面版本将DecorView拿出来了)。该类是一个FrameLayout的子类,并且是PhoneWindow的子类,该类就是对普通的FrameLayout进行功能的扩展,更确切点可以说是修饰(Decor的英文全称是Decoration,即“修饰”的意思),比如说添加TitleBar(标题栏),以及TitleBar上的滚动条等 。最重要的一点是,它是所有应用窗口的根View 。它主要有以下功能:

    1、Dispatch ViewRoot分发来的key、touch、trackball等外部事件;

    2、DecorView有一个直接的子View,我们称之为System Layout,这个View是从系统的Layout.xml中解析出的,它包含当前UI的风格,如是否带title、是否带ActionBar等。可以称这些属性为Window decorations。

    3、作为PhoneWindow与ViewRoot之间的桥梁,ViewRoot通过DecorView设置窗口属性。可以这样获取 View:view =getWindow().getDecorView();

 DecorView只有一个子元素为LinearLayout。代表整个Window界面,包含通知栏,标题栏,内容显示栏三块区域。DecorView里面TitleView:标题,可以设置requestWindowFeature(Window.FEATURE_NO_TITLE)取消掉,ContentView:是一个id为content的FrameLayout。我们平常在Activity使用的setContentView就是设置在这里,也就是在FrameLayout上;


    ViewRoot:在介绍View的绘制前,首先我们需要知道是谁负责执行View绘制的整个流程。实际上,View的绘制是由ViewRoot来负责的。每个应用程序窗口的decorView都有一个与之关联的ViewRoot对象,这种关联关系是由WindowManager来维护的。(又一说DecorView是ViewRoot与WindowManager的关联纽带,总之这三者是依据彼此进行相互联系,所以这两种说法应该不冲突)。

    那么decorView与ViewRoot的关联关系是在什么时候建立的呢?答案是Activity启动时,ActivityThread.handleResumeActivity()方法中建立了它们两者的关联关系。这里我们不具体分析它们建立关联的时机与方式,感兴趣的同学可以参考相关源码。

    在这里,我们就可以知道我们通常的setContentView,其实就是将我们对应的xml布局界面制定添加到DecorView的子元素ContentView里面而已。当然,我们也借此需要明确的一点就是,setContentView并不是绘制流程的开始(虽然他是我们主要界面的指定),setContentView()方法所处的时间点,Activity只是完成了ContentView的创建,还未开始绘制。那View绘制的起点是在什么时候呢?

    当建立好了decorView与ViewRoot的关联后,ViewRoot类的requestLayout()方法会被调用,以完成应用程序用户界面的初次布局。该方法中调用了scheduleTraversals()方法来调度一次完成的绘制流程,该方法会向主线程发送一个“遍历”消息,最终会导致ViewRootImpl的performTraversals()方法被调用。从这里开始,系统就开始了View的绘制流程了!

2View绘制主要流

    View绘制大致可以分为三个流程,分别是measure(测量),layout(布局),draw(绘制),这三者的顺序就是measure(测量)->layout(布局)->draw(绘制)。

    measure: 判断是否需要重新计算View的大小(宽高),需要的话则计算;

    layout: 判断是否需要重新计算View的位置,需要的话则计算;

    draw: 判断是否需要重新绘制View,需要的话则重绘制。

View的简要绘制流程图

    当然上图是高度抽象化的简要流程,对于我们陌生的时候,作为熟悉的借鉴是十分友好的,而且也十分符合我们的章节流程。不过对于开发人员来说,我们常常遇见View里面的各种各样的方法,其中对应方法对应的时机更需要我们去了解,才能更好地运用,因此我把大致的方法流程图也贴出来:

View绘制具体方法流程图

    在这里补充一下,树的遍历是有序的,由父视图到子视图,每一个ViewGroup 负责测绘它所有的子视图,而最底层的 View 会负责测绘自身。所以,无论是Measure或者Layout都是自上而下地进行遍历。

3View绘制流程之大小测量——Measure

    我们也知道屏幕和其他相关因素的限制,View的大小宽高不能随心所欲,也因此我们需要进行测量,前头也说明了,Measure的目的就是为了测量View的宽高。对于开发人员,我们最基础与常见的布局宽高设置,有以下几种值选定:

    1、具体值:以px或者dp为单位

    2、fill _ parent:这个已经过时,强制性使子视图的大小扩展至与父视图大小相等(不含padding )

    3、match _ parent:特性和fill_parent相似,Android版本大于2.3使用

    4、wrap _ content:自适应大小,强制性地使视图扩展以便显示其全部内容(含padding )

    接下来,我们就要着重讲一下测量模式了,就如上文所述,Measure是自上而下的,而且父布局也可能对其进行限制,这种时候就通过测量模式进行传递约束了。而测量模式与测量大小,统一组成测量规格:

  MeasureSpec:MeasureSpec(View的内部类)测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成。其中SpecMode只有三种值:

    MeasureSpec.EXACTLY    //确定模式,父View希望子View的大小是确定的,由specSize决定;

    MeasureSpec.AT_MOST   //最多模式,父View希望子View的大小最多是specSize指定的值;

    MeasureSpec.UNSPECIFIED    //未指定模式,父View完全依据子View的设计值来决定;

 注意对于一个ViewGroup或者View的宽高而言,都一一对应一个MeasureSpec。

    当然,通过“父亲(View)”的测量模式与测量宽高,结合本身(子View)的LayoutParams可以进行本身(子View)的测量宽高,其中主要存在于9种情况(我将9中情况单独列出来,再将结果列出来,有兴趣的可以自己推导一下):

9种情况条件图
9种情况结果图

通过上述介绍,我们大致知道测量规格里的测量大小于测量模式的影响与作用了,让我们继续看看测量的流程与对应的作用。首先是View的测量过程:

View的测量流程图

    我们简单介绍一下上述的几个方法的作用

    1、measure()基本测量逻辑的判断,为 final 类型,不可被复写。但 measure 调用链最终会回调 View/ViewGroup 对象的 onMeasure()方法,因此自定义视图时,只需要复写 onMeasure() 方法即可;

    2、onMeasure()该方法就是我们自定义视图中实现测量逻辑的方法,该方法的参数是父视图对子视图的 width 和 height 的测量要求。在我们自身的自定义视图中,要做的就是根据该widthMeasureSpec 和heightMeasureSpec 计算视图的 width 和 height,不同的模式处理方式不同。

    3、setMeasuredDimension()存储测量后的宽和高。在 onMeasure(int widthMeasureSpec,

int heightMeasureSpec) 方法中调用,将计算得到的尺寸,传递给该方法,测量阶段即结束。该方法也是必须要调用的方法,否则会报异常。在我们在自定义视图的时候,不需要关心系统复杂的Measure 过程的,只需调用setMeasuredDimension()设置根据 MeasureSpec 计算得到的尺寸即可,你可以参考 ViewPagerIndicator 的 onMeasure 方法。

    4、getDefaultSize()根据View宽/高的测量规格计算View的宽/高。

在自定义View时,我们常常会利用以下两个方法:

    getDefaultSize() = 计算View的宽/高值;

    setMeasuredDimension() = 存储测量后的View宽 / 高,注意:如上文所说,此方法必须在onMeasure()方法中调用,否则会发生异常。

    这里我们再补充一下,再OnMeasure与OnMeasureDimension方法之间,就是在测量的过程种,如果是ViewGroup又将多处对子View的遍历与合并:

ViewGroup测量流程图

    1、measure():基本测量逻辑的判断。

    2、onMeasure():遍历所有的子View进行测量,如何遍历子View进行测量呢,就是调用measureChildren()方法,当所有的子View测量完成后,将会合并所有子View的尺寸最终计算出ViewGroup的尺寸。

    3、measureChildren():遍历子View并对子View进行测量,后续会调用measureChild()方法。

    4、measureChild():计算出单个子View的MeasureSpec,通过调用getChildMeasureSpce()方法实现,调用每个子View的measure()方法进行测量。

    5、getChildMeasureSpec():计算出子View的MeasureSpec。

    6、setMeasuredDimension():存储测量后的宽和高。

4View绘制流程之布局(位置)测量——Layout

    Layout的目的就是确认View&ViewGroup的位置。也就是计算View&ViewGroup的四个顶点的位置,left,top,right,bottom。同样,我们先看看View的测量过程:

View的布局测量流程

 layout()调用layout()方法主要为了计算View自身的位置,在此方法调用路径中有一个方法特别重要,这个方法就是setFrame(),它的作用就是根据传入的4个位置值,设置View本身的四个顶点位置,也就是用来确定最终View的位置的。接下来就是回调onLayout()方法。

 onLayout()对于View的onLayout()方法来说,它是一个空实现。为什么View的onLayout()方法是空实现呢?因为onLayout()方法作用是计算此VIew的子View的位置,对于单一的View而言,它并不存在子View,因此它肯定是空实现啦!

    我们再看看ViewGroup的布局测量过程:

ViewGroup的布局测量流程

layout()

调用layout()方法计算ViewGroup自身的位置,在此方法调用路径中有一个方法特别重要,这个方法就是setFrame(),它的作用就是根据传入的4个位置值,设置View本身的四个顶点位置,也就是用来确定最终View的位置的。接下来就是回调onLayout()方法。

  onLayout()对于ViewGroup而言,它不仅仅要确认自身的位置,它还要计算它的子View的位置,因此onLayout的作用就是遍历并计算每个子View的位置。

5View绘制流程之内容绘制——Draw

    Draw过程的目的绘制View&ViewGroup的视图。我们先看看View的绘制过程:

View的绘制流程图

    1、draw():绘制View自身。

    2、drawBackground():绘制View自身的背景。

    3、onDraw():绘制View自身的内容。

    4、dispatchDraw():对于View而言,它是空实现,因为它的作用是绘制子View的,因为单一的View没有子View,因此它是空实现。

    5、onDrawScrollBars():它是绘制滑动条等装饰的,比如ListView的滑动条。

ViewGroup的绘制流程:

ViewGroup的绘制流程图

    1、draw():绘制ViewGroup自身。

    2、drawBackground():绘制ViewGroup自身的背景。

    3、onDraw():绘制View自身的内容。

    4、dispatchDraw():对于ViewGroup而言,它是存在子View的,因此此方法就是用来遍历子View,然后让每个子View进入Draw过程从而完成绘制过程。

    5、onDrawScrollBars():ViewGroup的装饰绘制。

6View提供的API控制视图的部分常用方法

    invalidate和postInvalidate方法:都是请求重新绘制视图,调用draw,区别在于:

    1、invalidate在主线程调用

    2、postInvalidate是在非主线程调用

    requestLayout方法:requestLayout()方法会调用measure过程和layout过程,不会调用draw过程,也不会重新绘制任何View包括该调用者本身。

    到这里,我们就简单地将View的绘制机制/流程捋了一遍,当然还是不够全面,甚至存在偏颇或者错误的地方,如果您看到了发现了,真诚地希望您能直接指正,本人将不胜感激!上述图文主要来自与参考博客和参考书籍,本人根据自己的理解进行部分拼接与阐述,未与对应作者进行沟通,如其有合理要求可与本人联系,本人将及时改正。

---------------------

参考书籍:《Android开发艺术探索》

参考博文:

---------------------

作者:ClAndEllen  来源:CSDN   题目:Android知识体系总结之Android部分View绘制机制篇

链接:https://blog.csdn.net/ClAndEllen/article/details/79365250

---------------------

作者:absfree  来源:简书   题目:深入理解Android之View的绘制流程

链接:https://www.jianshu.com/p/060b5f68da79

---------------------

作者:lightSky  来源:CodeKK   题目:公共技术点之 View 绘制流程

链接:http://www.codekk.com/blogs/detail/54cfab086c4761e5001b253f

---------------------

作者:jackzhouyu  来源:简书   题目:Android组件View绘制流程原理分析

链接:http://www.codekk.com/blogs/detail/54cfab086c4761e5001b253f

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