应用程序窗口内部所包含的视图对象的实际类型为DecorView。DecorView类继承了View类,是作为容器(ViewGroup)来使用的,它的实现如图1所示:

每一个应用程序窗口的视图对象都有一个关联的ViewRoot对象,这些关联关系是由窗口管理器WindowManagerImpl来维护的,如图:

简单来说,ViewRoot相当于是MVC模型中的Controller,它有以下职责:

1. 负责为应用程序窗口视图创建Surface。

2. 配合WindowManagerService来管理系统的应用程序窗口。

3. 负责管理、布局和渲染应用程序窗口视图的UI。

ViewRoot创建过程:

那么ViewRoot 和DecorView是什么关系???先看看整个绘制流程图:


总体过程如下:

绘制过程:

创建

首先,View公有的构造函数的重载形式就有四种:

  • View(Context context)    通过代码创建view时使用此构造函数,通过context参数,可以获取到需要的主题,资源等等。
  • View(Context context, AttributeSet attrs)    当通过xml布局文件创建view时会使用此构造函数,调用了3个参数的构造方法。
  • View(Context context, AttributeSet attrs, int defStyleAttr)     通过xml布局文件创建view,并采用在属性中指定的style。这个view的构造函数允许其子类在创建时使用自己的style。调用了下面四参的构造方法。
  • View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)    该构造函数可以通过xml布局文件创建view,可以采用theme属性或者style资源文件指定的style。

参数:

  • Context : view运行的上下文信息,从中可以获取到当前theme,资源文件等信息。
  • AttributeSet: xml布局文件中view标签下指定的属性集合。
  • defStyleAttr: 当前theme中的一条属性,它包含一条指向theme资源文件中style的引用。默认值为0。
  • defStyleRes: 一个style资源文件的标示,表示style的ID,当值为0或者找不到对应的theme资源时候采用默认值。

综上所述,单参的构造函数从代码创建view,其余都调用四参的构造函数根据xml布局文件创建view。我们可以在不同的地方指定属性值,例如:

直接在xml标签中中指定的attrs值,可以从AttributeSet中获取。

  • 通过在标签属性“style”中指定的资源文件。
  • 默认的defStyleAttr。
  • 默认的defStyleRes。
  • 当前theme中的默认值。

构造函数的代码过长,就不在这里贴了,主要进行的工作是:获取各项系统定义的属性,然后根据属性值初始化view的各项成员变量和事件。

一般情况下,我们自定义view的时候,根据实际情况重写构造函数时,如果只从code创建,则只用实现单参数的即可。如果需要从xml布局文件中创建,则需要实现单参数和一个多参数的就好了,因为多参数的默认调用了四参数的构造函数;然后再获取到自定义的属性进行处理就OK了。

至此,view的创建以及初始化工作完毕,然后开始绘制view的工作。那么Android系统是如何对view进行绘制的呢?

绘制

在activity获取到焦点后,会请求Android Framework根据它的布局文件进行绘制,activity需要提供所绘布局文件的根节点,然后对布局的树结构一边遍历一边进行绘制。我们都知道,ViewGroup是View的子类,它可以拥有若干子view,它的很多操作和view相同,不同的是ViewGroup负责绘制其子节点,而view则负责绘制其自身。整个遍历过程从上到下,在整个过程中,需要进行大小测量(measure函数)和定位(layout函数),然后再进行绘制。下面我们来看这些工作是如何进行的:

测定尺寸

在Android中,所有view被组织成树状结构,最顶层measure的主要工作就是负责递归测量出整个view树结构的尺寸大小,每个View的控件的实际宽高都是由父视图和本身视图决定的。

在研究源码之前,我先从整体上概况一下整个递归调用过程。从根view开始,使用measure方法中计算整个view树的大小,在该方法中调用子view的onMeasure方法。在onMeasure中主要进行两个工作:

  1. 调用setMeasuredDimension设置view自身的尺寸(mMeasureWidth和mMeasuredHeight),具体会在下面看到。
  2. 如果该view是ViewGroup,则需要继续递归调用其onMeasure方法来计算ViewGroup的子view大小。

根view通常就是一个ViewGroup,需要计算子view尺寸。首先获取到所有子view,然后调用measureChildWithMargins方法来计算子view的尺寸。在这个方法中调用了子view的measure方法。下面我们来看具体源码。

首先在measure方法中确定view的大小。这个方法被定义为final类型,不可被子类重写。在View中有一个静态内部类MeasureSpec封装了父view要传递给子View的布局参数,由size 和 mode共同组成。size即是大小,mode表示模式。(其实就是一个int值高2位表示mode,低30位表示size). mode总共有三种模式:

  • UNSPECIFIED:父view并未指定子view的大小,可随意根据开发人员需求指定view大小。
  • EXACTLY: 父view严格指定了子view的大小
  • AT_MOST: 子view的大小不超过该值
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {boolean optical = isLayoutModeOptical(this);//是否使用视觉边界布局 if (optical != isLayoutModeOptical(mParent)) {// 当view和它的父viewGroup就是否采用视觉边界布局不一致时Insets insets = getOpticalInsets();int oWidth  = insets.left + insets.right;int oHeight = insets.top  + insets.bottom;widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);}long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||widthMeasureSpec != mOldWidthMeasureSpec ||heightMeasureSpec != mOldHeightMeasureSpec) {// first clears the measured dimension flagmPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;resolveRtlPropertiesIfNeeded();int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :mMeasureCache.indexOfKey(key);if (cacheIndex < 0 || sIgnoreMeasureCache) {// measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;} else {long value = mMeasureCache.valueAt(cacheIndex);// Casting a long to int drops the high 32 bits, no mask neededsetMeasuredDimensionRaw((int) (value >> 32), (int) value);mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;}// flag not set, setMeasuredDimension() was not invoked, we raise// an exception to warn the developerif ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {throw new IllegalStateException("onMeasure() did not set the"+ " measured dimension by calling"+ " setMeasuredDimension()");}mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;}mOldWidthMeasureSpec = widthMeasureSpec;mOldHeightMeasureSpec = heightMeasureSpec;mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension}

方法接收的两个参数widthMeasureSpec和heightMeasureSpec表示view的宽高,由上一层父view计算后传递过来。view大小的测量工作在标红的onMeasure方法中进行。我们在自定义view时往往需要重写该方法,根据传入的view大小以及其内容来设定view最终显示的尺寸。

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

重写该方法时,我们需要调用setMeasuredDimension这个方法来存储已经测量好的尺寸(这里默认使用getDefalutSize),只有在调用过此方法后,才能通过getMeasuredWidth方法和getMeasuredHeight方法获取到尺寸。同时,我们要保证最后得到的尺寸不小于view的最小尺寸。我们需要注意的是,setMeasuredDimension方法必须在OnMeasure方法中调用,否则会抛出异常。

OK,measure方法至此完毕。然而,我们可以发现真正测量view大小的工作并不在此方法中进行,这里仅仅是一个测量框架,根据各种不同的情况进行判断,完成一些必要的步骤。这些步骤是必须的也是无法被开发者更改的,需要根据情况自定义的工作放在了onMeasure中由开发者完成。这样既保证了绘制流程的执行,又灵活的满足了各种需求,是典型的模板方法模式。

由于一个父view下可能有多个子view,所以measure方法不仅仅执行一次,而是在父view(viewGroup)中获取到所有子view,然后遍历调用子view的measure方法。

定位

当view的大小已经设定完毕,则需要确定view在其父view中的位置,也就是把子view放在合理的位置上。因为只有ViewGroup才包含子view,所以一般我们说起父view,肯定是在说ViewGroup。完成布局工作主要分为两部分,也是递归实现的:

  1. 在layout方法中调用setFrame设置该View视图位于父视图的坐标。
  2. 如果view是ViewGroup类型,则调用其onLayout方法完成子view布局工作。

下面来看具体源码,父view调用了子view的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); // 确定view在布局中的位置mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;ListenerInfo li = mListenerInfo;if (li != null && li.mOnLayoutChangeListeners != null) {ArrayList<OnLayoutChangeListener> listenersCopy =(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();int numListeners = listenersCopy.size();for (int i = 0; i < numListeners; ++i) {listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);}}}mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;}

该方法接收四个参数是子view相对于父view而言的上下左右位置。然而我们发现其中调用到的onLayout方法默认的实现是空的。这是因为确定view在布局的位置这个操作应该由Layout根据自身特点来完成。任何布局的定义都要重写其onLayout方法,并在其中设定子view的位置。

绘制

在进行完测定尺寸和定位之后,终于可以开始绘制了。这里的工作仍是通过递归来完成的。view调用draw方法来进行绘制,里面调用onDraw来绘制自身,如果还有子view则需要调用dispatchDraw来绘制子view。

绘制需要调用draw方法,总共分为六个步骤:

  1. 绘制背景
  2. 如果需要,保存canvas的层次准备边缘淡化。
  3. 绘制view的内容
  4. 绘制子view
  5. 如果需要,绘制淡化的边缘并存储图层。
  6. 绘制装饰部分,例如滚动条等。
public void draw(Canvas canvas) {final int privateFlags = mPrivateFlags;final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;// Step 1, 绘制背景int saveCount;if (!dirtyOpaque) {drawBackground(canvas);}// 如果不需要,跳过步骤2和5final int viewFlags = mViewFlags;boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;if (!verticalEdges && !horizontalEdges) {// Step 3, 绘制内容if (!dirtyOpaque) onDraw(canvas);// Step 4, 绘制子view
            dispatchDraw(canvas);// Step 6, 绘制装饰部分
            onDrawScrollBars(canvas);if (mOverlay != null && !mOverlay.isEmpty()) {mOverlay.getOverlayView().dispatchDraw(canvas);}// 完成return;}    }

我们选择常规的绘制过程,不介绍2,5步骤。

第一步,调用drawBackground绘制背景图案:

private void drawBackground(Canvas canvas) {final Drawable background = mBackground;
         // 获取到当前view的背景,是一个drawable对象 if (background == null) {return;}if (mBackgroundSizeChanged) {// 判断背景大小是否变化,是则设置背景边界background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);mBackgroundSizeChanged = false;mPrivateFlags3 |= PFLAG3_OUTLINE_INVALID;}// Attempt to use a display list if requested.if (canvas.isHardwareAccelerated() && mAttachInfo != null&& mAttachInfo.mHardwareRenderer != null) {mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);final RenderNode displayList = mBackgroundRenderNode;if (displayList != null && displayList.isValid()) {setBackgroundDisplayListProperties(displayList);((HardwareCanvas) canvas).drawRenderNode(displayList);return;}}
       // 调用drawable对象的绘制方法完成绘制final int scrollX = mScrollX;final int scrollY = mScrollY;if ((scrollX | scrollY) == 0) {background.draw(canvas);} else {canvas.translate(scrollX, scrollY);background.draw(canvas);canvas.translate(-scrollX, -scrollY);}}

第三步,调用onDraw方法绘制view的内容,由于不同的view内容不同,所以需要子类进行重写。

第四步,绘制子view,这里仍然需要当前layout的dispatchDraw方法来完成对各子view的绘制。

第六步,绘制滚动条。

通常情况下,我们自定义view,复写onDraw方法来绘制我们定义的view的内容即可。

总结

通过研究view类的源码,我们可以发现,在整个view的绘制流程中我们需要完成测定尺寸,布局定位,绘制这三个步骤。Android在设计过程中,将固定不变的流程设计为不可更改的模板方法,然而需要根据不同情况而定的内容则交给开发者来完成重写,在模板方法中调用即可。这样设计即保证了整个流程的完整,又给开发工作带来了灵活。同时,在类中又根据不同情况定义了不同的flag,来满足不同情况的绘制需求,以后有机会再具体研究这些flag的具体意义。

android view绘制过程相关推荐

  1. android view 绘制过程,深入理解Android中View绘制的三大流程

    前言 最近对Android中View的绘制机制有了一些新的认识,所以想记录下来并分享给大家.View的工作流程主要是指measure.layout.draw这三大流程,即测量.布局和绘制,其中meas ...

  2. 【Android View绘制之旅】Draw过程

    出效果:绘制 经过前面的准备工作 :[Android View绘制之旅]Measure过程,[Android View绘制之旅]Layout过程 我们的视图具备了宽高数据,位置数据,现在到了激动人心的 ...

  3. 【Android View绘制之旅】Layout过程

    1.为什么要进行Layout? 在[Android View绘制之旅]View之测量Measure过程后,View我们得到View的宽高,但光只有宽高值是不足以反映视图的,更需要知道View所在的位置 ...

  4. 【Android View绘制之旅】主脉络

    没搞清楚View绘制原理会怎么样? 只会玩玩初级的组件 看不懂哪些绚丽效果组件实现的原理,即使你有源码 PM会对你很失望,因为有点高级特性你就跪了 当然好的工作机会是没有你的份的 View绘制之旅该怎 ...

  5. Android View绘制之旅

    1.说在起点的话 很早前就想将View绘制原理这块给搞清楚搞透彻,但是奈何自己无知还是愚钝,总未能得真经,所以此次决意好好出发,做到有始有终. 我分析了一下自己的问题,自己实在太功利了,总希望看一两篇 ...

  6. android 绘图流程,Android View绘制流程

    前言 不知道大家有没有想过一个问题,当启动一个Activity的时候,相应的XML布局文件中的View是如何显示到屏幕上的?有些同学会说是通过onMeasure().onLayout().onDraw ...

  7. Android View 绘制流程

    前面讲到 Android View 加载流程,使用 LayoutInflater 将 xml 文件转变成 View ,但是还需要将 View 绘制出来,才能被用户看到,这一过程为绘制流程.由于 And ...

  8. Android UI 绘制过程浅析(二)onMeasure过程

    前言 View的绘制过程分为 measure.layout.draw三个步骤,接下来对这三个步骤逐一进行研究. measure方法的签名 public final void measure(int w ...

  9. Android View绘制原理解析

    概述 本篇文章主要讲述View是如何在Android源码中产生的,以便于我们能够更好的去自定义一些控件,大体上是按照View绘制的流程来走步骤,在追踪源码之前我们先了解几个基础知识.来看下面的这张图: ...

最新文章

  1. 我的操作系统复习——进程(下)
  2. POJ 1177 Picture [离散化+扫描线+线段树]
  3. windows 命令收集
  4. Ubuntu18.04换源更新国内源
  5. java excel中删除两列_Java 插入、隐藏/显示、删除Excel行或列
  6. Servlet API
  7. Silverlight+WCF 新手实例 象棋 棋子移动-线交叉点(六)
  8. Android的内容观察者
  9. Android 系统定时管理器AlarmManager的使用
  10. 一篇文章学会er图绘制
  11. LintCode 52: Next Permutation
  12. 中国象棋AI实现——alpha-beta剪枝
  13. matlab:蚁群算法原理的实现
  14. 写论文时引用作者名字
  15. 用Python多线程抓取并验证代理
  16. 阿里再发10亿助农,店宝宝:中小卖家喜迎流量红利
  17. android sdk离线安装方法,Android 4.0 SDK的离线方式安装
  18. 用uniapp实现微信小程序的电子签名效果
  19. 赠与大学毕业生_如何出售或赠与您的Kindle
  20. 帝国 loginjs.php,帝国cms JS调用登陆模板制作教程

热门文章

  1. django设置paypal支付如何获取signature
  2. navicat卡死问题
  3. 关闭sublime3自动更新(要输入license才会奏效)
  4. 深度学习:用于multinoulli输出分布的softmax单元
  5. mysql看表关联视图_MySQL数据库 : 自关联,视图,事物,索引
  6. git 教程2 (git常用命令解说)
  7. 文档加载完后执行相关事件
  8. maven工程错误汇总
  9. ​Linux下C如何调用PCI Lib函数
  10. [开源 .NET 跨平台 Crawler 数据采集 爬虫框架: DotnetSpider] [一] 初衷与架构设计