转载

Android View绘制流程总结

简介

在Android开发中, View 扮演了很重要的角色。在官方的API中,对View的描述是这样的:

A View occupies a rectangular area on the screen and is responsible for drawing and event handling

意思是 View 在屏幕上占用一块矩形区域,并且负责绘制和事件处理。 View 的绘制和事件处理是两个重要的主题。本文简单分析一下 View 的绘制流程。

View 的绘制流程是从 ViewRootImpl 的 performTraversals 方法开始,它经过 measure 、 layout 和 draw 三个过程才能最终将一个 View 绘制出来。

理解MeasureSpec

为了明白 View 的绘制原理,首先要知道 MeasureSpec 的概念。 MeasureSpec 相当于是 View 的测量说明,其中封装了测量模式(SpecMode)和测量规格(SpecSize)。看一下 MeasureSpec 的代码:

 private static final int MODE_SHIFT = 30;  private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  public static final int UNSPECIFIED = 0 << MODE_SHIFT;  public static final int EXACTLY     = 1 << MODE_SHIFT;  public static final int AT_MOST     = 2 << MODE_SHIFT;    public static int makeMeasureSpec(int size, int mode) {     if (sUseBrokenMakeMeasureSpec) {         return size + mode;     } else {         return (size & ~MODE_MASK) | (mode & MODE_MASK);     }  }  public static int getMode(int measureSpec) {     return (measureSpec & MODE_MASK);  }  public static int getSize(int measureSpec) {     return (measureSpec & ~MODE_MASK);  }  

MeasureSpec 将SpecMode和SpecSize封装成一个 int 值,通过 getMode 方法可以提取出SpecMode, getSize 方法可以提取出SpecSize。

测量模式(SpecMode)有三种类型:

  • EXACTLY父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。

  • AT_MOST父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。

  • UNSPECIFIED父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。

一个 View 的 MeasureSpec 由父布局 MeasureSpec 和自身的 LayoutParams 共同产生。父布局的 MeasureSpec 从何而来?从父布局的父布局而来。最顶层的布局是 DecorView ,常用的 setContent(view) 便是设置 DecorView 。 DecorView 的 MeasureSpec 是通过 ViewRootImpl 中的 getRootMeasureSpec 方法得到的。

下面主要分析普通 View 的 MeasureSpec 的产生,看一下 ViewGroup 的 measureChild 方法:

 protected void measureChild(View child, int parentWidthMeasureSpec,         int parentHeightMeasureSpec) {     final LayoutParams lp = child.getLayoutParams();      final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,             mPaddingLeft + mPaddingRight, lp.width);     final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,             mPaddingTop + mPaddingBottom, lp.height);      child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } 

从上面的代码可以看出调用 getChildMeasureSpec 方法得到子 View 的 MeasureSpec ,传进去的参数是父容器的 MeasureSpec 和子 View 的 LayoutParams ,由此可见 MeasurecSpec 是由父容器和 View 本身共同决定的。 getChildMeasureSpec 方法获取子 View 的 MeasureSpec 的逻辑如下图所示:

其中 UNSPECIFID 不需要考虑。

注意当子 View 的 LayoutParams 为 wrap_content 时,最终的SpecMode都是 AT_MOST ,SpecSize为父容器剩余空间大小。

得到子 View 的 MeasureSpec 后,调用子 View 的 measure 方法,传入相应的参数,开始下一层的 measure 过程。

Measure

View 的 measure 的过程由其 measure 方法来完成,这是一个 final 类型的方法,这意味着子类不能重写此方法,在 View 的方法中去调用 View 的 onMeasure 方法,它的实现如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {     setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),             getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } public static int getDefaultSize(int size, int measureSpec) {     int result = size;     int specMode = MeasureSpec.getMode(measureSpec);     int specSize = MeasureSpec.getSize(measureSpec);      switch (specMode) {     case MeasureSpec.UNSPECIFIED:         result = size;         break;     case MeasureSpec.AT_MOST:     case MeasureSpec.EXACTLY:         result = specSize;         break;     }     return result; }   

可以看出,最终是调用 getDefaultSize 方法得到实际的测量值。上一节提到,当子 View 的 LayoutParams 为 wrap_content 时,最终的SpecMode都是 AT_MOST ,SpecSize为父容器剩余空间大小。在 getDefaultSize 方法中,对于 AT_MOST 和 EXACTLY 均是直接使用父容器传进来的值,这可能不是我们想要的值,所以自定义 View 时要重写 onMeasure 方法处理 AT_MOST ,否则使用 wrap_content 相当于使用 match_parent 。

对于 ViewGroup ,测量完自己还要调用子 View 的 measure 方法,各个子元素再递归去执行这个过程。

Layout

Layout的作用是 ViewGroup 用来确定子元素的位置,当 ViewGroup 的位置被确定以后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,在 layout 方法中 onLayout 方法又会被调用。 onlayout 方法是抽象方法,所以自定义 ViewGroup 时需要实现这个方法确定子元素的布局。常用的 LinearLayout 以及 RelativeLayout 方法均重写了这个方法。

Draw

Draw过程就是将 View 绘制到屏幕上,有如下几步:

  1. 绘制背景

  2. 绘制自己

  3. 绘制children

  4. 绘制装饰

View 中有一个特殊的方法 setWillNotDraw ,源码如下:

public void setWillNotDraw(boolean willNotDraw) {     setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK); } 

这个方法的意义是,如果一个 View 不需要绘制任何内容,设置这个标记位为 true 后,系统会进行相应的优化。默认情况下, View 没有启用这个标记位,但是 ViewGroup 默认启用。

总结

View 的绘制要经过 measure 、 layout 以及 draw 三个步骤,总体来说还是比较复杂的,本文只是简要概括,更详细的分析见文末参考。

参考

原文  https://segmentfault.com/a/1190000004622988
正文到此结束
Loading...