从开发人员角度,我们可以知道,整个App主要有各个活动、服务等相关组成,而用户交互的基础来自于界面。存在用户交互就离不开界面,界面又依赖于各个View和ViewGroup所组成,也因此,对于View的绘制流程,对于我们自定义View也好,活动界面的创建流程等都是十分重要的知识。在这里我们将其简单的梳理一遍。本节的章节目录如下:
1、视图结构(又称:android视图构成)
2、View绘制主要流程
3、View绘制流程之大小测量——Measure
4、View绘制流程之布局(位置)测量——Layout
5、View绘制流程之内容绘制——Draw
6、View提供的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的绘制流程了!
2、View绘制主要流程
View绘制大致可以分为三个流程,分别是measure(测量),layout(布局),draw(绘制),这三者的顺序就是measure(测量)->layout(布局)->draw(绘制)。
measure: 判断是否需要重新计算View的大小(宽高),需要的话则计算;
layout: 判断是否需要重新计算View的位置,需要的话则计算;
draw: 判断是否需要重新绘制View,需要的话则重绘制。
View的简要绘制流程图当然上图是高度抽象化的简要流程,对于我们陌生的时候,作为熟悉的借鉴是十分友好的,而且也十分符合我们的章节流程。不过对于开发人员来说,我们常常遇见View里面的各种各样的方法,其中对应方法对应的时机更需要我们去了解,才能更好地运用,因此我把大致的方法流程图也贴出来:
View绘制具体方法流程图在这里补充一下,树的遍历是有序的,由父视图到子视图,每一个ViewGroup 负责测绘它所有的子视图,而最底层的 View 会负责测绘自身。所以,无论是Measure或者Layout都是自上而下地进行遍历。
3、View绘制流程之大小测量——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():存储测量后的宽和高。
4、View绘制流程之布局(位置)测量——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的位置。
5、View绘制流程之内容绘制——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的装饰绘制。
6、View提供的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
网友评论