Android自定义View之(一)View绘制流程详解——向源码要答案

本文深入探讨了Android View的绘制流程,从measure、layout到draw三个步骤,揭示了View如何从尺寸计算到位置确定再到屏幕绘制的全过程。通过源码分析,讲解了MeasureSpec和ViewGroup.LayoutParams的重要性,以及DecorView在整个流程中的关键角色。此外,文章还介绍了测量过程的细节,包括ViewGroup如何测量子View,并提供了DecorView的测量流程图。最后,概述了layout和draw阶段,强调了布局和绘制的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

       View作为整个app的颜值担当,在Android体系中占有重要的地位。深入理解Android View的绘制流程,对正确使用View来构建赏心悦目的外观,以及用自定义View来设计理想中的酷炫效果等方面,有着极其重要的帮助作用,所以将View的绘制流程作为自定义View系列文章的第一篇。当然,View的绘制流程原理,在现实的工作中是成为高级工程师路上必须克服的障碍;在面试中,也是面试高级一点的职位,面试官几乎一定会问的问题。总体来说,Android View的绘制流程原理,是一个Android程序员的基本内功之一。

       本文最大的特点,就是最大限度地向源码要答案。从源码中追流程的来龙去脉,在注释中查功能的点点滴滴,所有的结论都尽量在源码和注释中找根据,关键的流程尽量说细致,非重点的地方尽量简略。所以读者会看到,本文中贴了大量的源码,且基本都是和分析绘制流程相关的代码,同时附上了大量的源码注释,以及对这些注释的翻译及整理。在每一个过程的最后,还根据源码的流程走向,绘制了一幅简易的关键流程图,以此来帮助读者理解及加深印象。事实上,本文就是记录的笔者从几乎一无所知到追踪源码来搞弄明白整个绘制流程的完整经历的,中途遇到的一些困惑及疑难点都会花不少篇幅来解释。当然仅仅学习本文还是不够的,因为这方面的内容很多,仅仅一篇文章是不可能面面俱到的。正如其他的知名博客文一样,都有自己的侧重点,有的侧重根据示例来分析流程,有的会附着demo来验证关键结论,本文中也会适当给出一些比较好的博文的链接,读者可以去这些地方弥补本文的不足。最后,再啰嗦一句,本文的侧重点是从源码中“顺藤摸瓜”,带领读者“登堂入室”,希望能给读者带来一点帮助。另外,这里说明一点,本文中的源码是基于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()中,才正式开始绘制流程了,所以一般都是以该方法作为正式绘制的源头。

图3.1 View绘制起源UML时序图

 

   2、handleResumeActivity()方法

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

复制代码

 1 //==============ActivityThread.java=================
 2 final void handleResumeActivity(...) {
 3     ......
 4     //跟踪代码后发现其初始赋值为mWindow = new PhoneWindow(this, window, activityConfigCallback);
 5     r.window = r.activity.getWindow(); 
 6        //从PhoneWindow实例中获取DecorView  
 7     View decor = r.window.getDecorView();
 8     ......
 9     //跟踪代码后发现,vm值为上述PhoneWindow实例中获取的WindowManager。
10     ViewManager wm = a.getWindowManager();
11     ......
12     //当前window的属性,从代码跟踪来看是PhoneWindow窗口的属性
13     WindowManager.LayoutParams l = r.window.getAttributes();
14     ......
15     wm.addView(decor, l);
16     ......
17 }

复制代码

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

复制代码

 1 //===============Activity.java=============
 2 private Window mWindow;
 3 public Window getWindow() {
 4    return mWindow;
 5 }
 6 
 7 final void attach(...){
 8    ......
 9    mWindow = new PhoneWindow(this, window, activityConfigCallback);
10    ......
11 }

复制代码

 

3、两个重要参数分析

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

    (1)参数decor

复制代码

 1 //==========PhoneWindow.java===========
 2 // This is the top-level view of the window, containing the window decor.
 3 private DecorView mDecor;
 4 ......
 5 public PhoneWindow(...){
 6    ......
 7    mDecor = (DecorView) preservedWindow.getDecorView();
 8    ......
 9 }
10 
11 @Override
12 public final View getDecorView() {
13    ......
14    return mDecor;
15 }

复制代码

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

    (2)参数l

复制代码

//===================Window.java===================
//The current window attributes.
    private final WindowManager.LayoutParams mWindowAttributes =
        new WindowManager.LayoutParams();
......
public final WindowManager.LayoutParams getAttributes() {
        return mWindowAttributes;
    }
......


//==========WindowManager.java的内部类LayoutParams extends ViewGroup.LayoutParams=============
public LayoutParams() {
            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
            ......
        }


//==============ViewGroup.java内部类LayoutParams====================
public LayoutParams(int width, int height) {
            this.width = width;
            this.height = height;
        }

复制代码

该参数表示l的是PhoneWindow的LayoutParams属性,其width和height值均为LayoutParams.MATCH_PARENT。

 

        在源码中,WindowPhone和DecorView通过组合方式联系在一起的,而DecorView是整个View体系的根View。在前面handleResumeActivity(...)方法代码片段中,当Actiivity启动后,就通过第14行的addView方法,来间接调用ViewRootImpl类中的performTraversals(),从而实现视图的绘制。

 

四、主角登场 

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

复制代码

 1 // =====================ViewRootImpl.java=================
 2 private void performTraversals() {
 3    ......
 4    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
 5    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);      
 6    ......
 7    // Ask host how big it wants to be
 8    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
 9    ......
10    performLayout(lp, mWidth, mHeight);
11    ......
12    performDraw();
13 }

复制代码

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

      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 /**
 2      * A MeasureSpec encapsulates the layout requirements passed from parent to child.
 3      * Each MeasureSpec represents a requirement for either the width or the height.
 4      * A MeasureSpec is comprised of a size and a mode. There are three possible
 5      * modes:
 6      * <dl>
 7      * <dt>UNSPECIFIED</dt>
 8      * <dd>
 9      * The parent has not imposed any constraint on the child. It can be whatever size
10      * it wants.
11      * </dd>
12      *
13      * <dt>EXACTLY</dt>
14      * <dd>
15      * The parent has determined an exact size for the child. The child is going to be
16      * given those bounds regardless of how big it wants to be.
17      * </dd>
18      *
19      * <dt>AT_MOST</dt>
20      * <dd>
21      * The child can be as large as it wants up to the specified size.
22      * </dd>
23      * </dl>
24      *
25      * MeasureSpecs are implemented as ints to reduce object allocation. This class
26      * is provided to pack and unpack the &lt;size, mode&gt; tuple into the int.
27      */
28     public static class MeasureSpec {
29         private static final int MODE_SHIFT = 30;
30         private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
31         ......
32         /**
33          * Measure specification mode: The parent has not imposed any constraint
34          * on the child. It can be whatever size it wants.
35          */
36         public static final int UNSPECIFIED = 0 << MODE_SHIFT;
37 
38         /**
39          * Measure specification mode: The parent has determined an exact size
40          * for the child. The child is going to be given those bounds regardless
41          * of how big it wants to be.
42          */
43         public static final int EXACTLY     = 1 << MODE_SHIFT;
44 
45         /**
46          * Measure specification mode: The child can be as large as it wants up
47          * to the specified size.
48          */
49         public static final int AT_MOST     = 2 << MODE_SHIFT;
50         ......
51        /**
52          * Creates a measure specification based on the supplied size and mode.
53          *...... 
54          *@return the measure specification based on size and mode        
55          */
56         public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
57                                           @MeasureSpecMode int mode) {
58             if (sUseBrokenMakeMeasureSpec) {
59                 return size + mode;
60             } else {
61                 return (size & ~MODE_MASK) | (mode & MODE_MASK);
62             }
63             ......
64             
65         }
66         ......
67         /**
68          * Extracts the mode from the supplied measure specification.
69          *......
70          */
71         @MeasureSpecMode
72         public static int getMode(int measureSpec) {
73             //noinspection ResourceType
74             return (measureSpec & MODE_MASK);
75         }
76 
77         /**
78          * Extracts the size from the supplied measure specification.
79          *......
80          * @return the size in pixels defined in the supplied measure specification
81          */
82         public static int getSize(int measureSpec) {
83             return (measureSpec & ~MODE_MASK);
84         }
85         ......
86 }

复制代码

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

    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 //============================ViewGroup.java===============================
 2 /**
 3      * LayoutParams are used by views to tell their parents how they want
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值