手把手才教会女友的的——AndroidView绘制流程

前言

一个View,从无到有会走三个流程,也就是老生常谈的measure、layout、draw三流程:

我们都知道Android视图是由一层一层构成的层级结构,直白点说,就是父View包含子View而子View又可以包含子View。所以绘制流程是由最外层的View开始,一步一步向内传递执行。而整个过程又是递归等待的,最外层的View需要等内层所有的View执行完绘制流程才结束,所以便有了”减少布局层级,可以有效提升App性能”这一经典总结。

img

三个流程介绍:

android中控件相当于是画在一个无限大的画布 上的,那就产生了几个问题

1.画布无限大,但是画的内容肯定是有限的,即我们只需要画布的一小部分,那这部分有多大呢?

  • measure就是计算这个画布所需部分有多大的

2.决定好我们需要的画布部分,我们可能会在上面画很多内容每个内容都画在什么位置呢?

  • layout就是决定在选定范围内画在什么位置的

3.最后,决定好画在具体位置时,我们到底画什么内容呢?

  • draw自然就是决定画什么具体内容的了

而三个步骤对应的处理我们都可以在对应的on…方法中实现
即onMeasure onL ayout onDraw

1.measure过程

于是View的绘制就首先从measure开始measure的意思是测量,这个方法要做的事是确定所有要绘制的View(View族)的大小View的measure过程分为两部分:

  1. 一部分必须做的事情写在final的这个被调用的measure方法中
  2. 另一部分可以让子类自由重写的事情写在另一个姊妹方法onMeasure()中,这个方法被measure方法调用,我们所有关心的测量工作都是在onMeasure()中做的,下面的描述中某些地方会略去measure方法

View和ViewGroup的onMeasure()中做的是两类不同的事情,而且具体的某个View或ViewGroup中的实现细节也会不同先以一个一层以上的View和ViewGroup为例说说思路:

  1. 首先最外层的ViewGroup的onMeasure方法被调用,然后做这几步
    1. 遍历每一个子View,做这些事:

      1. 调用#measureChildWithMargins#方法,这个方法中:
        1. 使用onMeasure方法传入的MeasureSpec【2】和子View的LayoutParams得到要传给子View的MeasureSpec【3】【4】
        2. 调用子View的measure方法,传入上面计算出的MeasureSpec
          • 如果子View还是ViewGroup,那么回到第一层进行迭代
          • 如果子View是View,那么就根据这个MeasureSpec以及自己的特性进行计算,最后调用**#setMeasuredDimension#**方法,完成自己最后的测量
      2. 经过上面的迭代,里边所有层的View和ViewGroup应该都测量结束了,取出当前这个子View的测量结果到记录器(对于FrameLayout 可能用最大的字View的大小,对于LinearLayout,可能是高度的累加,所以可能是累加器或是比较器或是别的,取决于具体的ViewGroup)
    2. 所有的孩子测量之后,根据记录器中所有子View的测量结果进行计算,调用**#setMeasuredDimension#**方法传入计算结果,完成自己最后的测量

上面就是onMeasure()的逻辑流程,这个流程用的是迭代的思想,非常可以另外还把通知测量和获取测量结果分了两步做,都是父View主导,调用子View的方法,而不是通过一个方法和它的返回值或者是回调来实现,需要注意一下


以一个简单的实例来讲解这个过程:

因为最外层的DecorView就是FrameLayout,所以就用它来看

(也是因为ViewGroup中没有onMeasure方法,只能由不同的实现类来实现,否则就会调用View的默认onMeasure方法)

一、FrameLayout 的onMeasure 方法

这个方法中做的事情跟前面说的思路是对应的,不再解释

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
              // 遍历自己的子View,只要不是GONE的都会参与测量
        if (mMeasureAllChildren || child.getVisibility() != GONE) { 
                           //....  
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
          
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            maxWidth = Math.max(maxWidth, child.getMeasuredWidth() +  lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            //....
        }
    }
 
    //....
    setMeasuredDimension(
        resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
        resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)
    );
    //....
}

 

二、继续看#measureChildWithMargins#方法的细节

这个方法中做的事情跟前面说的思路是对应的,不再解释

protected void measureChildWithMargins(View child,
                                       int parentWidthMeasureSpec,  int widthUsed,
                                       int parentHeightMeasureSpec, int heightUsed) {

    final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();

    // 根据父View的测量规格、父 View自己的Padding、子 View的Margin和已经用掉的空间大小( widthUsed),就能算出子View的 MeasureSpec,具体计算过程要看getChildMeasureSpec方法
         int paddingWitdth = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed;
    int paddingHeight = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin  + heightUsed;
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,paddingWitdth, lp. width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,paddingHeight, lp. height);
   
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

其中通过父亲传来的MeasureSpec和子View的LayoutParams计算得到子View的MeasureSpec的方法是*getChildMeasureSpec*

这个方法的三个参数的意思是:

  1. spec参数 表示父View的MeasureSpec
  2. padding参数 父View的Padding+子View的Margin,父View的大小减去这些边距,才能精确算出子View的MeasureSpec的size
  3. childDimension参数 表示该子View内部LayoutParams属性的值(lp.width或者lp.height)可以是wrap_content、match_parent、一个精确值(an exactly size)
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);  //获得父 View给的mode
    int specSize = MeasureSpec.getSize(spec);  //获得父 View给的大小

          // 父View给的大小 -自己的Padding+子 View的Margin,得到值才是子 View的大小。
         int size = Math.max( 0, specSize - padding);

    int resultSize = 0;    //初始化值,最后通过这个两个值生成子 View的MeasureSpec
    int resultMode = 0;    //初始化值,最后通过这个两个值生成子 View的MeasureSpec
 
    switch (specMode) {}//这里具体的switch case省略,逻辑与表[1]是一样的
 
    //根据上面逻辑条件获取的 mode和size构建 MeasureSpec对象。
         return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

 

三、假如子View就是个View,上面child.onMeasure的默认实现

与ViewGroup不同的是,View的onMeasure有一个默认实现,可以来看一下

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int measuredWidth  = getDefaultSize (getSuggestedMinimumWidth() , widthMeasureSpec );
    int measuredHeight = getDefaultSize (getSuggestedMinimumHeight(), heightMeasureSpec);

    setMeasuredDimension(measuredWidth,measuredHeight);
}

View的onMeasure方法默认实现很清晰:

  1. 计算测量结束后的宽高(就是计算测量结果)
    1. 这里计算宽高用的是一个默认方法:getDefaultSize,这个方法接收一个默认长度和一个measureSpec

    2. 这个方法的计算逻辑是:(见[1])

      1. 只要measureSpec的mode不是UNSPECIFIED(未确定的),那么就会用这个measureSpec的数值当做View的高度

      2. 如果measureSpec的mode是UNSPECIFIED,那么就用前面那个默认长度

        1. 而这个默认长度默认是用getSuggestedMinimumXXX计算方法计算的,它的计算逻辑是:(

          见[2])

          • 由View的Background尺寸和xml中设置的View的minXXX属性(比如android:minHeight)共同决定的
  2. 调用setMeasuredDimension(),传入测量结果
    1. setMeasuredDimension()可以简单理解就是给mMeasuredWidth和mMeasuredHeight设值,如果这两个值一旦设置了,那么意味着对于这个View的测量结束了
    2. 另外setMeasuredDimension方法必须在onMeasure方法中调用,不然会抛异常
  3. (View类默认的onMeasure方法只支持EXACTLY模式,所以如果)在自定义控件的时候不重写onMeasure方法,你就不能使用wrap_content,只能用matchparent和具体的size

对于其他的一些View的派生类,如TextView、Button、ImageView

  1. 它们的onMeasure方法系统了都做了重写
  2. 会先去测量字符或者图片的高度等,然后拿到View本身content这个高度(字符高度等)
  3. 如果MeasureSpec是AT_MOST,而且View本身content的高度不超出MeasureSpec的size,那么可以直接用View本身content的高度(字符高度等)
[1]
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:        //表示该View的大小父视图未定,设置为默认值
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
    }
    return result;
}
[2]
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

【1】DecorView是什么以及View系统的构成

  1. DecorView可以理解成整个页面的根View
  2. DecorView是一个FrameLayout,其中包含两个子View,一个是id=statusBarBackground的View、一个是LineaLayout
  3. id=statusBarBackground的View可以先不管(我也不是特别懂这个View,应该就是statusBar的设置背景的一个控件,方便设置statusBar的背景
  4. 这个LinearLayout比较重要,它包含一个title和一个content
    1. title是TitleBar或者ActionBar,如果我们没有设置NoActionBar的Theme,就会有这个东西
    2. content 就更简单了,setContentView()方法你应该用过吧,android.R.id.content 你应该听过吧,没错就是它
      • content是一个FrameLayout,你写的页面布局通过setContentView加进来就成了content的直接子View,是我们打交道最多的View

img

加一个Window、PhoneWindow是什么

  1. 简单来说,Window是一个抽象类,是所有视图的最顶层容器,视图的外观和行为都归他管,不论是背景显示,标题栏还是事件处理都是他管理的范畴,它其实就像是View界的太上皇(虽然能管的事情看似很多,但是没实权,因为抽象类不能直接使用)。
  2. 而 PhoneWindow 作为 Window 的唯一亲儿子(唯一实现类),自然就是 View 界的皇帝了,PhoneWindow 的权利可是非常大大,不过对于我们来说用处并不大,因为皇帝平时都是躲在深宫里面的,虽然偶尔用特殊方法能见上一面,但想要完全指挥 PhoneWindow 为你工作是很困难的。
  3. 而上面说的 DecorView 是 PhoneWindow 的一个内部类,其职位相当于小太监,就是跟在 PhoneWindow 身边专业为 PhoneWindow 服务的,除了自己要干活之外,也负责消息的传递,PhoneWindow 的指示通过 DecorView 传递给下面的 View,而下面 View 的信息也通过 DecorView 回传给 PhoneWindow。

【2】MeasureSpec的含义

  1. 含义
    1. MeasureSpec是两个单词组成,翻译过来“测量规格”或者“测量参数”
    2. MeasureSpec封装了从父容器传递给子容器的布局要求,“传递” 两个字很重要,因为它不是父容器对子容器的布局要求
    3. 换种说法是:MeasureSpec是由父View的MeasureSpec和子View的LayoutParams通过简单的计算得出一个针对子View的测量要求,这个测量要求就是MeasureSpec
  2. 表示方法
    1. 一个MeasureSpec是一个大小跟模式的组合值
    2. MeasureSpec是一个32位 整型,其中高两位是mode,后面30位存的是size
    3. mode一共有三种:
      1. UNSPECIFIED(未确定的) : 父容器对于子容器没有任何限制,子容器想要多大就多大
      2. EXACTLY: 父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间。
      3. AT_MOST:子容器可以是声明大小内的任意大小
  3. 与view尺寸(就是LayoutParams,你在xml的layout_width和layout_height,layout_xxx的值最后都会封装到这个LayoutParams里)的对比
    1. 一个view可以这么描述自己的尺寸(不用另外配合SIZE使用)
      1. wrap_content:内容有多少,就定多少
      2. match_parent:爸妈给多少,就定多少
      3. size:一个固定的尺寸
    2. 父view可以这么告诉自己的子view的MODE(需要另外配合SIZE使用)
      1. UPSPECIFIED:你想要多少,就定多少(这时候SIZE是0)
      2. AT_MOST:我给你一个尺寸,不要超过它就好
      3. EXACTLY:就这么大,固定的

【3】如何把父亲传来的MeasureSpec和子View的LayoutParams计算得到子View的MeasureSpec

  1. 计算表格,见表[1](不知道是不是固定不变的)
  2. 这句话为什么这么怪
    1. 这句话容易引起误会,严格说是这样:爷爷A,父亲B,儿子C,B用A给的东西a和C的东西c生成了一个b,然后把这个b给C,这个过程不断迭代
    2. 但这样又容易让人感觉怪异,其实换种说法是这样:爷爷A,父亲B,儿子C,B用自己的b和C的c生成了一个新c,把这个新c给C,这样就感觉好理解多了,其实是一样的,因为B的这个b其实就是A给的

表[1]

(下)父View给的MeasureSpec(右)子View的LayoutParams(内容)要给子View的结果MeasureSpec size match_parent wrap_content
EXACTLY LayoutParams.size(居然是听子View的!但是可能无法全部显示)EXACTLY MeasureSpec.sizeEXACTLY MeasureSpec.sizeAT_MOST(也是不确定,需要到子View那儿去之后再根据具体情况计算)
AT_MOST LayoutParams.sizeEXACTLY MeasureSpec.sizeAT_MOST MeasureSpec.sizeAT_MOST
UNSPECIFIED LayoutParams.sizeEXACTLY 0(一旦没有任何要求和约束,size的值就没有任何意义了,所以一般都直接设置成0)UNSPECIFIED 0UNSPECIFIED

【4】最外面没有父亲的那层的MeasureSpec是从哪里来?

private void performTraversals() {
    ......
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    ......
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    ......
}

多补了两句代码,可以看到最早的MeasureSpec是通过getRootMeasureSpec方法返回的,看这个方法做了什么:

 private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.EXACTLY);
            break;
        ......
    }
    return measureSpec;
}


这个方法做的事情是:

  1. 这个方法第一个参数会传入屏幕的宽度和高度:mWith和mHeight
  2. 而第二个参数传入的lp是WindowManager.LayoutParams,它的lp.width和lp.height的默认值是MATCH_PARENT
  3. 所以通过getRootMeasureSpec 生成的测量规格MeasureSpec 的mode是MATCH_PARENT ,size是屏幕的高宽

2.layout过程

 private void performTraversals() {
    ......
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    ......
    mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
    ......
    mView.draw(canvas);
    ......
}



 

performTraversals 方法执行完mView.measure 计算出mMeasuredXXX后就开始执行layout 函数来确定View具体放在哪个位置

我们计算出来的View目前只知道view矩阵的大小,具体这个矩阵放在哪里,这就是layout 的工作了

所以layout的作用是 :根据子视图的大小以及布局参数将View树放到合适的位置上

(好像只有在ViewGroup的时候需要关心这个方法)

mView肯定是个ViewGroup,不会是View,我们直接看下ViewGroup 的layout函数

public 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 {
        mLayoutCalledWhileSuppressed = true;
    }
}

这段代码的意思是:

  1. 如果当前ViewGroup未添加LayoutTransition动画,或者LayoutTransition动画此刻并未运行,那么调用super.layout(l, t, r, b)也就是View的layout方法,继而调用到View中的onLayout
  2. 否则将mLayoutSuppressed设置为true,等待动画完成时再调用requestLayout()
所以View的layout方法:
public final void layout(int l, int t, int r, int b) {
    .....
          boolean changed = setFrame(l, t, r, b);
   
    // 判断View的位置是否发生过变化,看有必要进行重新 layout吗
         if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType. ON_LAYOUT);
        }
                   onLayout(changed, l, t, r, b);
        mPrivateFlags &= ~LAYOUT_REQUIRED;
    }
    mPrivateFlags &= ~FORCE_LAYOUT;
    .....
}

其中:

  1. setFrame(l, t, r, b) 可以理解为给mLeft 、mTop、mRight、mBottom赋值,然后基本就能确定View自己在父视图的位置了,这几个值构成的矩形区域就是该View显示的位置,这里的具体位置都是相对与父视图的位置
  2. 回调onLayout
    1. 对于View来说,onLayout只是一个空实现,一般情况下我们也不需要重载该函数
    2. 对于ViewGroup 来说,它的onLayout 方法多了关键字abstract的修饰,要求其子类必须重载
      • 而重载onLayout的目的就是安排其children在父视图的具体位置,那么如何安排子View的具体位置呢?
 int childCount = getChildCount() ;
for(int i= 0 ;i< childCount ;i ++){
    View child = getChildAt(i) ;
    child.layout(l, t, r, b) ;
}


  1. 代码很简单,就是遍历自己的孩子,然后调用 child.layout(l, t, r, b) ,给子view 通过setFrame(l, t, r, b) 确定位置
  2. 而重点是(l, t, r, b) 怎么计算出来的呢
    1. 还记得我们之前测量过程,测量出来的MeasuredWidth和MeasuredHeight吗?还记得你在xml 设置的Gravity吗?还有RelativeLayout 的其他参数吗,没错,就是这些参数和MeasuredHeight、MeasuredWidth 一起来确定子View在父视图的具体位置的。具体的计算过程大家可以看下最简单FrameLayout 的onLayout 函数的源码,每个不同的ViewGroup 的实现都不一样
      1. 提一下写在LayoutParams中的布局参数是怎么作用的:
      2. DecorView在确定给每个View分配的屏幕区域大小时,是允许View参与进来,与它一起商量的,但是每个View在屏幕区域中的位置就不能让View自己来决定了,而是由DecorView一手操办
        1. 虽然View无法决定自己在ViewGroup中的位置,但是开发者在使用View时,可以向ViewGroup表达自己所用的View要放在哪里,也就是layout_*之类的配置
        2. layout_*之类的配置虽然在书写上与View的属性在一起,但它们并不是View的属性,它们只是使用该View的使用者用来细化调整该View在ViewGroup中的位置的,同时,这些值在Inflate时,是由ViewGroup读取,然后生成一个ViewGroup特定的LayoutParams对象,再把这个对象存入子View中
          • 这样,ViewGroup在为该子View安排位置时,就可以参考这个LayoutParams中的信息了
          • 我们发现,调用inflate时,除了输入布局文件的id外,一般要求传入parent ViewGroup,传入这个参数的目的,就是为了读取布局文件中的layout配置信息,如果没有传入,这些信息将会丢失
          • 不同的ViewGroup拥有不同的LayoutParams内部类,这是因为,它们所允许的子View微微调整自己的位置的方式是不一样的,具体讲就是配置子View时,允许使用的layout_*是不一样的
            1. 比如,RelativeLayout就允许layout_toRightOf等配置,其他的ViewGroup没有这些配置
          • 这些确定View的位置的过程,被包装在View 的layout方法中,这样我们也很容易理解,对于基本View而言,这个方法是没有用的,所以都是空的,你可以查看下ImageView、TextView等的源代码,验证下这一点。对于ViewGroup而言,它们会用该方法为自己的子View安排位置
    2. 另外:因为View的最终的布局位置和大小完全由(l, t, r, b) 这4个参数决定,而measure过程产生的MeasuredWidth和MeasuredHeight这两个参数为计算这四个值提供了一个很重要的依据,但是这两个参数也不是必须的,所以measure过程并不是必须的,也就是我们完全可以不使用这两个值,这4个参数完全可以由我们任意指,而如果这样,getMeasuredWidth() 和getWidth() 就很有可能不是同一个值,它们的计算是不一样的:
      1. public final int getMeasuredWidth() { return mMeasuredWidth & MEASURED_SIZE_MASK; }
      2. public final int getWidth() { return mRight - mLeft; }

一个疑问:onlayout中不需要用左上右下这四个参数吗?只是在layout方法中setFrame时使用?


3.draw过程

performTraversals 方法的下一步就是mView.draw(canvas);

  1. View的draw 方法虽然不是final的,但一般不去重写,官网文档也建议不要去重写draw 方法,一般是重写其中第三步draw内容时回调的onDraw方法

  2. View 的onDraw(canvas) 是空实现,ViewGroup 也没有实现,每个View的内容是各不相同的,所以需要由子类去实现具体逻辑

  3. onDraw()函数将会传给你一个Canvas

    对象,通过它你可以在二维图形上做任何事情,包括其他的一些标准和通用的组件、文本的格式,任何你可以想到的东西都可以通过它实现

    • Canvas就像是一个画板,使用Paint就可以在上面作画了
  4. 另外:ViewGroup通常情况下不需要绘制,因为它本身就没有需要绘制的东西,如果不是指定了ViewGroup的背景颜色,那么它的onDraw方法都不会被调用,但是它会使用dispatchDraw方法来绘制自己的子View,过程是遍历所有子View,调用子View的绘制方法

我们先来看下View的draw方法源码:

public void draw(Canvas canvas) {
    ...
    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas' layers to prepare for fading
     *      3. Draw view's content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */

    // Step 1, draw the background, if needed
    ...
    background.draw(canvas);
    ...
    // skip step 2 & 5 if possible (common case)
    ...
    // Step 2, save the canvas' layers
    ...
    if (solidColor == 0) {
        final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

        if (drawTop) {
            canvas.saveLayer(left, top, right, top + length, null, flags);
        }
        ...
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers

        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            canvas.drawRect(left, top, right, top + length, p);
        }
        ...
        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
    }
}

注释写得比较清楚,一共分成6步,看到注释没有( // skip step 2 & 5 if possible (common case))除了2 和 5之外 我们一步一步来看:

1、第一步:背景绘制

看注释即可,不是重点

 private void drawBackground(Canvas canvas) {
    Drawable final Drawable background = mBackground;
    ......
    //mRight - mLeft, mBottom - mTop layout确定的四个点来设置背景的绘制区域
    if (mBackgroundSizeChanged) {
        background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
        mBackgroundSizeChanged = false; rebuildOutline();
    }
    ......
    //调用Drawable的draw() 把背景图片画到画布上
    background.draw(canvas);
    ......
}

2、第三步,对View的内容进行绘制。

onDraw(canvas) 方法是view用来draw 自己的,具体如何绘制,颜色线条什么样式就需要子View自己去实现,View.java 的onDraw(canvas) 是空实现,ViewGroup 也没有实现,每个View的内容是各不相同的,所以需要由子类去实现具体逻辑。

3、第4步 对当前View的所有子View进行绘制

dispatchDraw(canvas) 方法是用来绘制子View的,View.java 的dispatchDraw()方法是一个空方法,因为View没有子View,不需要实现dispatchDraw ()方法,ViewGroup就不一样了,它实现了dispatchDraw ()方法:

@Override
protected void dispatchDraw(Canvas canvas) {
    ...
    if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
        for (int i = 0; i < count; i++) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
    } else {
        for (int i = 0; i < count; i++) {
            final View child = children[getChildDrawingOrder(count, i)];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
    }
    ......
} 


这段代码:

  1. 就是遍历子View然后drawChild()
  2. drawChild()方法实际调用的是子View.draw()方法
  3. ViewGroup类已经为我们实现绘制子View的默认过程,这个实现基本能满足大部分需求,所以ViewGroup类的子类(LinearLayout,FrameLayout)也基本没有去重写dispatchDraw方法
    • 我们在实现自定义控件,除非比较特别,不然一般也不需要去重写它
  4. drawChild()的核心过程就是为子视图分配合适的cavas剪切区
    • 剪切区的大小正是由layout过程决定的
    • 而剪切区的位置取决于滚动值以及子视图当前的动画
    • 设置完剪切区后就会调用子视图的draw()函数进行具体的绘制了。

View绘制流程

View的绘制流程也是从上到下一层层遍历绘制的。我们最顶层的View是DecorView,但DecorView继承自FrameLayout,而ViewGroup的draw方法继承自View,so,所以我们直接看View#draw即可。

public void draw(Canvas canvas) {
    
    
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas' layers to prepare for fading
     *      3. Draw view's content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */
    // Step 1, draw the background, if needed
    int saveCount;
    if (!dirtyOpaque) {
    
    
        drawBackground(canvas);
    }
    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
    
    
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);
        // Step 4, draw the children
        dispatchDraw(canvas);
        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
    
    
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }
        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
        // we're done...
        return;
    }
    ...
}
复制代码

draw方法中,官方对其的步骤进行了清晰的注释,我们来看下流程,在执行流程之前会检查绘制区域是否透明:

  • 1、绘制View背景,如果透明则不绘制
  • 2、如果需要,则保存画布的图层
  • 3、绘制View内容,如果透明则不绘制
  • 4、绘制子View————这个很重要
  • 5、如果需要,则绘制View的褪色边缘和恢复图层
  • 6、绘制装饰滚动条等

这里最重要的步骤是第四步,绘制子View,现在我们来看下这个ViewGroup#dispatchDraw(canvas)方法,注意这里的View是一个DecorView,所以要在ViewGroup中去查看这个方法,View中的这个方法是一个空方法。

protected void dispatchDraw(Canvas canvas) {
    
    
    ...
    for (int i = 0; i < childrenCount; i++) {
    
    
        while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
    
    
            final View transientChild = mTransientViews.get(transientIndex);
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation() != null) {
    
    
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
    
    
                transientIndex = -1;
            }
        }
        int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
        final View child = (preorderedList == null)
                ? children[childIndex] : preorderedList.get(childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
    
    
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    while (transientIndex >= 0) {
    
    
        // there may be additional transient views after the normal views
        final View transientChild = mTransientViews.get(transientIndex);
        if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                transientChild.getAnimation() != null) {
    
    
            more |= drawChild(canvas, transientChild, drawingTime);
        }
        transientIndex++;
        if (transientIndex >= transientCount) {
    
    
            break;
        }
    }
    ...
}
复制代码

上述代码对所有的子View进行遍历,并调用ViewGroup#drawChild方法。

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    
    
    return child.draw(canvas, this, drawingTime);
}
复制代码

drawChild又调用了子View的draw方法,这样绘制就传递了下去,当然这个draw方法和之前这一小节一开始介绍的View#draw方法并不一样,我们来看看

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    
    
	...
	if (!drawingWithRenderNode) {
    
    
    	computeScroll();
    	sx = mScrollX;
    	sy = mScrollY;
	}
	...
    if (!drawingWithDrawingCache) {
    
    
        if (drawingWithRenderNode) {
    
    
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
        } else {
    
    
            // Fast path for layouts with no backgrounds
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
    
    
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                dispatchDraw(canvas);
            } else {
    
    
                draw(canvas);
            }
        }
    } else if (cache != null) {
    
    
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        if (layerType == LAYER_TYPE_NONE) {
    
    
            // no layer paint, use temporary paint to draw bitmap
            Paint cachePaint = parent.mCachePaint;
            if (cachePaint == null) {
    
    
                cachePaint = new Paint();
                cachePaint.setDither(false);
                parent.mCachePaint = cachePaint;
            }
            cachePaint.setAlpha((int) (alpha * 255));
            canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
        } else {
    
    
            // use layer paint to draw the bitmap, merging the two alphas, but also restore
            int layerPaintAlpha = mLayerPaint.getAlpha();
            mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
            canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
            mLayerPaint.setAlpha(layerPaintAlpha);
        }
    }
	...
}
复制代码

上述代码会先判断之前是否进行过了绘制,如果没有则进入快速绘制通道,对没有背景的View进行绘制。判断是否需要跳过自身的draw绘制方法,如果跳过则进入dispatchDraw,不跳过则进入当前View的draw方法,即这一小节开头的draw方法,就此形成了循环。

常用优化技巧:

1、onDraw()中不要创建新的局部对象

onDraw()会被频繁调用,如果方法内部创建了局部变量,则会一瞬间产生大量的临时对象,这使得占用过多的内存,系统频繁GC,降低了程序执行效率。

2、避免onDraw()中执行大量耗时操作

View的最佳绘制效率为60fps,因为LCD的频率是60HZ,显示每一帧的间隔是16ms,所以每一个VSync信号的时间间隔是16ms,接收到该信号时视图会进行刷新,如果你绘画时间过长就会导致View绘制不流畅,可以使用多线程来解决。

3、避免Overdraw

在同一个地方绘制多次肯定是浪费资源的,也避免浪费资源去渲染那些不必要和看不见的背景。我们可以在手机的开发者设置中开启调试GPU过度绘制选项来查看视图绘制的情况。

总结

从View的测量、布局和绘制原理来看,要实现自定义View,根据自定义View的种类不同,可能分别要自定义实现不同的方法。但是这些方法不外乎:onMeasure()方法,onLayout()方法,onDraw()方法。

更多UI绘制,往:Android核心技术进阶学习,获取~

onMeasure()方法:单一View,一般重写此方法,针对wrap_content情况,规定View默认的大小值,避免与match_parent情况一致。ViewGroup,若不重写,就会执行和单子View中相同逻辑,不会测量子View,一般会重写onMeasure()方法,循环测量子View。

onLayout()方法:单一View,不需要实现该方法。ViewGroup必须实现,该方法是一个抽象方法,实现该方法,来对子View进行布局。

onDraw()方法:无论单一View,或者ViewGroup都需要实现该方法,因其是一个空方法。

猜你喜欢

转载自blog.csdn.net/Androidxiaofei/article/details/125336840
今日推荐