Android自定义View篇之(一)View绘制流程

2020-03-17  本文已影响0人  风影_638f

前言

       自定义View、多线程、网络,被认为是Android开发者必须牢固掌握的最基础的三大基本功。Android View的绘制流程原理又是学好自定义View的理论基础,所以掌握好View的绘制原理是Android开发进阶中无法绕过的一道坎。而关乎到原理性的东西往往又让很多初学者感到难以下手,所以真正掌握的人并不多。本文采用非常浅显的语言,从顺着Android源码的思路,对View的整个绘制流程进行近乎“地毯式搜索”般的方式,对其中的关键流程和知识点进行查证和分析,以图让初级程序员都能轻松读懂。本文最大的特点,就是最大限度地向源码要答案,从源码中追流程的来龙去脉,在注释中查功能的点点滴滴,所有的结论都尽量在源码和注释中找根据。

       为了能对其中的重难点分析透彻,文中贴出了大量的源码依据以及源码中的注释,并对重要的注释进行了翻译和讲解,所以文章会比较长。讲解该知识点的文章普遍都非常长,所以希望读者能够秉承程序员吃苦耐劳的精神,攻克这个难关。本文中的源码是基于API26的,即Android8.0系统版本,主要内容大致如下:

一、View绘制的三个流程

       我们知道,在自定义View的时候一般需要重写父类的onMeasure()、onLayout()、onDraw()三个方法,来完成视图的展示过程。当然,这三个暴露给开发者重写的方法只不过是整个绘制流程的冰山一角,更多复杂的幕后工作,都让系统给代劳了。一个完整的绘制流程包括measure、layout、draw三个步骤,其中:

     measure:测量。系统会先根据xml布局文件和代码中对控件属性的设置,来获取或者计算出每个View和ViewGrop的尺寸,并将这些尺寸保存下来。

     layout:布局。根据测量出的结果以及对应的参数,来确定每一个控件应该显示的位置。

     draw:绘制。确定好位置后,就将这些控件绘制到屏幕上。

二、Android视图层次结构简介

       在介绍View绘制流程之前,咱们先简单介绍一下Android视图层次结构以及DecorView,因为View的绘制流程的入口和DecorView有着密切的联系。

 咱们平时看到的视图,其实存在如上的嵌套关系。上图是针对比较老的Android系统版本中制作的,新的版本中会略有出入,还有一个状态栏,但整体上没变。我们平时在Activity中setContentView(...)中对应的layout内容,对应的是上图中ViewGrop的树状结构,实际上添加到系统中时,会再裹上一层FrameLayout,就是上图中最里面的浅蓝色部分了。

       这里咱们再通过一个实例来继续查看。AndroidStudio工具中提供了一个布局视察器工具,通过Tools > Android > Layout Inspector可以查看具体某个Activity的布局情况。下图中,左边树状结构对应了右边的可视图,可见DecorView是整个界面的根视图,对应右边的红色框,是整个屏幕的大小。黄色边框为状态栏部分;那个绿色边框中有两个部分,一个是白框中的ActionBar,对应了上图中紫色部分的TitleActionBar部分,即标题栏,平时咱们可以在Activity中将其隐藏掉;另外一个蓝色边框部分,对应上图中最里面的蓝色部分,即ContentView部分。下图中左边有两个蓝色框,上面那个中有个“contain_layout”,这个就是Activity中setContentView中设置的layout.xml布局文件中的最外层父布局,咱们能通过layout布局文件直接完全操控的也就是这一块,当其被add到视图系统中时,会被系统裹上ContentFrameLayout(显然是FrameLayout的子类),这也就是为什么添加layout.xml视图的方法叫setContentView(...)而不叫setView(...)的原因。

三、故事开始的地方

        如果对Activity的启动流程有一定了解的话,应该知道这个启动过程会在ActivityThread.java类中完成,在启动Activity的过程中,会调用到handleResumeActivity(...)方法,关于视图的绘制过程最初就是从这个方法开始的。

  1、View绘制起源UML时序图

       整个调用链如下图所示,直到ViewRootImpl类中的performTraversals()中,才正式开始绘制流程了,所以一般都是以该方法作为正式绘制的源头。

 2、handleResumeActivity()方法

       在这咱们先大致看看ActivityThread类中的handleResumeActivity方法,咱们这里只贴出关键代码:

上述代码第8行中,ViewManager是一个接口,addView是其中定义个一个空方法,WindowManager是其子类,WindowManagerImpl是WindowManager的实现类(顺便啰嗦一句,这种方式叫做面向接口编程,在父类中定义,在子类中实现,在Java中很常见)。第4行代码中的r.window的值可以根据Activity.java的如下代码得知,其值为PhoneWindow实例。

3、两个重要参数分析

       之所以要在这里特意分析handleResumeActivity()方法,除了因为它是整个绘制流程的最初源头外,还有就是addView的两个参数比较重要,它们经过一层一层传递后进入到ViewRootImpl中,在后面分析绘制中要用到。这里再看看这两个参数的相关信息:

    (1)参数decor

可见decor参数表示的是DecorView实例。注释中也有说明:这是window的顶级视图,包含了window的decor。

    (2)参数l

该参数表示l的是PhoneWindow的LayoutParams属性,其width和height值均为LayoutParams.MATCH_PARENT。在源码中,WindowPhone和DecorView通过组合方式联系在一起的,而DecorView是整个View体系的根View。在前面handleResumeActivity(...)方法代码片段中,当Actiivity启动后,就通过第14行的addView方法,来间接调用ViewRootImpl类中的performTraversals(),从而实现视图的绘制。

四、主角登场 

  无疑,performTraversals()方法是整个过程的主角,它把控着整个绘制的流程。该方法的源码有大约800行,这里咱们仅贴出关键的流程代码,如下所示:

上述代码中就是一个完成的绘制流程,对应上了第一节中提到的三个步骤:

      1)performMeasure():从根节点向下遍历View树,完成所有ViewGroup和View的测量工作,计算出所有ViewGroup和View显示出来需要的高度和宽度;

      2)performLayout():从根节点向下遍历View树,完成所有ViewGroup和View的布局计算工作,根据测量出来的宽高及自身属性,计算出所有ViewGroup和View显示在屏幕上的区域;

      3)performDraw():从根节点向下遍历View树,完成所有ViewGroup和View的绘制工作,根据布局过程计算出的显示区域,将所有View的当前需显示的内容画到屏幕上。

咱们后续就是通过对这三个方法来展开研究整个绘制过程。

五、measure过程分析

       这三个绘制流程中,measure是最复杂的,这里会花较长的篇幅来分析它。本节会先介绍整个流程中很重要的两个类MeasureSpec和ViewGroup.LayoutParams类,然后介绍ViewRootImpl、View及ViewGroup中测量流程涉及到的重要方法,最后简单梳理DecorView测量的整个流程并链接一个测量实例分析整个测量过程。

  1、MeasureSpec简介

       这里咱们直接上源码吧,先直接通过源码和注释认识一下它,如果看不懂也没关系,在后面使用的时候再回头来看看。

 从这段代码中,咱们可以得到如下的信息:

    1)MeasureSpec概括了从父布局传递给子view布局要求。每一个MeasureSpec代表了宽度或者高度要求,它由size(尺寸)和mode(模式)组成。

    2)有三种可能的mode:UNSPECIFIED、EXACTLY、AT_MOST

    3)UNSPECIFIED:未指定尺寸模式。父布局没有对子view强加任何限制。它可以是任意想要的尺寸。(笔者注:这个在工作中极少碰到,据说一般在系统中才会用到,后续会讲得很少)

    4)EXACTLY:精确值模式。父布局决定了子view的准确尺寸。子view无论想设置多大的值,都将限定在那个边界内。(笔者注:也就是layout_width属性和layout_height属性为具体的数值,如50dp,或者设置为match_parent,设置为match_parent时也就明确为和父布局有同样的尺寸,所以这里不要以为笔者搞错了。当明确为精确的尺寸后,其也就被给定了一个精确的边界)

    5)AT_MOST:最大值模式。子view可以一直大到指定的值。(笔者注:也就是其宽高属性设置为wrap_content,那么它的最大值也不会超过父布局给定的值,所以称为最大值模式)

    6)MeasureSpec被实现为int型来减少对象分配。该类用于将size和mode元组装包和拆包到int中。(笔者注:也就是将size和mode组合或者拆分为int型数据)

    7)分析代码可知,一个MeasureSpec的模式如下所示,int长度为32位置,高2位表示mode,后30位用于表示size

     8)UNSPECIFIED、EXACTLY、AT_MOST这三个mode的示意图如下所示:

 9)makeMeasureSpec(int mode,int size)用于将mode和size打包成一个int型的MeasureSpec。

    10)getSize(int measureSpec)方法用于从指定的measureSpec值中获取其size。

    11)getMode(int measureSpec)方法用户从指定的measureSpec值中获取其mode。

2、ViewGroup.LayoutParams简介

   该类的源码及注释分析如下所示。

这对其中重要的信息做一些翻译和整理:

    1)LayoutParams被view用于告诉它们的父布局它们想要怎样被布局。(笔者注:字面意思就是布局参数)

    2)该LayoutParams基类仅仅描述了view希望宽高有多大。对于每一个宽或者高,可以指定为以下三种值中的一个:MATCH_PARENT,WRAP_CONTENT,an exact number。(笔者注:FILL_PARENT从API8开始已经被MATCH_PARENT取代了,所以下文就只提MATCH_PARENT)

    3)MATCH_PARENT:意味着该view希望和父布局尺寸一样大,如果父布局有padding,则要减去该padding值。

    4)WRAP_CONTENT:意味着该view希望其大小为仅仅足够包裹住其内容即可,如果自己有padding,则要加上该padding值。

    5)对ViewGroup不同的子类,也有相应的LayoutParams子类。 

    6)其width和height属性对应着layout_width和layout_height属性。

转载请申明转自【https://www.cnblogs.com/andy-songwei/p/10955062.html

上一篇下一篇

猜你喜欢

热点阅读