自定义view:view的绘制流程
1、view的绘制流程
当 Activity 接收到焦点的时候,它会被请求绘制布局,该请求由 Android framework 处理.绘制是从根节点开始,对布局树进行 measure 和 draw。整个 View 树的绘图流程在ViewRootImpl类的performTraversals()函数展开,该函数所做的工作可简单概况为是否需要重新计算视图大小(performMeasure)、是否需要重新安置视图的位置(performLayout)、以及是否需要重绘(而performDraw),流程图如下:

图中host为的ViewRootImpl全局变量mView。
总体来说,UI界面的绘制从开始到结束要经历几个过程:
- 测量大小,回调 onMeasure()方法
- 组件定位,回调 onLayout()方法
- 组件绘制,回调 onDraw()方法
整个绘制流程函数链调用如下:

需要说明的是,如果用户主动调用 request,只会出发 measure 和 layout 过程,而不会执行 draw 过程。接下来详细介绍各个绘制过程。
2、测量大小
performMeasure()方法负责view自身尺寸的测量。我们知道,在layout布局文件中,每一个view都必须设置layout_width和layout_height属性,属性值有三种可选模式:wrap_content、match_parent和数值,performMeasure()方法根据设置的模式计算出组件的宽度和高度。事实上,大多数情况下模式为 match_parent 和数值的时候是不需要计算的,传过来的就是父容器自己计算好的尺寸或是一个指定的精确值,只有当模式为wrap_content的时候才需要根据内容进行尺寸的测量。
performMeasure()方法的完整代码:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
对象 mView 是 View树的根视图,performMeasure()最终调用了mView的measure()方法,我们进入该方法的源代码:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
……
onMeasure(widthMeasureSpec, heightMeasureSpec);
……
}
忽略了其余的代码,只剩下了onMeasure这一行,onMeasure()方法是为组件尺寸的测量预留的功能接口,当然,也定义了默认的实现,默认实现并没有太多意义,在绝大部分情况下,onMeasure()方法必须重写。
如果测量的是容器的尺寸,而容器的尺寸又依赖于子组件的大小,所以必须先测量容器中子组件的大小,不然,测量出来的宽度和高度永远为0。
measure 过程会为一个 View 及所有子节点的mMeasuredWidth和mMeasuredHeight 变量赋值,该值可以通过getMeasuredWidth()和getMeasuredHeight()方法获得。而且这两个值必须在父视图约束范围之内,这样才可以保证所有的父视图都接收所有子视图的测量。如果子视图对于Measure得到的大小不满意的时候,父视图会介入并设置测量规则进行第二次measure。比如,父视图可以先根据未给定的 尺寸去测量每一个子视图,如果最终子视图的未约束尺寸太大或者太小的时候,父视图就会使用一个确切的大小再次对子视图进行 measure。
2.1 measure 过程传递尺寸的两个类
- ViewGroup.LayoutParams (View 自身的布局参数)
- MeasureSpecs 类(父视图对子视图的测量要求)
ViewGroup.LayoutParams这个类我们很常见,就是用来指定视图的高度和宽度等参数。对于每个视图的 height 和 width,你有以下选择:
- 具体值
- MATCH_PARENT 表示子视图希望和父视图一样大(不包含 padding 值)
- WRAP_CONTENT 表示视图为正好能包裹其内容大小(包含 padding 值)
ViewGroup 的子类有其对应的 ViewGroup.LayoutParams 的子类。比如 RelativeLayout 拥有的ViewGroup.LayoutParams的子类RelativeLayoutParams。
有时我们需要使用 view.getLayoutParams()方法获取一个视图LayoutParams,然后进行强转,但由于不知道其具体类型,可能会导致强转错误。其实该方法得到的就是其所在父视图类型的LayoutParams,比如View的父控件为RelativeLayout,那么得到的 LayoutParams 类型就为 RelativeLayoutParams。
MeasureSpecs测量规格,包含测量要求和尺寸的信息,有三种模式:
- UNSPECIFIED
父视图不对子视图有任何约束,它可以达到所期望的任意尺寸。比如 ListView、ScrollView,一般自定义 View 中用不到, - EXACTLY
父视图为子视图指定一个确切的尺寸,而且无论子视图期望多大,它都必须在该指定大小的边界内,对应的属性为 match_parent 或具体值,比如 100dp,父控件可以通过MeasureSpec.getSize(measureSpec)直接得到子控件的尺寸。 - AT_MOST
父视图为子视图指定一个最大尺寸。子视图必须确保它自己所有子视图可以适应在该尺寸范围内,对应的属性为 wrap_content,这种模式下,父控件无法确定子 View 的尺寸,只能由子控件自己根据需求去计算自己的尺寸,这种模式就是我们自定义视图需要实现测量逻辑的情况。
2.3measure 核心方法
measure(int widthMeasureSpec,intheightMeasureSpec)
该方法定义在View类中,为final类型,不可被复写,但measure调用链最终会回调 View/ViewGroup 对象的onMeasure()方法,因此自定义视图时,只需要复写onMeasure()方法即可。
onMeasure(int widthMeasureSpec,intheightMeasureSpec)
该方法就是我们自定义视图中实现测量逻辑的方法,该方法的参数是父视图对子视图的width和height的测量要求。在我们自身的自定义视图中,要做的就是根据该 widthMeasureSpec和heightMeasureSpec计算视图的width和height,不同的模式处理方式不同。
setMeasuredDimension()
测量阶段终极方法,在onMeasure(intwidthMeasureSpec,intheightMeasureSpec)方法中调用,将计算得到的尺寸,传递给该方法,测量阶段即结束。该方法也是必须要调用的方法,否则会报异常。在我们在自定义视图的时候,不需要关心系统复杂的Measure过程的,只需调用setMeasuredDimension()设置根据MeasureSpec计算得到的尺寸即可,你可以参考ViewPagerIndicator 的 onMeasure 方法。
3、确定子view的位置
performLayout()方法用于确定子组件的位置,所以,该方法只针对 ViewGroup 容器类。作为容器,必须为容器中的子View精确定义位置和大小。该方法的源码如下:
private void performLayout(WindowManager.LayoutParams lp,int desiredWindowWidth, int desiredWindowHeight){
……
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
……
for (int i = 0; i < numValidRequests; ++i) {
final View view = validLayoutRequesters.get(i);
view.requestLayout();
}
}
代码中的 host 是 View树中的根视图(DecroView),也就是最外层容器,容器的位置安排在左上角(0,0),其大小默认会填满mContentParent容器。我们重点来看一下 layout()方法的源码:
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags &PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
……
}
……
}
在 layout()方法中,在定位之前如果需要重新测量view的大小,则先调用onMeasure()方法,接下来执行setOpticalFrame()或setFrame()方法确定自身的位置与大小,此时只是保存了相关的值,与具体的绘制无关。随后,onLayout()方法被调用,该方法是空方法。
onLayout()方法在这里的作用是当前组件为ViewGroup时,负责定位ViewGroup中的子组件,这其实是一个递归的过程,如果子view也是一个ViewGroup,该ViewGroup依然要负责他的子vew的定位,依此类推,直到所有的view都定位完成为止。也就是说,从最顶层的DecorView开始定位,像多米诺骨牌一样,从上往下驱动,最后每一个view都放到了他应该出现的位置上。onLayout()方法和上节的onMeasure()方法一样,是为开发人员预留的功能扩展接口,自定义ViewGroup时,该方法必须重写。
首先要明确的是,子视图的具体位置都是相对于父视图而言的。View的onLayout方法为空实现,而 ViewGroup的onLayout为abstract,因此,如果自定义的View 要继承 ViewGroup 时,必须实现onLayout 函数。
在 layout 过程中,子视图会调用getMeasuredWidth()和getMeasuredHeight()方法获取到measure过程得到的mMeasuredWidth和mMeasuredHeight,作为自己的width和height。然后调用每一个子视图的layout(l,t,r,b)函数,来确定每个子视图在父视图中的位置。
4、绘制组件
performDraw()方法执行view的绘制功能,view的绘制是一个十分复杂的过程,不仅仅绘制view本身,还要绘制背景、滚动条,好消息是每个view只需要负责自身的绘制,而且一般来说,ViewGroup不需要绘制。
和measure和layout一样,draw过程也是在ViewRoot的performTraversals()的内部发起的,其调用顺序在measure()和layout()之后,同样的,performTraversals()发起的draw过程最终会调用到mView的draw()函数,这里的mView对于Actiity来说就是PhoneWindow.DecorView。
下面将转到mView.draw(),之前提到mView.draw()调用的就是View.java的默认实现,View类中的draw函数体现了View绘制的核心流程,因此我们下面重点来看下View.java中draw的调用流程:
public void draw(Canvas canvas) {
...
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
...
background.draw(canvas);
...
// skip step 2 & 5 if possible (common case)
...
// Step 2, save the canvas' layers
...
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
...
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
canvas.drawRect(left, top, right, top + length, p);
}
...
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
}
对于View.java和ViewGroup.java,onDraw()默认都是空实现,因为具体View本身长什么样子是由View的设计者来决定的,默认不显示任何东西。
View.java中dispatchDraw()默认为空实现,因为其不包含子视图,而ViewGroup重载了dispatchDraw()来对其子视图进行绘制,通常应用程序不应该对dispatchDraw()进行重载,其默认实现体现了View系统绘制的流程。那么,接下来我们继续分析下ViewGroup中dispatchDraw()的具体流程:
@Override
protected void dispatchDraw(Canvas canvas) {
...
if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
for (int i = 0; i < count; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
} else {
for (int i = 0; i < count; i++) {
final View child = children[getChildDrawingOrder(count, i)];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
}
......
}
dispatchDraw()的核心代码就是通过for循环调用drawChild()对ViewGroup的每个子视图进行绘制,上述代码中如果FLAG_USE_CHILD_DRAWING_ORDER为true,则子视图的绘制顺序通过getChildDrawingOrder来决定,默认的绘制顺序即是子视图加入ViewGroup的顺序,而我们可以重载getChildDrawingOrder函数来更改默认的绘制顺序,这会影响到子视图之间的重叠关系。
drawChild()的核心过程就是为子视图分配合适的cavas剪切区,剪切区的大小正是由layout过程决定的,而剪切区的位置取决于滚动值以及子视图当前的动画。设置完剪切区后就会调用子视图的draw()函数进行具体的绘制,如果子视图的包含SKIP_DRAW标识,那么仅调用dispatchDraw(),即跳过子视图本身的绘制,但要绘制视图可能包含的字视图。完成了dispatchDraw()过程后,View系统会调用onDrawScrollBars()来绘制滚动条。
view的绘制最终是通过Canvas类完成的,该类定义了若干个绘制图形的方法,通过Paint类配置绘制参数,便能绘制出各种图案效果。有时候为了提高绘图的性能,使用了Surface技术,Surface提供了一套双缓存机制,能大大加快绘图效率,而我们绘图时需要的 Canvas 对象也由是 Surface创建的。
View 类的 draw()方法是组件绘制的核心方法,主要做了下面几件事:
- 绘制背景:background.draw(canvas)
- 绘制自己:onDraw(canvas)
- 绘制子视图:dispatchDraw(canvas)
- 绘制滚动条:onDrawScrollBars(canvas)
组件的绘制也是一个递归的过程,说到底Activity的UI界面的根一定是容器,根容器绘制结束后开始绘制子组件,子组件如果是容器继续往下递归绘制,否则将子组件绘制出来……直到所有的组件正确绘制为止。
自定义view:view的绘制流程的更多相关文章
- Android View 的添加绘制流程 (二)
概述 上一篇 Android DecorView 与 Activity 绑定原理分析 分析了在调用 setContentView 之后,DecorView 是如何与 activity 关联在一起的,最 ...
- android View层的绘制流程
还记得前面<Android应用setContentView与LayoutInflater加载解析机制源码分析>这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原理,记不记得 ...
- 【朝花夕拾】Android自定义View篇之(一)View绘制流程
前言 转载请申明转自[https://www.cnblogs.com/andy-songwei/p/10955062.html]谢谢! 自定义View.多线程.网络,被认为是Android开发者必须牢 ...
- 深入理解 Android 之 View 的绘制流程
概述 本篇文章会从源码(基于Android 6.0)角度分析Android中View的绘制流程,侧重于对整体流程的分析,对一些难以理解的点加以重点阐述,目的是把View绘制的整个流程把握好,而对于特定 ...
- Android中View绘制流程以及invalidate()等相关方法分析
[原文]http://blog.csdn.net/qinjuning 整个View树的绘图流程是在ViewRoot.java类的performTraversals()函数展开的,该函数做的执行过程可简 ...
- Android视图绘制流程完全解析,带你一步步深入了解View(二)
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/16330267 在上一篇文章中,我带着大家一起剖析了一下LayoutInflater ...
- View (三) 视图绘制流程完全解析
相 信每个Android程序员都知道,我们每天的开发工作当中都在不停地跟View打交道,Android中的任何一个布局.任何一个控件其实都是直接或间 接继承自View的,如TextView.Butto ...
- Android中View绘制流程以及invalidate()等相关方法分析(转载的文章,出处在正文已表明)
转载请注明出处:http://blog.csdn.net/qinjuning 前言: 本文是我读<Android内核剖析>第13章----View工作原理总结而成的,在此膜拜下作者 .同时 ...
- 【view绘制流程】理解
一.概述 View的绘制是从上往下一层层迭代下来的.DecorView-->ViewGroup(--->ViewGroup)-->View ,按照这个流程从上往下,依次measure ...
- 【转】深入理解Android之View的绘制流程
概述 本篇文章会从源码(基于Android 6.0)角度分析Android中View的绘制流程,侧重于对整体流程的分析,对一些难以理解的点加以重点阐述,目的是把View绘制的整个流程把握好,而对于特定 ...
随机推荐
- 20145211 《网络渗透》MS08_067安全漏洞
20145211 <网络渗透>MS08_067安全漏洞 一.实验原理 ms08_067是服务器服务中一个秘密报告的漏洞,于2008年被发现.攻击者利用靶机默认开放的SMB服务的445端口, ...
- springboot集成shiro 前后端分离 统一处理shiro异常
在前后端分离的情况下,shiro一些权限异常处理会返回401之类的结果,这种结果不好统一管理.我们希望的结果是统一管理,所有情况都受我们控制 就算权限验证失败,我们也希望返回200,并且返回我们定义的 ...
- 爬虫之动态HTML处理(Selenium与PhantomJS )动态页面模拟点击
动态页面模拟点击 #!/usr/bin/env python # -*- coding:utf-8 -*- # python的测试模块 import unittest from selenium im ...
- Zookeeper Zkclient客户端
Zkclient是对Zookeeper的原生API进行了包装,实现了超时重连.Watcher反复注册等功能,它可以实现递归创建,删除节点,但是zkClient不能递归给节点赋值. 主要的api如下: ...
- [转载]Eclipse的常用快捷键
常用的快捷键 ctrl+1:快速修复错误 ctrl+shift+L :查看快捷键 alt+?或alt+/:自动补全代码或者提示代码 ctrl+o:快速outline视图 ctrl+shift+r:打开 ...
- 新浪云连接数据库php
一般数据库连接$con = mysql_connect("localhost", "root", ""); 而新浪云共享数据库 <?p ...
- 缓存技术内部交流_04_Cache Aside续篇
额外参考资料: http://www.ehcache.org/documentation/3.2/expiry.html F. Cache Aside 模式的问题:缓存过期 有时我们会在上线前给缓存系 ...
- javascript深入浅出
第一章 数据类型 1,六种数据类型:原始类型(number,string,boolean,null,undefined) + object对象(Function Array Date) 2,隐式转换: ...
- panda2
pandas是python为数据分析建造的可靠工具,很多地方和R语言有想通之处.数据分析并不是工具越高深越好,excel,R,python都是针对不同情况的不同工具,各有各的优缺点,就像你要搭一个架子 ...
- selenium的三种等待时间设置
为了提高脚本的稳定性,我们需要在脚本中增加等待时间 第一种:强制等待 Thread.sleep():固定休眠时间设置,Java的Thread类里提供了休眠方法sleep,导入包后就能使用 sleep( ...