Android View的绘制流程

1. 视图坐标系

1.1 Android坐标系

android坐标系

1.2 视图坐标系

视图坐标系

2. View的绘制流程前情提要

2.1 View 树的绘图流程

当 Activity 接收到焦点的时候,它会被请求绘制布局,该请求由 Android framework 处理.绘制是从根节点开始,对布局树进行 measure 和 draw。整个 View 树的绘图流程在ViewRoot.java类的performTraversals()函数展开,该函数所做 的工作可简单概况为是否需要重新计算视图大小(measure)、是否需要重新安置视图的位置(layout)、以及是否需要重绘(draw),流程图如下:
View树的绘制流程
在此之前
DecorView添加到窗口window的过程
详见https://blog.csdn.net/jacklam200/article/details/50039189

2.2 View绘制流程函数调用链

函数调用链
可以说是: (测量)大小 -->(安排)位置 --> (绘制)内容

2.3 绘制顺序

树的遍历

  • DecorView是View树的根
  • 树的遍历是有序的,由父视图到子视图,每个ViewGroup负责绘制他所有的子视图,最底层的View负责绘制自身。
    或者说,measure流程分为View的measure流程和ViewGroup的measure流程,只不过ViewGroup的measure流程除了要完成自己的测量还要遍历去调用子元素的measure()方法

2.4 measure过程传递尺寸的两个类

  • ViewGroup.LayoutParams(View自身的布局参数)
  • MeasureSpecs类(父视图对自视图的测量要求)

2.4.1 ViewGroup.LayoutParams

用来指定视图的宽度和高度等参数,对于height和width有以下选择:

  • 具体值
  • MATCH_PARENT 子视图希望和父视图一样大(不包含padding值)
  • WRAP_CONTENT 视图正好能包裹内容大小(包含padding值)
    ViewGroup的子类有其对应的ViewGroup.LayoutParams的子类。
    比如RelativeLayout拥有的ViewGroup.LayoutParams的子类RelativeLayoutParams.
    注意:我们使用view.getLayoutParams()方法获取的是其所在父视图类型的LayoutParams,比如View的父控件为RelativeLayout,那么得到的LayoutParams类型就是RelativeLayoutParams。

2.4.2 MeasureSpecs […speks]

测量规格,包含测量要求和尺寸信息,有三个模式:

  • UNSPECIFIED
    父视图不对子视图有任何约束,它可以达到所期望的任意尺寸。比如 ListView、ScrollView,一般自定义 View 中用不到,

  • EXACTLY
    父视图为子视图指定一个确切的尺寸,而且无论子视图期望多大,它都必须在该指定大小的边界内,对应的属性为 match_parent 或具体值,比如 100dp,父控件可以通过MeasureSpec.getSize(measureSpec)直接得到子控件的尺寸。

  • AT_MOST
    父视图为子视图指定一个最大尺寸。子视图必须确保它自己所有子视图可以适应在该尺寸范围内,对应的属性为 wrap_content,这种模式下,父控件无法确定子 View 的尺寸,只能由子控件自己根据需求去计算自己的尺寸,这种模式就是我们自定义视图需要实现测量逻辑的情况。

3. View的绘制流程

3.1 View的measure流程:

一个View的真正测量工作在onMeasure(int,int)中,由measure()方法调用。
而measure()方法为final所以只有onMeasure(int,int)可以而且必须被子类复写。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
      ...
      onMeasure(widthMeasureSpec, heightMeasureSpec);
      ...
    }

首先看一下onMeasure()方法(View.java):

   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

往里看setMeasuredDimension()方法:

 protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

我们看到measuredWidthmeasuredHeight 显然它是用来设置View的宽高的。
我们再来看看getDefaultSize()方法处理了什么:

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;
    }

specMode是View的测量模式,而specSize是View的测量大小,我们有必要查看一下MeasureSpec类:

 public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

...省略

 public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }
  public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
...省略        
}

MeasureSpec类帮助我们来测量View

我们回头再看getDefaultSize()方法,显然在AT_MOST和EXACTLY模式下,都返回specSize这个值,也就是View测量后的大小,而在UNSPECIFIED模式(未指定模式)返回的是getDefaultSize()方法的第一个参数的值,这第一个参数从onMeasure()方法来看是getSuggestedMinimumWidth()方法和getSuggestedMinimumHeight()得到的,看一下这两个方法:(类似的只放Width)

 protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

如果View没有设置背景(MBackground==null)则取值为mMinWidth,mMinWidth是可以设置的,它对应于android:minWidth这个属性设置的值或者View的setMinimumWidth的值,若不指定则默认0:

  public void setMinimumWidth(int minWidth) {
        mMinWidth = minWidth;
        requestLayout();

    }

如果View设置了背景 在取值为max(mMinWidth,mBackground.getMinimumWidth()),取值mMinWidth和mBackground.getMinimumWidth()两者中的最大值,mMinWidth上面已经看过,我们接下来看看mBackground.getMinimumWidth(),这个mBackground是Drawable类型的,看一下Drawable类的getMInimumWidth()方法(Drawable.java):

 public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }

intrinsicWidth得到的是这个Drawable的固有宽度,如果其大于0则返回固有宽度否则返回0。
我们可以看到,getSuggestedMinimumWidth()方法:如果View没有设置背景则返回mMinWidth,如果设置了背景就返回mMinWidth和Drawable最小宽度两个值的最大值。

3.1.1 ViewGroup的measure流程

我们知道对于ViewGroup,它除了需要measure自己本身,还要遍历调用子视图的measure()方法
ViewGroup_measure

ViewGroup中没有定义onMeasure()方法,然而它定义了measureChildren()方法(ViewGroup.java):

 protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

显然它是一个遍历children的方法,看一下调用的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);
    }

调用child.getLayoutParams()方法来获得子元素的LayoutParams属性,并获取到子元素的MeasureSpec并调用其measure()方法进行测量。
看一下getChildMeasureSpec()方法:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
...省略
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

这是根据父容器的MeasureSpec的模式,结合子元素的LayoutParams属性得出自容器的MeasureSpec属性。

3.1.2 LinearLayout的measure流程

ViewGroup没有提供onMeasure方法,而是让其子类来个字实现测量的方法,我们看一下ViewGroup的子类LinearLayout的measure流程,看一下onMeasure()方法(LinearLayout.java):

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

复写了View的onMeasure()方法。
看一下measureVertical()方法部分源码:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        mTotalLength = 0;
     mTotalLength = 0;       
 ...省略
  for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);

            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }

            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i);
               continue;
            }

            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerHeight;
            }

            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

            totalWeight += lp.weight;

             final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                // Optimization: don't bother measuring children who are only
                // laid out using excess space. These views will get measured
                // later if we have space to distribute.
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                if (useExcessSpace) {
                    // The heightMode is either UNSPECIFIED or AT_MOST, and
                    // this child is only laid out using excess space. Measure
                    // using WRAP_CONTENT so that we can find out the view's
                    // optimal height. We'll restore the original height of 0
                    // after measurement.
                    lp.height = LayoutParams.WRAP_CONTENT;
                }
                // Determine how big this child would like to be. If this or
                // previous children have given a weight, then we allow it to
                // use all available space (and we will shrink things later
                // if needed).
               final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

                final int childHeight = child.getMeasuredHeight();
                if (useExcessSpace) {
                    // Restore the original height and record how much space
                    // we've allocated to excess-only children so that we can
                    // match the behavior of EXACTLY measurement.
                    lp.height = 0;
                    consumedExcessSpace += childHeight;
                }

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));
...省略

        if (useLargestChild &&
                (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);

                if (child == null) {
                    mTotalLength += measureNullChild(i);
                    continue;
                }

                if (child.getVisibility() == GONE) {
                    i += getChildrenSkipCount(child, i);
                    continue;
                }

                final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
                        child.getLayoutParams();
                // Account for negative margins
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }
        }

        // Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;

        int heightSize = mTotalLength;

        // Check against our minimum height

定义了mTotalLength用来储存LinearLayout在垂直方向的高度,然后遍历子元素,根据子元素的MeasureSpec模式分别计算每个子元素的高度,如果是wrap_content则将每个子元素的高度和margin垂直高度等值相加 并且赋值给mTotalLength得出LinearLayout的高度。如果布局高度设置为match_parent,具体数值则和view的测量方法一样。

3.3 View的layout流程

先来看一下View的layout()方法:

public void layout(int l, int t, int r, int b) {
...省略
 boolean changed = isLayoutModeOptical(mParent) ?
         setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
 if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
...省略          
    }

传进来的分别是View的四个点的坐标,注意其是对于父布局来说的。
看看setFrame()方法:

protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (DBG) {
            Log.d("View", this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }
        
        //在setFrame()方法里主要是用来设置View的四个顶点的值,
        //即mLeft、mTop、mRight和mBottom

        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            invalidate(sizeChanged);

            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

        ...省略  
        }
        return changed;
    }

在setFrame()方法里主要是用来设置View的四个顶点的值,即mLeft、mTop、mRight和mBottom。调用setFrame()方法后调用onLayout()方法:

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

和onMeasure()方法类似,确定位置时候,不同控件有不同的实现,所以需要子类重写这个方法。我们看一下LinearLayout的onLayout()方法:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

看一看layoutVertical()方法:

 void layoutVertical(int left, int top, int right, int bottom) {
     ...省略
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
           ...
                setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
                
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }

该方法会遍历子元素,调用setChildFrame()方法:

private void setChildFrame(View child, int left, int top, int width, int height) {        
        child.layout(left, top, left + width, top + height);
    }

该方法中调用子元素的layout()方法来确定自己的位置。我们看到childTop这个值逐渐增大,这是为了在垂直方向,子元素一个接一个安排而不重叠。(setChildFrame()传入的第三个参数为childTop + getLocationOffset(child));

3.4 View的draw流程

绘制流程图
先来看下与 draw 过程相关的函数:

  • View.draw(Canvas canvas):
    由于 ViewGroup 并没有复写此方法,因此,所有的视图最终都是调用 View 的 draw 方法进行绘制的。在自定义的视图中,也不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制,如果自定义的视图确实要复写该方法,那么请先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制。

  • View.onDraw():
    View 的onDraw(Canvas)默认是空实现,自定义绘制过程需要复写的方法,绘制自身的内容。

  • dispatchDraw()
    发起对子视图的绘制。View 中默认是空实现,ViewGroup 复写了dispatchDraw()来对其子视图进行绘制。该方法我们不用去管,自定义的 ViewGroup 不应该对dispatchDraw()进行复写。

我们先来看一下ViewGroup的dispatchDraw的源码

protected void dispatchDraw(Canvas canvas){

...省略

 if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {//处理 ChildView 的动画
     final boolean buildCache = !isHardwareAccelerated();
            for (int i = 0; i < childrenCount; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {//只绘制 Visible 状态的布局,因此可以通过延时加载来提高效率
                    final LayoutParams params = child.getLayoutParams();
                    attachLayoutAnimationParameters(child, params, i, childrenCount);// 添加布局变化的动画
                    bindLayoutAnimation(child);//为 Child 绑定动画
                    if (cache) {
                        child.setDrawingCacheEnabled(true);
                        if (buildCache) {
                            child.buildDrawingCache(true);
                        }
                    }
                }
            }

     final LayoutAnimationController controller = mLayoutAnimationController;
            if (controller.willOverlap()) {
                mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
            }

    controller.start();// 启动 View 的动画
}

 // 绘制 ChildView
 for (int i = 0; i < childrenCount; i++) {
            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);//在drawChild中调用view.draw
            }
        }

...
if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
            invalidate(true);
        }

}

  • drawChild(canvas, this, drawingTime)
    直接调用了 View 的child.draw(canvas, this,drawingTime)方法,文档中也说明了,除了被ViewGroup.drawChild()方法外,你不应该在其它任何地方去复写或调用该方法,它属于 ViewGroup。而View.draw(Canvas)方法是我们自定义控件中可以复写的方法,具体可以参考上述对view.draw(Canvas)的说明。从参数中可以看到,child.draw(canvas, this, drawingTime) 肯定是处理了和父视图相关的逻辑,但 View 的最终绘制,还是 View.draw(Canvas)方法。

  • invalidate()
    请求重绘 View 树,即 draw 过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些调用了invalidate()方法的 View。

dispatchDraw()–>drawChild()–>view.draw()

注意:view.draw()中调用的onDraw(Canvas)默认是空实现,自定义绘制过程需要复写的方法,绘制自身的内容。

我们再来看一下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
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
...省略
   // Step 2, save the canvas' layers
        int paddingLeft = mPaddingLeft;

        final boolean offsetRequired = isPaddingOffsetRequired();
        if (offsetRequired) {
            paddingLeft += getLeftPaddingOffset();
        }
...省略
  // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);	//onDraw空实现

        // Step 4, draw the children
        dispatchDraw(canvas);
...省略
   // Step 5, draw the fade effect and restore layers
        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        final Shader fade = scrollabilityCache.shader;
...省略
  // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);

        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }
   }

从源码的注释我们看到draw流程有六个步骤,其中第2步和第5步可以跳过:
1.若有背景则绘制
2.保存canvas层
3.绘制自身内容
4.如果有子元素则绘制子元素
5.绘制效果
6.绘制装饰品(scrollbars)

注意:由上面的处理过程,我们也可以得出一些优化的小技巧:当不需要绘制 Layer 的时候第二步和第五步会跳过。因此在绘制的时候,能省的 layer 尽可省,可以提高绘制效率

引用:
https://blog.csdn.net/itachi85/article/details/50708391
http://developer.android.com/guide/topics/ui/how-android-draws.html
http://blog.csdn.net/wangjinyu501/article/details/9008271
http://blog.csdn.net/qinjuning/article/details/7110211
http://blog.csdn.net/qinjuning/article/details/8074262

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值