Android View绘制流程
框架分析
在之前的下拉刷新中,小结过触屏消息先到WindowManagerService(Wms)然后顺次传递给ViewRoot(派生自Handler),经decor view到Activity再传递给指定的View,这次整理View的绘制流程,通过源码可知,这个过程应该没有涉及到IPC(或者我没有发现),需要绘制时在UI线程中通过ViewRoot发送一个异步请求消息,然后ViewRoot自己接收并不处理这个消息。
在正式进入View绘制之前,首先需要明确一下Android UI的架构组成,偷图如下:
上述架构很清晰的呈现了Activity、Window、DecorView(及其组成)、ViewRoot和WMS之间的关系,我通过源码简单理了下从启动Activity到创建View的过程,大致如下
在上图中,performLaunchActivity函数是关键函数,除了新建被调用的Activity实例外,还负责确保Activity所在的应用程序启动、读取manifest中关于此activity设置的主题信息以及上图中对“6.onCreate”调用也是通过对mInstrumentation.callActivityOnCreate来实现的。图中的“8. mContentParent.addView”其实就是架构图中phoneWindow内DecorView里面的ContentViews,该对象是一个ViewGroup类实例。在调用AddView之后,最终就会触发ViewRoot中的scheduleTraversals这个异步函数,从而进入ViewRoot的performTraversals函数,在performTraversals函数中就启动了View的绘制流程。
performTraversals函数在2.3.5版本源码中就有近六百行的代码,跟我们绘制view相关的可以抽象成如下的简单流程图
流程图中的host其实就是mView,而ViewRoot中的这个mView其实就是DecorView,之所以这么说,又得具体看源码中ActivityThread的handleResumeActivity函数,在这里我就不展开了。上述流程主要调用了View的measure、layout和draw三个函数。
measure过程分析
因为DecorView实际上是派生自FrameLayout的类,也即一个ViewGroup实例,该ViewGroup内部的ContentViews又是一个ViewGroup实例,依次内嵌View或ViewGroup形成一个View树。所以measure函数的作用是为整个View树计算实际的大小,设置每个View对象的布局大小(“窗口”大小)。实际对应属性就是View中的mMeasuredHeight(高)和mMeasureWidth(宽)。
在View类中measure过程主要涉及三个函数,函数原型分别为
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
前面两个函数都是final类型的,不能重载,为此在ViewGroup派生的非抽象类中我们必须重载onMeasure函数,实现measure的原理是:假如View还有子View,则measure子View,直到所有的子View完成measure操作之后,再measure自己。ViewGroup中提供的measureChild或measureChildWithMargins就是实现这个功能的。
在具体介绍测量原理之前还是先了解些基础知识,即measure函数的参数由类measureSpec的makeMeasureSpec函数方法生成的一个32位整数,该整数的高两位表示模式(Mode),低30位则是具体的尺寸大小(specSize)。
MeasureSpec有三种模式分别是UNSPECIFIED, EXACTLY和AT_MOST,各表示的意义如下
如果是AT_MOST,specSize代表的是最大可获得的尺寸;
如果是EXACTLY,specSize代表的是精确的尺寸;
如果是UNSPECIFIED,对于控件尺寸来说,没有任何参考意义。
那么对于一个View的上述Mode和specSize值默认是怎么获取的呢,他们是根据View的LayoutParams参数来获取的:
参数为fill_parent/match_parent时,Mode为EXACTLY,specSize为剩余的所有空间;
参数为具体的数值,比如像素值(px或dp),Mode为EXACTLY,specSize为传入的值;
参数为wrap_content,Mode为AT_MOST,specSize运行时决定。
具体测量原理
上面提供的Mode和specSize只是程序员对View的一个期望尺寸,最终一个View对象能从父视图得到多大的允许尺寸则由子视图期望尺寸和父视图能力尺寸(可提供的尺寸)两方面决定。关于期望尺寸的设定,可以通过在布局资源文件中定义的android:layout_width和android:layout_height来设定,也可以通过代码在addView函数调用时传入的LayoutParams参数来设定。父View的能力尺寸归根到最后就是DecorView尺寸,这个尺寸是全屏,由手机的分辨率决定。期望尺寸、能力尺寸和最终允许尺寸的关系,我们可以通过阅读measureChild或measureChildWithMargins都会调用的getChildMeasureSpec函数的源码来获得,下面简单列表说明下三者的关系
父视图能力尺寸 |
子视图期望尺寸 |
子视图最终允许尺寸 |
EXACTLY + Size1 |
EXACTLY + Size2 |
EXACTLY + Size2 |
EXACTLY + Size1 |
fill_parent/match_parent |
EXACTLY+Size1 |
EXACTLY + Size1 |
wrap_content |
AT_MOST+Size1 |
AT_MOST+Size1 |
EXACTLY + Size2 |
EXACTLY+Size2 |
AT_MOST+Size1 |
fill_parent/match_parent |
AT_MOST+Size1 |
AT_MOST+Size1 |
wrap_content |
AT_MOST+Size1 |
UNSPECIFIED+Size1 |
EXACTLY + Size2 |
EXACTLY + Size2 |
UNSPECIFIED+Size1 |
fill_parent/match_parent |
UNSPECIFIED+0 |
UNSPECIFIED+Size1 |
wrap_content |
UNSPECIFIED+0 |
上述表格展现的是子视图最终允许得到的尺寸,显然1、4、7三项没有对Size1和Size2进行比较,所以允许尺寸是可以大于父视图的能力尺寸的,这个时候最终的视图尺寸该是多少呢?AT_MOST和UNSPECIFIED的View又该如何决策最终的尺寸呢?
通过Demo演示的得到的结果,假如Size2比Size1的尺寸大,假如不使用滚动效果的话,子视图超出部分将被裁剪掉,该父视图中如果在该子视图后面还有其他视图,那么也将被裁剪掉,但是通过调用其getVisibility还是显示该控件是可见的,所以裁剪后控件依然是有的,只是用户没办法观察到;在使用滚动效果的情况下,就能将原本被裁剪掉的控件通过滚动显示出来。
对于第二个问题,根据源码View的OnMeasure函数调用的getDefaultSize函数获知,默认情况下,控件都有一个最小尺寸,该值可以通过设置android:minHeight和android:minWidth来设置(无设置时缺省为0);在设置了背景的情况下,背景drawable的最小尺寸与前面设置的最小尺寸比较,两者取大者,作为控件的最小尺寸。在UNSPECIFIED情况下就选用这个最小尺寸,其它情况则根据允许尺寸来。不过这个是默认规则,通过demo发现,TextView在AT_MOST+Size情况下,并不是以Size作为控件的最终尺寸,结果发现在TextView的源码中,重载了onMeasure函数,有价值的代码如下:
……
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
……
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(widthSize, width);
}
……
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desired, heightSize);
}
……
至于其中的width和desired值,感兴趣的同学可以具体关注下。虽然FrameWork提供了视图默认的尺寸计算规则,但是最终的视图布局大小可以重载onMeasure函数来修改计算规则,当然也可以不计算直接通过setMeasuredDimension来设置(需要注意的是,如果通过setMeasuredDimension的同时还要调用父类的onMeasure函数,那么在调用父类函数之前调用的setMeasuredDimension会无效果)。
layout过程分析
上述measure过程达到的结果是设定了视图的高和宽,layout过程的作用就是设定视图在父视图中的四个点(分别对应View四个成员变量mLeft,mTop,mLeft,mBottom)。同样layout也是被fianl修饰符限定为不能重载,不过在ViewGroup中onLayout函数被abstract修饰,即所有派生自ViewGroup的类必须实现onLayout函数,从而实现对其包含的所有子视图的布局设定。
那么上述的measure结果与layout有什么关系,截取ViewRoot和FrameLayout两个类中onLayout函数的部分代码如下:
//ViewRoot的performTraversals函数measure之后对layout的调用代码
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
//FrameLayou的onLayout函数部分源码
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int count = getChildCount();
……
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft = parentLeft;
int childTop = parentTop;
final int gravity = lp.gravity;
if (gravity != -1) {
final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (horizontalGravity) {
case Gravity.LEFT:
childLeft = parentLeft + lp.leftMargin;
break;
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = parentRight - width - lp.rightMargin;
break;
default:
childLeft = parentLeft + lp.leftMargin;
}
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 + lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
}
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
从代码显然可知具体layout布局时,就是根据measure过程设置的高和宽,结合视图在父视图中的起始位置,再外加视图的layoutgravity属性来设置四个点的具体位置(在LinearLayout中还会增加对layoutweight属性的考虑)。这个过程相对没有measure那么复杂。
需要注意的是在自定义组合控件的时候,我们可以根据需要不用或只用部分measure过程计算得到的尺寸,具体可以看下之前做的下拉刷新控件直接重载的onLayout函数:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (getChildCount() > 2) {
throw new IllegalStateException("NPullToFreshContainer can host only two direct child");
}
View headView = getChildAt(0);
View contentView = getChildAt(1);
if(headView != null){
headView.layout(0, -HEAD_VIEW_HEIGHT + mTatolScroll, getMeasuredWidth(), mTatolScroll);// mTatolScroll是下拉的位移值
}
if(contentView != null){
contentView.layout(0, mTatolScroll, getMeasuredWidth(), getMeasuredHeight());
}
if (mFirstLayout) {
HEAD_VIEW_HEIGHT = getChildAt(0).getMeasuredHeight();
mFirstLayout = false;
}
}
draw过程分析
View的Draw过程,其实相对来说应该比measure过程更为复杂,正因为其很复杂,所以android框架层已经将draw过程考虑得相当周全,虽然view类的Draw函数没用final修饰,但是我们自定义的View,一般也不需要去重载实现它,自己目前也没有自己去draw过界面,对整个过程,只能偷别人整理的逻辑,结合源码浏览了一下,在这里做个标注。
draw()方法实现的功能流程如下:
1、调用background.draw(canvas)绘制该View的背景
2、调用onDraw(canvas)方法绘制视图本身(每个View都需要重载该方法,ViewGroup不需要实现该方法)
3、调用dispatchDraw(canvas)方法绘制子视图(ViewGroup类已经为我们重写了dispatchDraw ()的功能实现,其内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法)
4、调用onDrawScrollBars(canvas)绘制滚动条
为了说明measure、layout和draw过程的连续性,摘得draw中的源码如下
……
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
……
上述的mLeft,mTop,mLeft,mBottom就是我们在layout是设定的结果值,这里之所以要用减法获取高宽尺寸而不用measure过程设定的mMeasuredHeight和mMeasureWidth,个人感觉就是因为我们可以在代码中通过直接调用View的layout函数避开measure测算结果而导致真实高宽不等于mMeasuredHeight和mMeasureWidth这种情况。
上述代码中的mBackgroundSizeChanged是个私有成员变量,源码中只能在View的onScrollChanged(int l, int t, int oldl, int oldt) 、layout过程调用的setFrame(int left, int top, int right, int bottom) 和setBackgroundDrawable(Drawable d)这三个函数中对其修改为true。
到这里,除了具体的绘制外,我们对从Activity到View的绘制流程应该比较清楚了。
本文除了参阅源码,发现下面两篇博文帮助很大,有兴趣可以详细阅读
http://blog.csdn.net/qinjuning/article/details/7110211
http://www.cnblogs.com/bastard/archive/2012/04/10/2440577.html
验证View measure现象的demo见 http://files.cnblogs.com/franksunny/ViewDemo.rar
由于文档中的图片没有显示出来,所以上传一个pdf文档,方便查阅 http://files.cnblogs.com/franksunny/AndroidView%E7%BB%98%E5%88%B6%E6%B5%81%E7%A8%8B.pdf
Android View绘制流程的更多相关文章
- Android View 绘制流程(Draw) 完全解析
前言 前几篇文章,笔者分别讲述了DecorView,measure,layout流程等,接下来将详细分析三大工作流程的最后一个流程——绘制流程.测量流程决定了View的大小,布局流程决定了View的位 ...
- android view绘制流程 面试
一.view树的绘制流程 measure--->layout--->draw measure 1.ViewGroup.LayoutParams 指定部件的长宽 2.MeasureSpec ...
- Android View 绘制流程
Android 中 Activity 是作为应用程序的载体存在,代表着一个完整的用户界面,提供了一个窗口来绘制各种视图,当 Activity 启动时,我们会通过 setContentView 方法来设 ...
- Android View 布局流程(Layout)完全解析
前言 上一篇文章,笔者详细讲述了View三大工作流程的第一个,Measure流程,如果对测量流程还不熟悉的读者可以参考一下上一篇文章.测量流程主要是对View树进行测量,获取每一个View的测量宽高, ...
- Android View 测量流程(Measure)完全解析
前言 上一篇文章,笔者主要讲述了DecorView以及ViewRootImpl相关的作用,这里回顾一下上一章所说的内容:DecorView是视图的顶级View,我们添加的布局文件是它的一个子布局,而V ...
- Android应用层View绘制流程与源码分析
1 背景 还记得前面<Android应用setContentView与LayoutInflater加载解析机制源码分析>这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原 ...
- 【朝花夕拾】Android自定义View篇之(一)View绘制流程
前言 转载请申明转自[https://www.cnblogs.com/andy-songwei/p/10955062.html]谢谢! 自定义View.多线程.网络,被认为是Android开发者必须牢 ...
- 简单研究Android View绘制三 布局过程
2015-07-28 17:29:19 这一篇主要看看布局过程 一.布局过程肯定要不可避免的涉及到layout()和onLayout()方法,这两个方法都是定义在View.java中,源码如下: /* ...
- 简单研究Android View绘制一 测量过程
2015-07-27 16:52:58 一.如何通过继承ViewGroup来实现自定义View?首先得搞清楚Android时如何绘制View的,参考Android官方文档:How Android Dr ...
随机推荐
- 跨站点端口攻击 – XSPA(SSPA)
许多Web应用程序提供的功能将数据从其他Web服务器,由于种种原因.下载XML提要,从远程服务器,Web应用程序可以使用用户指定的URL,获取图像,此功能可能会被滥用,使制作的查询使用易受攻击的Web ...
- HDU 1104 Remainder
与前一题类似,也是BFS+记录路径, 但是有很多BUG点, 第一MOD操作与%不同i,其实我做的时候注意到了我们可以这样做(N%K+K)%K就可以化为正数,但是有一点要注意 N%K%M!=N%M%K; ...
- HDU2295 Radar (DLX)
下面的代码99%参考了这个网站http://www.cnblogs.com/183zyz/archive/2011/08/07/2130193.html 人生的第一道DLX肯定是需要作一些参考的啦. ...
- POJ 3336 Count the string (KMP+DP,好题)
参考连接: KMP+DP: http://www.cnblogs.com/yuelingzhi/archive/2011/08/03/2126346.html 另外给出一个没用dp做的:http:// ...
- 【QT】找茬外挂制作
找茬外挂制作 找茬游戏大家肯定都很熟悉吧,两张类似的图片,找里面的不同.在下眼神不大好,经常瞪图片半天也找不到区别.于是乎决定做个辅助工具来解放一下自己的双眼. 一.使用工具 Qt:主要是用来做界面的 ...
- 使用Visio进行UML建模
http://www.qdgw.edu.cn/zhuantiweb/jpkc/2009/rjkf/xmwd/Visio_UmlModel.htm#_Toc80417837 内容提纲: 1.VISIO中 ...
- mvc5 @RenderSection("scripts", required: false) 什么意思
在模板中 相当于占位符 使用方法如下 @section scripts{ //coding }
- ESASP 业界第一个最为完善的 ASP MVC框架(待续)
EchoSong 疯狂了,竟然整ASP框架. ASP就是抛弃的孩子,没人养没人疼的, 智力.四肢不全.何谈框架?? 很多ASP的前辈们要么放弃ASP 投入 ASP.net 或者 PHP怀抱.要么直接用 ...
- Android Grapics图像类体系
- angularJS seed 安装
安装nodejs 安装python 配置python 环境 安装git 配置git 环境 clone angularJS seed 代码. 环境变量如下: C:\Program Files\nodej ...