Android是怎样绘制视图的

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android是怎样绘制视图的相关的知识,希望对你有一定的参考价值。

当一个活动接收到焦点时,它将被要求绘制它的布局。android框架将处理这个绘画的过程,但是活动必须提供它的布局层次的根节点。

绘画从布局的根节点开始。它被要求来测量和绘制布局树。绘画通过遍历布局树并渲染每个和失效区域相交的视图来处理。相应的,每个视图组负责请求绘制它的子视图(通过draw() 方法)而每个视图负责画它自己。因为这个树是顺序遍历的,这意味着先画父节点(也就是在屏幕后面),然后按照树中出现的顺序画其同层次节点。

框架将不会画不在失效区域的视图,而且还将会帮你画视图背景。

你可以强制一个视图被重画,通过调用invalidate()。

绘画布局共有两步:一个度量过程和一个布局过程。度量过程在measure(int, int)里实现且是一个自顶向下的视图树遍历。每个视图在递归时往下推送尺寸规格。在度量过程的最后,每个视图都已经保存了自己的度量。第二个过程发生在layout(int, int, int, int) 中并且也是自顶向下。在这个过程中,每个父节点负责定位它的所有子节点,通过使用在度量过程中计算得到的尺寸。

当一个视图的measure()方法返回时,它的getMeasuredWidth()和getMeasuredHeight() 值必须被设置,以及所有这个视图子节点的值。一个视图的度量的宽度和高度值必须符合父视图引入的限制。这确保在度量过程之后,所有父节点接受所有它们的子节点的度量值。一个父视图可能会在其子视图上多次调用measure()方法。比如,父视图可能会通过未指定的尺寸调用measure来发现它们的大小,然后使用实际数值再次调用measure(),如果所有子视图未做限制的尺寸总合过大或过小(也即是,如果子视图之间不能对各自占据的空间达成共识的话,父视图将会干预并设置第二个过程的规则)。

要开始一个布局,可调用requestLayout()。这个方法通常在视图认为它自己不再适合它当前的边界的情况下被调用。

度量过程使用两个类来交流尺寸。View.MeasureSpec类被视图用来告诉它们的父视图它们想如何被度量和定位。基础的LayoutParams类仅仅描述了视图想有多大(高和宽)。对于每个维度,它可以指定下面之一:

    ·         一个准确的数值

    ·         FILL_PARENT, 这意味着视图想和父视图一样大(减掉填充padding)。

    ·         WRAP_CONTENT, 这意味着视图只想有刚好包装其内容那么大(加上填充)

    对于不同的ViewGroup子类,有相应的LayoutParams子类。比如,相对布局RelativeLayout有它自己的LayoutParams子类,这包含了能够让子视图横向和竖向居中显示的能力。

    度量规格(MeasureSpecs)被用来沿着树从父到子的下传度量需求。一个MeasureSpecs可以是下面三种模式之一:

    ·         UNSPECIFIED: 这被父视图用来决定其子视图期望的尺寸。比如,一个线性布局可能在它的子视图上调用measure() on its child,通过设置其高度为UNSPECIFIED 以及一个宽度为EXACTLY 240,来找出这个子视图在给定240像素宽度的情况下需要显示多高。

    ·         EXACTLY: 这被父视图用来给子视图强加一个准确的尺寸。子视图必须使用这个大小,并确保其所有的后代将适合这个尺寸。

    ·         AT_MOST: 这被父视图用来给子视图强加一个最大尺寸。子视图必须确保它自己以及所有的后代都适合这个尺寸。

参考技术A Canvas 绘制 参考技术B

View的Layout过程

上文提到View的绘制流程是从ViewRoot的performTraversals方法开始,那么在View完成测量以后,在performTraversals方法中对performLayout进行调用。在performLayout中可以找到下面这行代码。

host.layout(0, 0, host.getMeasuredWidth(),host.getMeasuredHeight())

上面这行代码中的host指的就是DecorView,对于这个DecorView我们都知道它是一个继承自FrameLayout的ViewGroup。这个layout方法也就是ViewGroup中的layout方法。下面就来看一下ViewGroup中的这个layout方法。

@Overridepublic final void layout(int l, int t, int r, int b)     if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout()))         if (mTransition != null) 
            mTransition.layoutChange(this);
                super.layout(l, t, r, b);
     else         // record the fact that we noop'd it; request layout when transition finishes
        mLayoutCalledWhileSuppressed = true;
    

详细

Android - View 绘制流程

参考技术A

我们知道,在 Android 中,View 绘制主要包含 3 大流程:

Android 中,主要有两种视图: View 和 ViewGroup ,其中:

虽然 ViewGroup 继承于 View ,但是在 View 绘制三大流程中,某些流程需要区分 View 和 ViewGroup ,它们之间的操作并不完全相同,比如:

对 View 进行测量,主要包含两个步骤:

对于第一个步骤,即求取 View 的 MeasureSpec ,首先我们来看下 MeasureSpec 的源码定义:

MeasureSpec 是 View 的一个公有静态内部类,它是一个 32 位的 int 值,高 2 位表示 SpecMode(测量模式),低 30 位表示 SpecSize(测量尺寸/测量大小)。
MeasureSpec 将两个数据打包到一个 int 值上,可以减少对象内存分配,并且其提供了相应的工具方法可以很方便地让我们从一个 int 值中抽取出 View 的 SpecMode 和 SpecSize。

一个 MeasureSpec 表达的是:该 View 在该种测量模式(SpecMode)下对应的测量尺寸(SpecSize)。其中,SpecMode 有三种类型:

对 View 进行测量,最关键的一步就是计算得到 View 的 MeasureSpec ,子View 在创建时,可以指定不同的 LayoutParams (布局参数), LayoutParams 的源码主要内容如下所示:

其中:

LayoutParams 会受到父容器的 MeasureSpec 的影响,测量过程会依据两者之间的相互约束最终生成子View 的 MeasureSpec ,完成 View 的测量规格。

简而言之,View 的 MeasureSpec 受自身的 LayoutParams 和父容器的 MeasureSpec 共同决定( DecorView 的 MeasureSpec 是由自身的 LayoutParams 和屏幕尺寸共同决定,参考后文)。也因此,如果要求取子View 的 MeasureSpec ,那么首先就需要知道父容器的 MeasureSpec ,层层逆推而上,即最终就是需要知道顶层View(即 DecorView )的 MeasureSpec ,这样才能一层层传递下来,这整个过程需要结合 Activity 的启动过程进行分析。

我们知道,在 Android 中, Activity 是作为视图组件存在,主要就是在手机上显示视图界面,可以供用户操作, Activity 就是 Andorid 中与用户直接交互最多的系统组件。

Activity 的基本视图层次结构如下所示:

Activity 中,实际承载视图的组件是 Window (更具体来说为 PhoneWindow ),顶层View 是 DecorView ,它是一个 FrameLayout , DecorView 内部是一个 LinearLayout ,该 LinearLayout 由两部分组成(不同 Android 版本或主题稍有差异): TitleView 和 ContentView ,其中, TitleView 就是标题栏,也就是我们常说的 TitleBar 或 ActionBar , ContentView 就是内容栏,它也是一个 FrameLayout ,主要用于承载我们的自定义根布局,即当我们调用 setContentView(...) 时,其实就是把我们自定义的布局设置到该 ContentView 中。

当 Activity 启动完成后,最终就会渲染出上述层次结构的视图。

因此,如果我们要求取得到子View 的 MeasureSpec ,那么第一步就是求取得到顶层View(即 DecorView )的 MeasureSpec 。大致过程如下所示:

经过上述步骤求取得到 View 的 MeasureSpec 后,接下来就可以真正对 View 进行测量,求取 View 的最终测量宽/高:

Android 内部对视图进行测量的过程是由 View#measure(int, int) 方法负责的,但是对于 View 和 ViewGroup ,其具体测量过程有所差异。

因此,对于测量过程,我们分别对 View 和 ViewGroup 进行分析:

综上,无论是对 View 的测量还是 ViewGroup 的测量,都是由 View#measure(int widthMeasureSpec, int heightMeasureSpec) 方法负责,然后真正执行 View 测量的是 View 的 onMeasure(int widthMeasureSpec, int heightMeasureSpec) 方法。

具体来说, View 直接在 onMeasure(...) 中测量并设置自己的最终测量宽/高。在默认测量情况下, View 的测量宽/高由其父容器的 MeasureSpec 和自身的 LayoutParams 共同决定,当 View 自身的测量模式为 LayoutParams.UNSPECIFIED 时,其测量宽/高为 android:minWidth / android:minHeight 和其背景宽/高之间的较大值,其余情况皆为自身 MeasureSpec 指定的测量尺寸。

而对于 ViewGroup 来说,由于布局特性的丰富性,只能自己手动覆写 onMeasure(...) 方法,实现自定义测量过程,但是总的思想都是先测量 子View 大小,最终才能确定自己的测量大小。

当确定了 View 的测量大小后,接下来就可以来确定 View 的布局位置了,也即将 View 放置到屏幕具体哪个位置。

View 的布局过程由 View#layout(...) 负责,其源码如下:

View#layout(...) 主要就做了两件事:

ViewGroup 的布局流程由 ViewGroup#layout(...) 负责,其源码如下:

可以看到, ViewGroup#layout(...) 最终也是通过 View#layout(...) 完成自身的布局过程,一个注意的点是, ViewGroup#layout(...) 是一个 final 方法,因此子类无法覆写该方法,主要是 ViewGroup#layout(...) 方法内部对子视图动画效果进行了相关设置。

由于 ViewGroup#layout(...) 内部最终调用的还是 View#layout(...) ,因此, ViewGroup#onLayout(...) 就会得到回调,用于处理 子View 的布局放置,其源码如下:

由于不同的 ViewGroup ,其布局特性不同,因此 ViewGroup#onLayout(...) 是一个抽象方法,交由 ViewGroup 子类依据自己的布局特性,摆放其 子View 的位置。

当 View 的测量大小,布局位置都确定后,就可以最终将该 View 绘制到屏幕上了。

View 的绘制过程由 View#draw(...) 方法负责,其源码如下:

其实注释已经写的很清楚了, View#draw(...) 主要做了以下 6 件事:

我们知道,在 Activity 启动过程中,会调用到 ActivityThread.handleResumeActivity(...) ,该方法就是 View 视图绘制的起始之处:

可以看到, ActivityThread.handleResumeActivity(...) 主要就是获取到当前 Activity 绑定的 ViewManager ,最后调用 ViewManager.addView(...) 方法将 DecorView 设置到 PhoneWindow 上,也即设置到当前 Activity 上。 ViewManager 是一个接口, WindowManager 继承 ViewManager ,而 WindowManagerImpl 实现了接口 WindowManager ,此处的 ViewManager.addView(...) 实际上调用的是 WindowManagerImpl.addView(...) ,源码如下所示:

WindowManagerImpl.addView(...) 内部转发到 WindowManagerGlobal.addView(...) :

在 WindowManagerGlobal.addView(...) 内部,会创建一个 ViewRootImpl 实例,然后调用 ViewRootImpl.setView(...) 将 ViewRootImpl 与 DecorView 关联到一起:

ViewRootImpl.setView(...) 内部首先关联了传递过来的 DecorView (通过属性 mView 指向 DecorView 即可建立关联),然后最终调用 requestLayout() ,而 requestLayout() 内部又会调用方法 scheduleTraversals() :

ViewRootImpl.scheduleTraversals() 内部主要做了两件事:

Choreographer.postCallback(...) 会申请一次 VSYNC 中断信号,当 VSYNC 信号到达时,便会回调 Choreographer.doFrame(...) 方法,内部会触发已经添加的回调任务, Choreographer 的回调任务有以下四种类型:

因此, ViewRootImpl.scheduleTraversals(...) 内部通过 mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null) 发送的异步视图渲染消息就会得到回调,即回调 mTra

以上是关于Android是怎样绘制视图的的主要内容,如果未能解决你的问题,请参考以下文章

View的绘制流程

如何在Android中的特定位置绘制按钮?

Android:在绘制之前获取视图的高度

ANDROID自己定义视图——onLayout源代码 流程 思路具体解释

在Android中绘制之前测量所有视图层次结构

Android - 如何在视图上绘制