View的绘制流程

1.整体流程

View的首次绘制流程从ViewRootImpl的setView()方法开始

//ViewRootImpl 

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                requestLayout();
    }


    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();//检查是否在主线程
            mLayoutRequested = true;//mLayoutRequested 是否measure和layout布局。
            scheduleTraversals();
        }
    }


    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();

            //post一个runnable处理-->mTraversalRunnable
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            ``````
        }
    }


    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();



    void doTraversal() {
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            performTraversals();//View的绘制流程正式开始。
    }

ViewRootImpl在其创建过程中通过requestLayout()向主线程发送了一条触发遍历操作的消息,遍历操作是指performTraversals()方法。它是一个包罗万象的方法。

ViewRootImpl中接收的各种变化,如来自WMS的窗口属性变化、来自控件树的尺寸变化及重绘请求等都引发performTraversals()的调用,并在其中完成处理。

View类及其子类中的onMeasure()、onLayout()、onDraw()等回调也都是在performTraversals()的执行过程中直接或间接的引发。也正是如此,一次次的performTraversals()调用驱动着控件树有条不紊的工作,一旦此方法无法正常执行,整个控件树都将处于僵死状态。因此performTraversals()函数可以说是ViewRootImpl的心跳。 

//ViewRootImpl

    private void performTraversals() {

            //调用performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            measureHierarchy(host, lp, res,desiredWindowWidth, desiredWindowHeight);

            performLayout(lp, mWidth, mHeight);

            performDraw();
    }

image

首先我们看一下performTraversals()首次绘制的大致流程,performTraversals()会依次调用performMeasure、performLayout、performDraw三个方法,这三个方法便是View绘制流程的精髓所在。

performMeasure : 会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传到子元素中了,这样就完成了一次measure过程。measure完成以后,可以通过getMeasuredWidth和getMeasureHeight方法来获取到View测量后的宽高。

performLayout : 和performMeasure同理。Layout过程决定了View的四个顶点的坐标和实际View的宽高,完成以后,可以通过getTop/Bottom/Left/Right拿到View的四个顶点位置,并可以通过getWidth和getHeight方法来拿到View的最终宽高。

performDraw : 和performMeasure同理,唯一不同的是,performDraw的传递过程是在draw方法中通过dispatchDraw来实现的。Draw过程则决定了View的显示,只有draw方法完成以后View的内容才能呈现在屏幕上。

因此可以得出View的绘制分为measure,layout, draw 三个阶段:

  1. measure测量 :根据父 view 传递的 MeasureSpec 进行计算大小。
  2. layout布局:根据 measure 子 View 所得到的布局大小和布局参数,将子View放在合适的位置上。
  3. draw绘制 :把 View 对象绘制到屏幕上。

2.measure测量

View的测量的目的便是 测量控件的宽高值,围绕该目的,View的设计者通过代码编织了一整套复杂的逻辑:

1、对于子View而言,其本身宽高直接受限于父View的 布局要求,举例来说,父View被限制宽度为40px,子View的最大宽度同样也需受限于这个数值。因此,在测量子View之时,子View必须已知父View的布局要求,这个 布局要求,  Android中通过使用 MeasureSpec 类来进行描述。

2、对于完整的测量流程而言,父控件必然依赖子控件宽高的测量;若子控件本身未测量完毕,父控件自身的测量亦无从谈起。

Android中View的测量流程中使用了非常经典的 递归思想:对于一个完整的界面而言,每个页面都映射了一个View树,其最顶端的父控件测量开始时,会通过 遍历 将其 布局要求 传递给子控件,以开始子控件的测量,子控件在测量过程中也会通过 遍历 将其 布局要求 传递给它自己的子控件,如此往复一直到最底层的控件。

而当最底层位置的子控件自身测量完毕后,其父控件会将所有子控件的宽高数据进行聚合,然后通过对应的 测量策略 计算出父控件本身的宽高,测量完毕后,父控件的父控件也会根据其所有子控件的测量结果对自身进行测量,最终完成最顶层父控件的测量,至此界面整个View树测量完毕。

在整个 测量流程 中, 布局要求 都是一个非常重要的核心名词,Android中通过使用 MeasureSpec 类来对其进行描述。

子控件的测量过程本身还应该依赖于父控件的一些布局约束,子控件的测量结果是由父控件和其本身共同决定的 ,而父控件对子控件的布局约束,便是前文提到的 布局要求,即MeasureSpec类。

MeasureSpec

public final class MeasureSpec {

  int size;     // 测量大小
  Mode mode;    // 测量模式

  enum Mode { UNSPECIFIED, EXACTLY, AT_MOST }

  MeasureSpec(Mode mode, int size){
    this.mode = Mode;
    this.size = size;
  }

  public int getSize() { return size; }

  public Mode getMode() { return mode; }
}

MeasureSpec分成了2个属性。

测量大小 意味着控件需要对应大小的宽高;

测量模式 则表示控件对应的宽高模式: 

  • UNSPECIFIED:父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小;日常开发中自定义View不考虑这种模式,可暂时先忽略;
  •  EXACTLY:父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;这里我们理解为控件的宽或者高被设置为 match_parent 或者指定大小,比如20dp;
  • AT_MOST:子元素至多达到指定大小的值;这里我们理解为控件的宽或者高被设置为wrap_content。

巧妙的是,Android并非通过上述定义MeasureSpec对象的方式对 布局要求 进行描述,而是使用了更简单的二进制的方式,用一个32位的int值进行替代:

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30; //移位位数为30
    //int类型占32位,向右移位30位,该属性表示掩码值,用来与size和mode进行"&"运算,获取对应值。
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    //00左移30位,其值为00 + (30位0)
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    //01左移30位,其值为01 + (30位0)
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    //10左移30位,其值为10 + (30位0)
    public static final int AT_MOST     = 2 << MODE_SHIFT;

    // 根据size和mode,创建一个测量要求
    public static int makeMeasureSpec(int size, int mode) {
        return size + mode;
    }

    // 根据规格提取出mode,
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }

    // 根据规格提取出size
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

这个int值中,前2位代表了测量模式,后30位则表示了测量的大小,对于模式和大小值的获取,只需要通过位运算即可。
以宽度举例来说,若我们设置宽度=5px(二进制对应了101),那么mode对应EXACTLY,在创建测量要求的时候,只需要通过二进制的相加,便可得到存储了相关信息的int值: 

图片

而当需要获得Mode的时候只需要用measureSpec与MODE_MASK相与即可,如下图:

图片

同理,想获得size的话只需要只需要measureSpec与~MODE_MASK相与即可,如下图:

图片

现在读者对MeasureSpec类有了初步地认识,在Android绘制过程中,View宽或者高的 布局要求 实际上是通过32位的int值进行的描述, 而MeasureSpec类本身只是一个静态方法的容器而已。

对于普通View(DecorView略有不同),其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定,MeasureSpec一旦确定后,o

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值