zoukankan      html  css  js  c++  java
  • Android之View绘制流程源码分析

    版权声明:本文出自汪磊的博客,转载请务必注明出处。

    对于稍有自定义View经验的安卓开发者来说,onMeasure,onLayout,onDraw这三个方法都不会陌生,起码多少都有所接触吧。

    在安卓中,一个View显示到屏幕上基本上都是经过测量,摆放,绘制这三个过程才显示出来,那么这三个过程到底是怎么执行的呢?本文与大家一起探讨一下安卓中View的绘制流程。

    一,View树绘制流程开始的地方(API23)

    对一个布局进项测量,摆放,绘制肯定要有开始的地方吧,这里就直接跟大家说了,View绘制流程开始的地方是ViewRootImpl类的performTraversals()方法(至于为什么是这里不是本篇重点,后续有时间写一篇针对这里的文章说明一下),接下来我们看下performTraversals()方法(此方法过长,只列出重要逻辑代码)

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

     第3,4行代码调用getRootMeasureSpec方法生成对应宽高,我们先看下getRootMeasureSpec都做了什么,源码如下:

     1   /**
     2      * Figures out the measure spec for the root view in a window based on it's
     3      * layout params.
     4      *
     5      * @param windowSize
     6      *            The available width or height of the window
     7      *
     8      * @param rootDimension
     9      *            The layout params for one dimension (width or height) of the
    10      *            window.
    11      *
    12      * @return The measure spec to use to measure the root view.
    13      */
    14     private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    15         int measureSpec;
    16         switch (rootDimension) {
    17 
    18         case ViewGroup.LayoutParams.MATCH_PARENT:
    19             // Window can't resize. Force root view to be windowSize.
    20             measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
    21             break;
    22         case ViewGroup.LayoutParams.WRAP_CONTENT:
    23             // Window can resize. Set max size for root view.
    24             measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
    25             break;
    26         default:
    27             // Window wants to be an exact size. Force root view to be that size.
    28             measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
    29             break;
    30         }
    31         return measureSpec;
    32     }

     先说一下MeasureSpec这个概念:也叫测量规格,MeasureSpec是一个32位整数,由SpecMode和SpecSize两部分组成,其中,高2位为SpecMode,低30位为SpecSize。SpecMode为测量模式,SpecSize为相应测量模式下的测量尺寸。

    View(包括普通View和ViewGroup)的SpecMode由本View的LayoutParams结合父View的MeasureSpec生成(普通View的MeasureSpec是由其父类ViewGroup生成的,后面会详细讲到)。
    SpecMode的取值可为以下三种:

     MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;

     MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;

     MeasureSpec.UNSPECIFIED //未指定模式,父View对子View的大小不做限制,完全由子View自己决定;

     

    getRootMeasureSpec方法就是生成根视图的MeasureSpec,还记得我们上一篇《Android之View绘制流程开胃菜---setContentView(...)详细分析》中分析的吗,平时我们自己写的布局都是被添加到DecorView中id为content的布局中的,这里传入进来的windowSize参数是window的可用宽高信息,rootDimension宽高参数均为MATCH_PARENT。

    我们上面说普通View的MeasureSpec是由其父类ViewGroup生成的,但是根视图DecorView是没有父类的,所以getRootMeasureSpec就是给根视图生成测量规格的,生成的MeasureSpec中SpecMode为MeasureSpec.EXACTLY,SpecSize则为窗口的可用尺寸。

    回到performTraversals()方法中:

    3,4行代码分别生成宽高的测量规格

    7,9,11行代码分别执行performMeasure(childWidthMeasureSpec, childHeightMeasureSpec), performLayout(lp, desiredWindowWidth, desiredWindowHeight),performDraw();

    方法,我们看下这三个方法源码:都经过简化处理

    1 private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    2         Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    3         try {
    4             mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    5         } finally {
    6             Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    7         }
    8     }
    1    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
    2             int desiredWindowHeight) {
    3   
    4         ...
    5         final View host = mView;
    6 
    7         host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    8        ...
    9     }        
    1     private void performDraw() {
    2            ...
    3             draw(fullRedrawNeeded);
    4             ...
    5     }
    1 private void draw(boolean fullRedrawNeeded) {
    2 
    3           ...
    4           if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
    5              return;
    6           }
    7           ...
    8     }
    1     private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
    2             boolean scalingRequired, Rect dirty) {
    3         ...
    4          mView.draw(canvas);
    5         ...
    6         return true;
    7     }
    performMeasure方法最核心的是第4行调用mView的measure方法。
    performLayout方法通过5,7行代码发现其实也是调用的mView的layout方法。
    performDraw最终调用的也是调用的mView的draw方法。
    上面的mView就是DecorView,我们知道DecorView是FrameLayout,FrameLayout继承自ViewGroup,ViewGroup继承自View,所以最终都会调用View类中measure,
    layout,draw方法。
    实际上View的绘制流程可以分为三个阶段:
    • measure: 判断是否需要重新计算View的大小,需要的话则计算;
    • layout: 判断是否需要重新计算View的位置,需要的话则计算;
    • draw: 判断是否需要重新绘制View,需要的话则重绘制。
    大体流程如图:

    二,View绘制流程第一步measure过程分析(API23)
    接下来我们看下View中的measure源码:简化处理
     1    /**
     2      * <p>
     3      * This is called to find out how big a view should be. The parent
     4      * supplies constraint information in the width and height parameters.
     5      * </p>
     6      *
     7      * <p>
     8      * The actual measurement work of a view is performed in
     9      * {@link #onMeasure(int, int)}, called by this method. Therefore, only
    10      * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
    11      * </p>
    12      *
    13      *
    14      * @param widthMeasureSpec Horizontal space requirements as imposed by the
    15      *        parent
    16      * @param heightMeasureSpec Vertical space requirements as imposed by the
    17      *        parent
    18      *
    19      * @see #onMeasure(int, int)
    20      */
    21     public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    22         ...
    23         // measure ourselves, this should set the measured dimension flag back
    24         onMeasure(widthMeasureSpec, heightMeasureSpec);
    25          ...
    26     }

    注释已经给出大体描述:这个被调用用来测算出view大小,并且其父类提供了约束信息widthMeasureSpec与heightMeasureSpec。

    我们发现measure方法被final修饰,所以这个方法不能被子类重写。

    实际的测量是在onMeasure方法进行,所以在View的普通子类中需要重写onMeasure方法来实现自己的测量逻辑。

    对于普通View,调用View类的onMeasure()方法来进行实际的测量工作即可,当然我们也可以重载onMeasure并调用setMeasuredDimension来设置任意大小的布局。

    接下来我们看下默认情况下View类中onMeasure方法都做了什么,源码如下;

     1     /**
     2      * <p>
     3      * Measure the view and its content to determine the measured width and the
     4      * measured height. This method is invoked by {@link #measure(int, int)} and
     5      * should be overridden by subclasses to provide accurate and efficient
     6      * measurement of their contents.
     7      * </p>
     8      *
     9      * <p>
    10      * <strong>CONTRACT:</strong> When overriding this method, you
    11      * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
    12      * measured width and height of this view. Failure to do so will trigger an
    13      * <code>IllegalStateException</code>, thrown by
    14      * {@link #measure(int, int)}. Calling the superclass'
    15      * {@link #onMeasure(int, int)} is a valid use.
    16      * </p>
    17      *
    18      * <p>
    19      * The base class implementation of measure defaults to the background size,
    20      * unless a larger size is allowed by the MeasureSpec. Subclasses should
    21      * override {@link #onMeasure(int, int)} to provide better measurements of
    22      * their content.
    23      * </p>
    24      *
    25      * <p>
    26      * If this method is overridden, it is the subclass's responsibility to make
    27      * sure the measured height and width are at least the view's minimum height
    28      * and width ({@link #getSuggestedMinimumHeight()} and
    29      * {@link #getSuggestedMinimumWidth()}).
    30      * </p>
    31      *
    32      * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
    33      *                         The requirements are encoded with
    34      *                         {@link android.view.View.MeasureSpec}.
    35      * @param heightMeasureSpec vertical space requirements as imposed by the parent.
    36      *                         The requirements are encoded with
    37      *                         {@link android.view.View.MeasureSpec}.
    38      *
    39      * @see #getMeasuredWidth()
    40      * @see #getMeasuredHeight()
    41      * @see #setMeasuredDimension(int, int)
    42      * @see #getSuggestedMinimumHeight()
    43      * @see #getSuggestedMinimumWidth()
    44      * @see android.view.View.MeasureSpec#getMode(int)
    45      * @see android.view.View.MeasureSpec#getSize(int)
    46      */
    47     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    48         setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
    49                 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    50     }

    这个方法看注释就已经大体明白了,简单翻译一下吧:这个方法用来测量view以及自身内容来决定宽高,子类应该重写这个方法提供更精确更高效的测量的内容。当重写这个方法的时候子类必须调用setMeasuredDimension(int, int)来存储已经测量出来的宽高。
    我们看到系统默认的onMeasure方法只是直接调用了setMeasuredDimension,setMeasuredDimension函数是一个很关键的函数,它对View的成员变量mMeasuredWidth和mMeasuredHeight变量赋值,measure的主要目的就是对View树中的每个View的mMeasuredWidth和mMeasuredHeight进行赋值,所以一旦这两个变量被赋值意味着该View的测量工作结束。

    接下来我们看看设置的默认View宽高,默认宽高都是通过getDefaultSize方法来获取的,而getDefaultSize又调用了getSuggestedMinimumXXX方法,我们先看下getSuggestedMinimumXXX方法:

    1 protected int getSuggestedMinimumHeight() {
    2         return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    3 }
    1 protected int getSuggestedMinimumWidth() {
    2         return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    3 }
    mMinHeight或mMinWidth就是我们设置的android:minHeight或android:minWidth参数。
    如果我们没有设置背景则直接返回mMinHeight或mMinWidth,如果设置了背景,则返回miniXXX属性与mBackground二者中较大者。如背景以及miniXXX属性都没设置呢?那就返回0了。
    接下来再看getDefaultSize方法源码:
     1 public static int getDefaultSize(int size, int measureSpec) {
     2         int result = size;
     3         int specMode = MeasureSpec.getMode(measureSpec);
     4         int specSize = MeasureSpec.getSize(measureSpec);
     5 
     6         switch (specMode) {
     7         case MeasureSpec.UNSPECIFIED:
     8             result = size;
     9             break;
    10         case MeasureSpec.AT_MOST:
    11         case MeasureSpec.EXACTLY:
    12             result = specSize;
    13             break;
    14         }
    15         return result;
    16 }
    getDefaultSize返回值由上面讲到的getSuggestedMinimumXXX方法获取的Size以及父类传递过来的measureSpec共同决定。
    可以看到如果specMode等于AT_MOST或EXACTLY就返回specSize,这也是系统默认的规格。
    到此为止,普通View(非ViewGroup)的测量就基本讲完了。但是ViewGroup这种容器类布局是怎么测量其内每个子View的呢?
    ViewGroup容器类布局大部分情况下是用来嵌套具体子View的,所以需要负责其子View的测量,在ViewGroup中定义了
    measureChildren(int widthMeasureSpec, int heightMeasureSpec)
    measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec)
    以及measureChildWithMargins(View child,int parentWidthMeasureSpec, int widthUsed,int parentHeightMeasureSpec, int heightUsed)
    三个方法来供其子类调用对具体子View进行测量。
    measureChildren,measureChild源码如下:
     1 protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
     2         final int size = mChildrenCount;
     3         final View[] children = mChildren;
     4         for (int i = 0; i < size; ++i) {
     5             final View child = children[i];
     6             if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
     7                 measureChild(child, widthMeasureSpec, heightMeasureSpec);
     8             }
     9         }
    10}
    
    
     1 protected void measureChild(View child, int parentWidthMeasureSpec,
     2             int parentHeightMeasureSpec) {
     3         final LayoutParams lp = child.getLayoutParams();
     4 
     5         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
     6                 mPaddingLeft + mPaddingRight, lp.width);
     7         final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
     8                 mPaddingTop + mPaddingBottom, lp.height);
     9 
    10         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    11}

    看到了吧,measureChildren只是循环调用measureChild方法,而measureChild方法中会根据父类提供的测量规格parentXXXMeasureSpec一级子类自己LayoutParams调用getChildMeasureSpec方法生成子类自己具体的测量规格。(getChildMeasureSpec稍后会具体分析)

    接下来我们看下measureChildWithMargins方法源码:

     

     1 protected void measureChildWithMargins(View child,
     2             int parentWidthMeasureSpec, int widthUsed,
     3             int parentHeightMeasureSpec, int heightUsed) {
     4         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
     5 
     6         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
     7                 mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
     8                         + widthUsed, lp.width);
     9         final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
    10                 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
    11                         + heightUsed, lp.height);
    12 
    13         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    14 }

    与measureChild相比最主要的区别就是measureChildWithMargins额外将具体子View LayoutParams参数的margin也当作参数来生成测量规格。

    measureChild与measureChildWithMargins均调用了getChildMeasureSpec方法来生成具体测量规格,接下来我们重点看下这个方法:

     1     public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
     2         int specMode = MeasureSpec.getMode(spec);//获取父View的mode 3         int specSize = MeasureSpec.getSize(spec);//获取父View的size
     4      //父View的size减去padding与0比较取其大,specSize - padding得到的值是父View可以用来盛放子View的空间大小
     5         int size = Math.max(0, specSize - padding);
     6 
     7         int resultSize = 0;
     8         int resultMode = 0;
     9 
    10         switch (specMode) {
    11         // Parent has imposed an exact size on us
    12         case MeasureSpec.EXACTLY://父View希望子View是明确大小
    13             if (childDimension >= 0) {//子View设置了明确的大小:如 10dp,20dp
    14                 resultSize = childDimension;//设置子View测量规格大小为其本身设置的大小
    15                 resultMode = MeasureSpec.EXACTLY;//mode设置为EXACTLY
    16             } else if (childDimension == LayoutParams.MATCH_PARENT) {//子VIEW的宽或者高设置为MATCH_PARENT,表明子View想和父View一样大小
    17                 // Child wants to be our size. So be it.
    18                 resultSize = size;//设置子View测量规格大小为父View可用空间的大小
    19                 resultMode = MeasureSpec.EXACTLY;//mode设置为EXACTLY
    20             } else if (childDimension == LayoutParams.WRAP_CONTENT) {//子VIEW的宽或者高设置为WRAP_CONTENT,表明子View大小是动态的
    21                 // Child wants to determine its own size. It can't be
    22                 // bigger than us.
    23                 resultSize = size;//设置子View测量规格大小为父View可用空间的大小
    24                 resultMode = MeasureSpec.AT_MOST;//mode设置为AT_MOST,表明子View宽高最大值不能超过resultSize 
    25             }
    26             break;27      //其余情况请自行分析
    28         ......
    29 return MeasureSpec.makeMeasureSpec(resultSize, resultMode); 30 }

    想说的注释已经给出。

    上面的方法展现了根据父View的MeasureSpec和子View的LayoutParams生成子View的MeasureSpec的过程, 子View的LayoutParams表示了子View的期待大小。这个产生的MeasureSpec用于指导子View自身的测量。

    在我们自定义View的时候一般会重写onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法其中的widthMeasureSpec与heightMeasureSpec参数就是父类通过getChildMeasureSpec方法生成的。一个好的自定义View会根据父类传递过来的测量规格动态设置大小,而不是直接写死其大小。

    好了,到此为止View的测量过程想说的就差不多都说完了,我们稍微总结一下关键的部分;

    • View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。

    • 最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的。

    • ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,以供容器类布局测量自身子View使用

    • 使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值,只有onMeasure流程完后mMeasuredWidth与mMeasuredHeight才会被赋值

    • View的布局大小是由父View和子View共同决定的。我们平时设置的宽高可以理解为希望的大小,具体大小还要结合父类大小来确定。

     

    最后附上View绘制流程图:相信你会理解的更加深刻:

    三,View绘制流程第二步layout过程分析(API23)
    performMeasure执行完,接着就会执行performLayout:
    1    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
    2             int desiredWindowHeight) {
    3   
    4         ...
    5         final View host = mView;
    6 
    7         host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    8        ...
    9     } 
    mView为根View,即DecorView,DecorView是FrameLayout的子类,最终会调用ViewGroup中layout方法。
    所以接下来我们看下ViewGroup中layout方法源码:
     1 @Override
     2     public final void layout(int l, int t, int r, int b) {
     3         if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
     4             if (mTransition != null) {
     5                 mTransition.layoutChange(this);
     6             }
     7             super.layout(l, t, r, b);
     8         } else {
     9             // record the fact that we noop'd it; request layout when transition finishes
    10             mLayoutCalledWhileSuppressed = true;
    11         }
    12     }
    
    

     第7行代码表明又调用父类View的layout方法。所以我们看下View的layout源码,如下:

    1     public void layout(int l, int t, int r, int b) {
    2     // l为本View左边缘与父View左边缘的距离
    // t为本View上边缘与父View上边缘的距离
           // r为本View右边缘与父View左边缘的距离
          // b为本View下边缘与父View上边缘的距离
    
    3     ...
    4         boolean changed = isLayoutModeOptical(mParent) ?
    5                 setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    6 
    7         if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
    8             onLayout(changed, l, t, r, b);
    9      ...
    10     }

    4,5行代码主要判断View的位置是否发生变化,发生变化则changed 会为true,并且setOpticalFrame也是调用的setFrame方法

    我们看下setFrame方法源码:

     1 protected boolean setFrame(int left, int top, int right, int bottom) {
     2         boolean changed = false;
     3 
     4 
     5      ...
     6         if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
     7             changed = true;
     8     
     9             ...
    10             mLeft = left;
    11             mTop = top;
    12             mRight = right;
    13             mBottom = bottom;
    14      ...
    15         }
    16         return changed;
    17     }    

    第6行代码分别比较之前的记录的mLeft,mRight,mTop,mBottom 与新传入的参数如果有一个不同则进入判断,将changed变量置为true,并且将新传入的参数分别重新赋值给mLeft,mRight,mTop,mBottom,最后返回changed。

    这里还有一点要说,getWidth()、getHeight()和getMeasuredWidth()、getMeasuredHeight()这两对方法之间的区别,先看一下源码;

     1    public final int getMeasuredWidth() {
     2         return mMeasuredWidth & MEASURED_SIZE_MASK;
     3     }
     4 
     5     public final int getMeasuredHeight() {
     6         return mMeasuredHeight & MEASURED_SIZE_MASK;
     7     }
     8 
     9     public final int getWidth() {
    10         return mRight - mLeft;
    11     }
    12 
    13     public final int getHeight() {
    14         return mBottom - mTop;
    15     }

    在讨论View的measure过程时提到过mMeasuredWidth与mMeasuredHeight只有测量过程完成才会被赋值,所以只有测量过程完成调用getMeasuredWidth()、getMeasuredHeight()才会获取正确的值。

    同样getWidth()、getHeight()只有在layout过程完成时mLeft,mRight,mTop,mBottom才会被赋值,调用才会获取正确返回值,所以二者调用时机是不同的。

    继续看View中layout源码第7行,如果changed为true,也就是说View的位置发生了变化,或者标记为PFLAG_LAYOUT_REQUIRED则进入判断执行onLayout方法。

    我们继续看View中onLayout方法源码:

    1     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    2     }

    看到了吧,竟然是个空方法。

    对比View的layout和ViewGroup的layout方法发现,View的layout方法是可以在子类重写的,而ViewGroup的layout是不能在子类重写的,那么容器类View是怎么对其子View进行摆放的呢?别急,在ViewGroup中同样也有onLayout方法,源码如下;

    1 /**
    2      * {@inheritDoc}
    3      */
    4     @Override
    5     protected abstract void onLayout(boolean changed,
    6             int l, int t, int r, int b);

    看到了吧,还是个抽象方法,因为具体ViewGroup摆放规则不同,所以其具体子类需要重写这个方法来实现对其子View的摆放逻辑。

    既然这样我们就只能分析一个继承自ViewGroup的具体子类了,我们选取FrameLayout,其onLayout源码如下:

     1     @Override
     2     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
     3         layoutChildren(left, top, right, bottom, false /* no force left gravity */);
     4     }
     5 
     6     void layoutChildren(int left, int top, int right, int bottom,
     7                                   boolean forceLeftGravity) {
     8         final int count = getChildCount();
     9 
    10         ......
    11 
    12         for (int i = 0; i < count; i++) {
    13             final View child = getChildAt(i);
    14             if (child.getVisibility() != GONE) {
    15                .....
    16 
    17                 child.layout(childLeft, childTop, childLeft + width, childTop + height);
    18             }
    19         }
    20     }

    看到了吧,onLayout方法调用layoutChildren方法,在layoutChildren方法中遍历每个子View调用其layout方法。

    好了,到此Layout过程就讨论的差不多了,相比measure过程还是简单不少的,其也是递归调用逻辑。如图:

    我们总结一下主要部分:

    • View.layout方法可被重写,ViewGroup.layout为final的不可重写,ViewGroup.onLayout为abstract的,具体ViewGroup子类必须重载来按照自己规则对子View进行摆放。

    • measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。

    • 使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。

    测量,摆放过程都分析完了,接下来我们分析View的draw过程。

    四,View绘制流程第三步draw过程分析(API23)

    performMeasure, performLayout过程执行完,接下来就执行performDraw()逻辑了,ViewGroup没有重写View的draw方法,最终调用的是View中的draw方法,源码如下:

     1    public void draw(Canvas canvas) {
            final int privateFlags = mPrivateFlags;
    2   final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
         (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); 3 4 /* 5 * Draw traversal performs several drawing steps which must be executed 6 * in the appropriate order: 7 * 8 * 1. Draw the background 9 * 2. If necessary, save the canvas' layers to prepare for fading 10 * 3. Draw view's content 11 * 4. Draw children 12 * 5. If necessary, draw the fading edges and restore layers 13 * 6. Draw decorations (scrollbars for instance) 14 */ 15 16 // Step 1, draw the background, if needed 17 int saveCount; 18 19 if (!dirtyOpaque) { 20 drawBackground(canvas); 21 } 22 23 // skip step 2 & 5 if possible (common case) 24 final int viewFlags = mViewFlags; 25 boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; 26 boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; 27 if (!verticalEdges && !horizontalEdges) { 28 // Step 3, draw the content 29 if (!dirtyOpaque) onDraw(canvas); 30 31 // Step 4, draw the children 32 dispatchDraw(canvas); 33 34 // Overlay is part of the content and draws beneath Foreground 35 if (mOverlay != null && !mOverlay.isEmpty()) { 36 mOverlay.getOverlayView().dispatchDraw(canvas); 37 } 38 39 // Step 6, draw decorations (foreground, scrollbars) 40 onDrawForeground(canvas); 41 42 // we're done... 43 return; 44 } 45 ... 46 // Step 2, save the canvas' layers 47 .... 48 // Step 3, draw the content 49 if (!dirtyOpaque) onDraw(canvas); 50 51 // Step 4, draw the children 52 dispatchDraw(canvas); 53 54 // Step 5, draw the fade effect and restore layers 55 .... 56 // Step 6, draw decorations (foreground, scrollbars) 57 onDrawForeground(canvas); 58 }

    5到14行注释可以看到draw过程分为6步,再看23行提示大部分情况下跳过第2,5步。所以我们着重分析其余4步。

    19-21行执行第一步,绘制背景源码如下:

     1 private void drawBackground(Canvas canvas) {
     2         final Drawable background = mBackground;
     3         if (background == null) {
     4             return;
     5         }
     6      ....
     7         setBackgroundBounds();
     8         ....
     9         background.draw(canvas);
    10        
    11     }
    12 
    13 
    14     void setBackgroundBounds() {
    15         if (mBackgroundSizeChanged && mBackground != null) {
    16             mBackground.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
    17             mBackgroundSizeChanged = false;
    18             rebuildOutline();
    19         }
    20     }

    只要逻辑就是获取我们在xml文件或者代码中设置的背景,然后根据layout过程摆放的位置绘制出来。

    第29行执行绘制内容逻辑,源码如下:

    1     /**
    2      * Implement this to do your drawing.
    3      *
    4      * @param canvas the canvas on which the background will be drawn
    5      */
    6     protected void onDraw(Canvas canvas) {
    7     }

    看到了吧,是一个空方法,需要具体子类自己去实现,因为每个具体View要绘制的内容是不同的,所以子类需要实现这个方法来绘制自身的内容。

    第32行执行绘制子View逻辑,源码如下:

    1 /**
    2      * Called by draw to draw the child views. This may be overridden
    3      * by derived classes to gain control just before its children are drawn
    4      * (but after its own view has been drawn).
    5      * @param canvas the canvas on which to draw the view
    6      */
    7     protected void dispatchDraw(Canvas canvas) {
    8 
    9     }

    看到了吧,也是空方法,这个方法被用来绘制子View的,如果有子View则需要调用这个方法去绘制,我们知道一般只有容器类View才可以盛放子View,所以我们看下ViewGroup中有没有相关逻辑,在
    ViewGroup中果然实现了这个方法,源码如下:

     1  @Override
     2     protected void dispatchDraw(Canvas canvas) {
     3         boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
     4         final int childrenCount = mChildrenCount;
     5         final View[] children = mChildren;
     6         .......
     7         for (int i = 0; i < childrenCount; i++) {
     8             while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
     9                 final View transientChild = mTransientViews.get(transientIndex);
    10                 if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
    11                         transientChild.getAnimation() != null) {
    12                     more |= drawChild(canvas, transientChild, drawingTime);
    13                 }
    14                 .......
    15             }
    16           ......
    17         }
    18         ......   
    19     }

    在dispatchDraw方法中遍历每个子View并且调用drawChild方法,接下来我们看下drawChild源码:

    1 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    2         return child.draw(canvas, this, drawingTime);
    3 }

    看到了吧,最终调用每个子View的draw方法来完成自身的绘制。

    接下来40行执行onDrawForeground逻辑,这一部分只要绘制一些装饰物,比如ScrollBar。这部分就不分析了,也不是重点。

    到这里,View的主要绘制流程我们也分析完了,也不复杂。

    但是,但是!!!!!!细心的你有没有发现在执行第一步,第三步的时候都有个if判断(if (!dirtyOpaque)),也就是说只有判断成立才会执行绘制背景和自身内容,难道还有View不绘制自身内容吗? 这里就直接说了,ViewGroup子类默认情况下就是不执行onDraw方法的,在ViewGroup源码中的initViewGroup()方法中设置了一个标记,源码如下:

    1 private void initViewGroup() {
    2         // ViewGroup doesn't draw by default
    3         if (!debugDraw()) {
    4             setFlags(WILL_NOT_DRAW, DRAW_MASK);
    5         }
    6         ......
    7 }    

    看第二行注释也知道,ViewGroup默认情况下是不会draw的。

    第四行调用setFlags方法设置标记WILL_NOT_DRAW,

    我们在回到View中draw方法看第2行代码:

    1  final int privateFlags = mPrivateFlags;
    2 final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && 3 (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);

    setFlags方法就是对View中mPrivateFlags值进行相应改变,我们设置标记WILL_NOT_DRAW那么dirtyOpaque得到的值就为true,从而if (!dirtyOpaque)不成立,也就不会执行onDraw方法。

    估计这点很多同学有疑问ViewGroup默认情况下onDraw方法是不执行的???别急,动手写个小demo验证一下就是了。

    布局如下:极其简单

     1 <com.wanglei.clearheart.MyView xmlns:android="http://schemas.android.com/apk/res/android"
     2     xmlns:tools="http://schemas.android.com/tools"
     3     android:layout_width="match_parent"
     4     android:layout_height="match_parent"
     5     android:gravity="center"
     6     android:orientation="vertical"
     7     tools:context=".MainActivity" >
     8 
     9 
    10 </com.wanglei.clearheart.MyView >

    MyView源码如下:同样极其简单

     1 public class MyView extends ViewGroup {
     2 
     3     private Paint mPaint;
     4     public MyView(Context context, AttributeSet attrs) {
     5         super(context, attrs);
     6         mPaint = new Paint();
     7         mPaint.setColor(Color.RED);
     8         mPaint.setStyle(Style.STROKE);
     9         mPaint.setStrokeWidth(10); 
    10         
    11     }
    12     
    13     @Override
    14     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    15         
    16         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    17     }
    18     
    19     @Override
    20     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    21         // TODO Auto-generated method stub
    22         
    23     }
    24     
    25     @Override
    26     protected void onDraw(Canvas canvas) {
    27         // TODO Auto-generated method stub
    28         canvas.drawCircle(getMeasuredWidth()/2, getMeasuredHeight()/2, 360, mPaint);
    29     }
    30 }

    运行程序会看到就是一个大白板,没有绘制出任何图形,那我们怎么让ViewGroup调用onDraw方法呢?

    很简单View类中给我们提供了一个方法供外部调用:

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

    看到了吧,本质也是调用的setFlags方法。如果我们传入true则绘制的时候不会调用onDraw方法,传入false则使其调用onDraw方法。

    我们修改MyView代码:构造方法中调用setWillNotDraw(false);

    1 public MyView(Context context, AttributeSet attrs) {
    2         super(context, attrs);
    3         mPaint = new Paint();
    4         mPaint.setColor(Color.RED);
    5         mPaint.setStyle(Style.STROKE);
    6         mPaint.setStrokeWidth(10); 
    7         setWillNotDraw(false);
    8         
    9 }

    运行程序,会看到手机屏幕中间画出一个红色的圆。还有一种方法我们在布局中给MyView添加背景同样会达到调用onDraw方法的目的。

    容器类布局(ViewGroup子类)为什么默认情况下不绘制背景和自身内容呢?答案是为了性能啊,大家想想容器类布局如果没有背景,只是用来盛放子类有必要调用onDraw方法吗?

    有什么可绘制的吗?子类会自己实现onDraw方法绘制自己内容的。

    接下来我们总结一下draw流程的重点:

    • 容器类布局需要递归绘制其所包含的所有子View。

    • View中onDraw默认是空方法,需要子类自己实现来完成自身容内的绘制。

    • 容器类布局默认情况下不会调用onDraw方法,我们可以为其设置背景或者调用setWillNotDraw(false)方法来使其主动调用onDraw方法

    最后附上draw流程图:

     

    好了,到此本篇就该结束了,用了很长的篇幅来探讨View的绘制流程,希望对大家有用,废话就不多说了,咱们下篇见。

  • 相关阅读:
    委托操作控件使用01
    C#读写ini
    List集合的使用小技巧|非常实用首先举例2个集合A,B. List<i
    xiaoqiang
    c#遍历目录及子目录下某类11型的所有的文件
    2015实习生面试记录
    Sliding Window Maximum
    Construct Binary Tree from Preorder and Inorder Traversal
    Binary Search Tree Iterator
    Populating Next Right Pointers in Each Node
  • 原文地址:https://www.cnblogs.com/leipDao/p/7573803.html
Copyright © 2011-2022 走看看