一、概述

View的绘制是从上往下一层层迭代下来的。DecorView-->ViewGroup(--->ViewGroup)-->View ,按照这个流程从上往下,依次measure(测量),layout(布局),draw(绘制)。

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

二、View组成架构

1.Window

Window即窗口,这个概念在Android Framework中的实现为android.view.Window这个抽象类,这个抽象类是对Android系统中的窗口的抽象。在介绍这个类之前,我们先来看看究竟什么是窗口呢?

实际上,窗口是一个宏观的思想,它是屏幕上用于绘制各种UI元素及响应用户输入事件的一个矩形区域。通常具备以下两个特点:

  • 独立绘制,不与其它界面相互影响;
  • 不会触发其它界面的输入事件;

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

android.view.Window这个抽象类可以看做Android中对窗口这一宏观概念所做的约定,而PhoneWindow这个类是Framework为我们提供的Android窗口概念的具体实现。接下来我们先来介绍一下android.view.Window这个抽象类。

这个抽象类包含了三个核心组件:

  • WindowManager.LayoutParams: 窗口的布局参数;
  • Callback: 窗口的回调接口,通常由Activity实现;
  • ViewTree: 窗口所承载的控件树。

下面我们来看一下Android中Window的具体实现(也是唯一实现)——PhoneWindow。

2.PhoneWindow

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

3.setContentView()

在分析setContentView()方法前,我们需要明确:这个方法只是完成了Activity的ContentView的创建,而并没有执行View的绘制流程。
当我们自定义Activity继承自android.app.Activity时候,调用的setContentView()方法是Activity类的,源码如下:

public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
. . .
}

getWindow()方法会返回Activity所关联的PhoneWindow,也就是说,实际上调用到了PhoneWindow的setContentView()方法,源码如下:

 public void setContentView(int layoutResID) {
if (mContentParent == null) {
// mContentParent即为上面提到的ContentView的父容器,若为空则调用installDecor()生成
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
     // 具有FEATURE_CONTENT_TRANSITIONS特性表示开启了Transition
// mContentParent不为null,则移除decorView的所有子View
mContentParent.removeAllViews();
} if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
     // 开启了Transition,做相应的处理,我们不讨论这种情况
// 感兴趣的同学可以参考源码
........
} else {
      // 一般情况会来到这里,调用mLayoutInflater.inflate()方法来填充布局 
// 填充布局也就是把我们设置的ContentView加入到mContentParent中
            mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
     // cb即为该Window所关联的Activity
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
    // 调用onContentChanged()回调方法通知Activity窗口内容发生了改变
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}

4.LayoutInflater.inflate()

在上面我们看到了,PhoneWindow的setContentView()方法中调用了LayoutInflater的inflate()方法来填充布局,这个方法的源码如下:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
} public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
. . .
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
在PhoneWindow的setContentView()方法中传入了decorView作为LayoutInflater.inflate()的root参数,我们可以看到,通过层层调用,最终调用的是inflate(XmlPullParser, ViewGroup, boolean)方法来填充布局。这个方法的源码如下:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
. . .
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext; View result = root; try {
// Look for the root node.
int type;
// 一直读取xml文件,直到遇到开始标记
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
// 最先遇到的不是开始标记,报错
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
} final String name = parser.getName();
. . .
// 单独处理<merge>标签,不熟悉的同学请参考官方文档的说明
if (TAG_MERGE.equals(name)) {
// 若包含<merge>标签,父容器(即root参数)不可为空且attachRoot须为true,否则报错
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
} // 递归地填充布局
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// temp为xml布局文件的根View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
. . .
// 获取父容器的布局参数(LayoutParams)
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 若attachToRoot参数为false,则我们只会将父容器的布局参数设置给根View
temp.setLayoutParams(params);
} } // 递归加载根View的所有子View
rInflateChildren(parser, temp, attrs, true);
. . . if (root != null && attachToRoot) {
// 若父容器不为空且attachToRoot为true,则将父容器作为根View的父View包裹上来
root.addView(temp, params);
} // 若root为空或是attachToRoot为false,则以根View作为返回值
if (root == null || !attachToRoot) {
result = temp;
}
} } catch (XmlPullParserException e) {
. . .
} catch (Exception e) {
. . .
} finally { . . .
}
return result;
}
}

在上面的源码中,首先对于布局文件中的<merge>标签进行单独处理,调用rInflate()方法来递归填充布局。这个方法的源码如下:

void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
// 获取当前标记的深度,根标记的深度为0
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
// 不是开始标记则继续下一次迭代
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
// 对一些特殊标记做单独处理
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
// 对<include>做处理
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
// 对一般标记的处理
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params=viewGroup.generateLayoutParams(attrs);
// 递归地加载子View
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
} if (finishInflate) {
parent.onFinishInflate();
}
}

我们可以看到,上面的inflate()和rInflate()方法中都调用了rInflateChildren()方法,这个方法的源码如下:

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

从源码中我们可以知道,rInflateChildren()方法实际上调用了rInflate()方法。

到这里,setContentView()的整体执行流程我们就分析完了,至此我们已经完成了Activity的ContentView的创建与设置工作。接下来,我们开始进入正题,分析View的绘制流程。

5.ViewRoot

在介绍View的绘制前,首先我们需要知道是谁负责执行View绘制的整个流程。实际上,View的绘制是由ViewRoot来负责的。每个应用程序窗口的decorView都有一个与之关联的ViewRoot对象,这种关联关系是由WindowManager来维护的。那么decorView与ViewRoot的关联关系是在什么时候建立的呢?答案是Activity启动时,ActivityThread.handleResumeActivity()方法中建立了它们两者的关联关系。这里我们不具体分析它们建立关联的时机与方式,感兴趣的同学可以参考相关源码。下面我们直入主题,分析一下ViewRoot是如何完成View的绘制的。

6.View绘制的起点

当建立好了decorView与ViewRoot的关联后,ViewRoot类的requestLayout()方法会被调用,以完成应用程序用户界面的初次布局。实际被调用的是ViewRootImpl类的requestLayout()方法,这个方法的源码如下:

@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 检查发起布局请求的线程是否为主线程
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
上面的方法中调用了scheduleTraversals()方法来调度一次完成的绘制流程,该方法会向主线程发送一个“遍历”消息,最终会导致ViewRootImpl的performTraversals()方法被调用。下面,我们以performTraversals()为起点,来分析View的整个绘制流程。

三、三个阶段

View的整个绘制流程可以分为以下三个阶段:

  • measure: 判断是否需要重新计算View的大小,需要的话则计算;
  • layout: 判断是否需要重新计算View的位置,需要的话则计算;
  • draw: 判断是否需要重新绘制View,需要的话则重绘制。
    这三个子阶段可以用下图来描述:

1.Measure流程

顾名思义,就是测量每个控件的大小。

调用measure()方法,进行一些逻辑处理,然后调用onMeasure()方法,在其中调用setMeasuredDimension()设定View的宽高信息,完成View的测量操作。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
}

measure()方法中,传入了两个参数 widthMeasureSpec, heightMeasureSpec 表示View的宽高的一些信息。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

由上述流程来看Measure流程很简单,关键点是在于widthMeasureSpec, heightMeasureSpec这两个参数信息怎么获得?

如果有了widthMeasureSpec, heightMeasureSpec,通过一定的处理(可以重写,自定义处理步骤),从中获取View的宽/高,调用setMeasuredDimension()方法,指定View的宽高,完成测量工作。

MeasureSpec的确定

先介绍下什么是MeasureSpec?

MeasureSpec由两部分组成,一部分是测量模式,另一部分是测量的尺寸大小。

其中,Mode模式共分为三类

UNSPECIFIED :不对View进行任何限制,要多大给多大,一般用于系统内部

EXACTLY:对应LayoutParams中的match_parent和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,

AT_MOST :对应LayoutParams中的wrap_content。View的大小不能大于父容器的大小。

那么MeasureSpec又是如何确定的?

对于DecorView,其确定是通过屏幕的大小,和自身的布局参数LayoutParams。

这部分很简单,根据LayoutParams的布局格式(match_parent,wrap_content或指定大小),将自身大小,和屏幕大小相比,设置一个不超过屏幕大小的宽高,以及对应模式。

对于其他View(包括ViewGroup),其确定是通过父布局的MeasureSpec和自身的布局参数LayoutParams。

这部分比较复杂。以下列图表表示不同的情况:

当子View的LayoutParams的布局格式是wrap_content,可以看到子View的大小是父View的剩余尺寸,和设置成match_parent时,子View的大小没有区别。为了显示区别,一般在自定义View时,需要重写onMeasure方法,处理wrap_content时的情况,进行特别指定。

从这里看出MeasureSpec的指定也是从顶层布局开始一层层往下去,父布局影响子布局。

可能关于MeasureSpec如何确定View大小还有些模糊,篇幅有限,没详细具体展开介绍,可以看这篇文章

View的测量流程:

2.Layout流程

测量完View大小后,就需要将View布局在Window中,View的布局主要通过确定上下左右四个点来确定的。

其中布局也是自上而下,不同的是ViewGroup先在layout()中确定自己的布局,然后在onLayout()方法中再调用子View的layout()方法,让子View布局。在Measure过程中,ViewGroup一般是先测量子View的大小,然后再确定自身的大小。

public void layout(int l, int t, int r, int b) {  

    // 当前视图的四个顶点
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight; // setFrame() / setOpticalFrame():确定View自身的位置
// 即初始化四个顶点的值,然后判断当前View大小和位置是否发生了变化并返回
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); //如果视图的大小和位置发生变化,会调用onLayout()
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { // onLayout():确定该View所有的子View在父容器的位置
onLayout(changed, l, t, r, b);
... }

上面看出通过 setFrame() / setOpticalFrame():确定View自身的位置,通过onLayout()确定子View的布局。 setOpticalFrame()内部也是调用了setFrame(),所以具体看setFrame()怎么确定自身的位置布局。

protected boolean setFrame(int left, int top, int right, int bottom) {
...
// 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
// 即确定了视图的位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
}

确定了自身的位置后,就要通过onLayout()确定子View的布局。onLayout()是一个可继承的空方法。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

如果当前View就是一个单一的View,那么没有子View,就不需要实现该方法。

如果当前View是一个ViewGroup,就需要实现onLayout方法,该方法的实现个自定义ViewGroup时其特性有关,必须自己实现。

由此便完成了一层层的的布局工作。

View的布局流程:

3.Draw过程

View的绘制过程遵循如下几步:

①绘制背景 background.draw(canvas)

②绘制自己(onDraw)

③绘制Children(dispatchDraw)

④绘制装饰(onDrawScrollBars)

从源码中可以清楚地看出绘制的顺序。

public void draw(Canvas canvas) {
// 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
// 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制。
// 如果自定义的视图确实要复写该方法,那么需要先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制。
...
int saveCount;
if (!dirtyOpaque) {
// 步骤1: 绘制本身View背景
drawBackground(canvas);
} // 如果有必要,就保存图层(还有一个复原图层)
// 优化技巧:
// 当不需要绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过
// 因此在绘制的时候,节省 layer 可以提高绘制效率
final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) { if (!dirtyOpaque)
// 步骤2:绘制本身View内容 默认为空实现, 自定义View时需要进行复写
onDraw(canvas); ......
// 步骤3:绘制子View 默认为空实现 单一View中不需要实现,ViewGroup中已经实现该方法
dispatchDraw(canvas); ........ // 步骤4:绘制滑动条和前景色等等
onDrawScrollBars(canvas); ..........
return;
}
...
}

无论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了 dispatchDraw()方法,而在单一子View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。

View绘制流程:

五、总结

从View的测量、布局和绘制原理来看,要实现自定义View,根据自定义View的种类不同,可能分别要自定义实现不同的方法。但是这些方法不外乎:onMeasure()方法,onLayout()方法,onDraw()方法。

onMeasure()方法:单一View,一般重写此方法,针对wrap_content情况,规定View默认的大小值,避免于match_parent情况一致。ViewGroup,若不重写,就会执行和单子View中相同逻辑,不会测量子View。一般会重写onMeasure()方法,循环测量子View。

**onLayout()方法:**单一View,不需要实现该方法。ViewGroup必须实现,该方法是个抽象方法,实现该方法,来对子View进行布局。

**onDraw()方法:**无论单一View,或者ViewGroup都需要实现该方法,因其是个空方法

【view绘制流程】理解的更多相关文章

  1. Android应用层View绘制流程与源码分析

    1  背景 还记得前面<Android应用setContentView与LayoutInflater加载解析机制源码分析>这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原 ...

  2. 【朝花夕拾】Android自定义View篇之(一)View绘制流程

    前言 转载请申明转自[https://www.cnblogs.com/andy-songwei/p/10955062.html]谢谢! 自定义View.多线程.网络,被认为是Android开发者必须牢 ...

  3. View绘制流程

    1. View 树的绘图流程 当 Activity 接收到焦点的时候,它会被请求绘制布局,该请求由 Android framework 处理.绘制是从根节点开始,对布局树进行 measure 和 dr ...

  4. 公共技术点( View 绘制流程)

    转载地址:http://p.codekk.com/blogs/detail/54cfab086c4761e5001b253f 本文为 Android 开源项目源码解析 公共技术点中的 View 绘制流 ...

  5. Android中View绘制流程以及invalidate()等相关方法分析

    [原文]http://blog.csdn.net/qinjuning 整个View树的绘图流程是在ViewRoot.java类的performTraversals()函数展开的,该函数做的执行过程可简 ...

  6. View绘制过程理解

    假期撸了几篇自定义View相关的东西,后两天下雨呆在家里还是效率太低Orz   每个Activity都包含一个Window对象,这个Window对象通常由PhoneWindow来实现[1],而每个Wi ...

  7. Android中View绘制流程以及invalidate()等相关方法分析(转载的文章,出处在正文已表明)

    转载请注明出处:http://blog.csdn.net/qinjuning 前言: 本文是我读<Android内核剖析>第13章----View工作原理总结而成的,在此膜拜下作者 .同时 ...

  8. Android View 绘制流程(Draw) 完全解析

    前言 前几篇文章,笔者分别讲述了DecorView,measure,layout流程等,接下来将详细分析三大工作流程的最后一个流程——绘制流程.测量流程决定了View的大小,布局流程决定了View的位 ...

  9. Android中View绘制流程以及invalidate()等相关方法分析(转)

    转自:http://blog.csdn.net/qinjuning 前言: 本文是我读<Android内核剖析>第13章----View工作原理总结而成的,在此膜拜下作者 .同时真挚地向渴 ...

随机推荐

  1. CentOS6系列系统启动常见故障排查与解决方法

    情景一.内核文件损坏 /boot/vmlinuz-2.6.32-642.el6.x86_64 内核文件 1.故障现象 2.解决方法:挂载光盘,进入rescue(救援)模式 3.选择--English- ...

  2. 使用WSL连接Docker for Windows

    在Windows下安装Docker for Windows Cotana搜索功能,打开Windows的Hype-v功能(注:会影响Virtualbox和Vmware的使用)并重启电脑. 从Docker ...

  3. CSS命名规则常用的css命名规则

    CSS命名规则常用的css命名规则 头:header 内容:content/container 尾:footer 导航:nav 侧栏:sidebar 栏目:column 页面外围控制整体布局宽度:wr ...

  4. 进阶-MongoDB 知识梳理

    MongoDB 一.MongoDB简介 MongoDB是一个高性能,开源,无模式的文档型数据库,是当前NoSql数据库中比较热门的一种.它在许多场景下可用于替代传统的关系型数据库或键/值存储方式.Mo ...

  5. PAT1132: Cut Integer

    1132. Cut Integer (20) 时间限制 400 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHEN, Yue Cutting a ...

  6. 爬虫-Python爬虫常用库

    一.常用库 1.requests 做请求的时候用到. requests.get("url") 2.selenium 自动化会用到. 3.lxml 4.beautifulsoup 5 ...

  7. PHP生成腾讯云COS请求签名

    目标 使用 PHP 创建 COS 接口所需要的请求签名 步骤 按照官方示例(也许是我笨,我怎么读都觉得官方文档结构费劲,示例细节互相不挨着,容易引起歧义),请求签名应用在需要身份校验的场景,即非公有读 ...

  8. tomcat设置端口号,访问指定ip就访问指定项目

    1.修改背景: A.通常我们访问我们的web应用格式为: http://ip:端口号/项目名称 例如: http://127.0.0.1:8080/projectName B.如果想直接输入" ...

  9. NET Core 跨平台执行命令、脚本

    一.前言 我们可能会遇到需要在程序中执行一些系统命令,来获取一些信息:或者调用shell脚本..NET Core 目前已经可以跨平台执行,那么它如何跨平台执行命令呢,请看下面的讲解. 二.Proces ...

  10. Axure使用——创建折叠菜单

    1.先添加动态面板 2.往动态面板中添加矩形 3.接着先隐藏下面的矩形(也就是你要折叠起来的内容) 4.一定要注意: 5.添加动态面板的状态 6.把之前做的那个矩形全部复制到state1中 7.把之前 ...