View 的工作原理

目的

介绍 View 的工作原理,为了更好地自定义 View(这才是学习的重点),需要掌握 View 的底层工作原理


(一)初识 ViewRoot 和 DecorView

在正式学习 View 的三大流程之前,我们先了解一下 ViewRootDecorView 的概念

(1)ViewRoot

ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManagerDecorView 的纽带,View 的三大流程(测量(measure),布局(layout),绘制(draw))均通过ViewRoot 来完成。View 的绘制流程是从 ViewRootPerformTraversals 方法开始的,这个方法会依次调用 performMeasureperformLayoutperformDraw 三个方法,这个三个方法分别完成顶级 Viewmeasurelayoutdraw 这三大流程,如此反复就完成了整个 View 树的遍历

PerformTraversals 的工作流程图
(2)DecorView
  • Activity

Activity 并不负责视图控制,它只是控制生命周期和处理事件。真正控制视图的是Window。一个 Activity 包含了一个 WindowWindow 才是真正代表一个窗口。Activity 就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来与 Window、以及 View 进行交互。

  • Window

Window是视图的承载器,内部持有一个 DecorView,而这个 DecorView 才是 view 的根布局。Window 是一个抽象类,实际在 Activity 中持有的是其子类 PhoneWindowPhoneWindow 中有个内部类 DecorView,通过创建 DecorView 来加载 Activity 中设置的布局 R.layout.activity_mainWindow 通过 WindowManagerDecorView 加载其中,并将 DecorView 交给 ViewRoot,进行视图绘制以及其他交互。

  • DecorView

作为顶级 View,一般情况下它内部会包含一个竖直方向的 LinearLayout,在这个 LinearLayout 里面有上下两部分,上面是标题栏,下面是内容栏。在 Acitivity 中我们通过 setContentView 所设置的布局文件其实就是被加到内容栏之中的,而内容栏的 idcontent。那么如何得到我们设置的 View 呢?代码如下:

  ViewGroup content = findViewById(android.R.id.content);
  View view = content.getChildAt(0);

DecorView 其实是一个 FrameLayoutView 层的事件都先经过 DecorView,然后才传递给我们的 View

顶级 View:DecorView 的结构图

(三)理解 MeasureSpec

MeasureSpec 由两部分组成,一部分是测量模式,另一部分是测量的尺寸大小。

MeasureSpec 的组成

其中,Mode模式共分为三类

  • UNSPECIFIED :不对View进行任何限制,要多大给多大,一般用于系统内部
  • EXACTLY:对应LayoutParams中的match_parent和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,
  • AT_MOST :对应LayoutParams中的wrap_content。View的大小不能大于父容器的大小。

(二)View 的工作流程

砰砰砰,敲黑板了!

View 的工作流程主要是指 measurelayoutdraw 这三大流程,即测量、布局和绘制,其中 measure 确定 View 的测量宽/高,layout 确定 View 的最终宽/高和四个顶点的位置,而 draw 则将 View 绘制到屏幕上

(1)measure 过程

measure 过程,如果只是一个 View,那么通过 measure 方法就完成了测量过程,如果是一个 ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个流程

1. View 的 measure 过程

首先来看一下 ViewonMeasure 方法:

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

这很显然是用来设置 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;
    }

SpecModeView 的测量模式,而 SpecSizeView 的测量大小,上一节我们了解了 MeasureSpec。这里很显然根据不同的 SpecMode 值来返回不同的 result 值,也就是 SpecSize。在 AT_MOSTEXACTLY 模式下,都返回 SpecSize 这个值,即 View 在这两种模式下的测量宽和高直接取决于 SpecSize。也就是说,对于一个直接继承自 View 的自定义 View 来说,它的 wrap_contentmatch_parent 属性的效果是一样的。因此如果要实现自定义 Viewwrap_content,则要重写 onMeasure 方法,并对自定义 Viewwarp_content 属性进行处理。而在 UNSPECIFIED 模式下返回的是 getDefaultSize 方法的第一个参数 size 的值,size 的值从 onMeasure 方法来看是 getSuggestedMinimumWidth 方法或者 getSuggestedMinimumHeight 方法得到的。

我们来查看 getSuggestedMinimumWidth 方法做了什么:

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

如果 View 没有设置背景,则取值为 mMinWidthmMinWidth 是可以设置的,它对应于 Android:minWidth 这个属性设置的值或者 ViewsetMinimumWidth 的值;如果不指定的话,则默认为 0。setMinimumWidth 方法如下所示:

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

如果 View 设置了背景,则取值为 max(mMinWidth, mBackground.getMinimumWidth()),也就是取 mMinWidthmBackground.getMinimumWidth() 之间的最大值

下面看看 mBackground.getMinimumWidth(),这个 mBackgroundDrawable 类的 getMinimumWidth() 方法如下所示:

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

intrinsicWidth 得到的是这个 Drawable 的固有宽度,如果固有宽度大于 0 则返回固有宽度,否则返回 0

总结一下:getSuggestedMinimumWidth 方法就是:如果 View 没有设置背景,则返回 mMinWidth;如果设置了背景,就返回 mMinWidthDrawable 的最小宽度之间的最大值


2. ViewGroup 的 measure 流程

ViewGroup 中没有定义 onMeasure() 方法,但却定义了 measureChildren() 方法:

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

遍历子元素并调用 measureChild 方法,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) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = 0;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = 0;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

很显然这是根据父容器的 MeasureSpec 的模式再结合子元素的 LayoutParams 属性来得出子元素的 MeasureSpec 属性,有一点需要注意的是如果父容器的 MeasureSpec 属性为 AT_MOST,子元素的 LayoutParams 属性为 WRAP_CONTENT,那根据代码我们会发现子元素的 MeasureSpec 属性也为 AT_MOST,它的 specSize 值为父容器的 specSize 减去 padding 的值,也就是说跟这个子元素设置 LayoutParams 属性为 MATCH_PARENT 效果是一样的,为了解决这个问题需要在 LayoutParams 属性为 WRAP_CONTENT 时指定一下默认的宽和高。

3. LinearLayout 的 measure 流程

ViewGroup 并没有提供 onMeasure() 方法,而是让其子类来各自实现测量的方法,究其原因就是 ViewGroup 有不同的布局的需要很难统一,接下来我们来简单分析一下 ViewGroup 的子类 LinearLayoutmeasure 流程,先来看看它的 onMeasure() 方法 (LinearLayout.java)

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

来看看垂直 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;
            
            if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
                // Optimization: don't bother measuring children who are going to use
                // leftover space. These views will get measured again down below if
                // there is any leftover space.
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                int oldHeight = Integer.MIN_VALUE;

                if (lp.height == 0 && lp.weight > 0) {
                    // heightMode is either UNSPECIFIED or AT_MOST, and this
                    // child wanted to stretch to fill available space.
                    // Translate that to WRAP_CONTENT so that it does not end up
                    // with a height of 0
                    oldHeight = 0;
                    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).
                measureChildBeforeLayout(
                       child, i, widthMeasureSpec, 0, heightMeasureSpec,
                       totalWeight == 0 ? mTotalLength : 0);

                if (oldHeight != Integer.MIN_VALUE) {
                   lp.height = oldHeight;
                }

                final int childHeight = child.getMeasuredHeight();
                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 的测量方法一样


(2)View 的 layout 流程

先来看看 Viewlayout() 方法:

public void layout(int l, int t, int r, int b) {
       if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
           onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
           mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
       }
       int oldL = mLeft;
       int oldT = mTop;
       int oldB = mBottom;
       int oldR = mRight;
       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);
           mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
           ListenerInfo li = mListenerInfo;
           if (li != null && li.mOnLayoutChangeListeners != null) {
               ArrayList listenersCopy =
                       (ArrayList)li.mOnLayoutChangeListeners.clone();
               int numListeners = listenersCopy.size();
               for (int i = 0; i < numListeners; ++i) {
                   listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
               }
           }
       }
       mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
       mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
   }

传进来里面的四个参数分别是 View 的四个点的坐标,它的坐标不是相对屏幕的原点,而且相对于它的父布局来说的。 lt 是子控件左边缘和上边缘相对于父类控件左边缘和上边缘的距离;rb 是子控件右边缘和下边缘相对于父类控件左边缘和上边缘的距离。来看看 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 + ")");
    }
    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 的四个顶点的值,也就是 mLeftmTopmRightmBottom 的值。在调用 setFrame() 方法后,调用 onLayout() 方法:

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

onLayout() 方法没有去做什么,这个和 onMeasure() 方法类似,确定位置时根据不同的控件有不同的实现,所以在 ViewViewGroup 中均没有实现 onLayout() 方法。既然这样,我们就来看看 LinearLayoutonLayout() 方法:

@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) {
       final int paddingLeft = mPaddingLeft;
       int childTop;
       int childLeft;       
       // Where right end of child should go
       final int width = right - left;
       int childRight = width - mPaddingRight;     
       // Space available for child
       int childSpace = width - paddingLeft - mPaddingRight;    
       final int count = getVirtualChildCount();
       final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
       final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
       switch (majorGravity) {
          case Gravity.BOTTOM:
              // mTotalLength contains the padding already
              childTop = mPaddingTop + bottom - top - mTotalLength;
              break;
              // mTotalLength contains the padding already
          case Gravity.CENTER_VERTICAL:
              childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
              break;
          case Gravity.TOP:
          default:
              childTop = mPaddingTop;
              break;
       }
       for (int i = 0; i < count; i++) {
           final View child = getVirtualChildAt(i);
           if (child == null) {
               childTop += measureNullChild(i);
           } else if (child.getVisibility() != GONE) {
               final int childWidth = child.getMeasuredWidth();
               final int childHeight = child.getMeasuredHeight();            
               final LinearLayout.LayoutParams lp =
                       (LinearLayout.LayoutParams) child.getLayoutParams();           
               int gravity = lp.gravity;
               if (gravity < 0) {
                   gravity = minorGravity;
               }
               final int layoutDirection = getLayoutDirection();
               final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
               switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                   case Gravity.CENTER_HORIZONTAL:
                       childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                               + lp.leftMargin - lp.rightMargin;
                       break;
                   case Gravity.RIGHT:
                       childLeft = childRight - childWidth - lp.rightMargin;
                       break;
                   case Gravity.LEFT:
                   default:
                       childLeft = paddingLeft + lp.leftMargin;
                       break;
               }
               if (hasDividerBeforeChildAt(i)) {
                   childTop += mDividerHeight;
               }
               childTop += lp.topMargin;
               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);
  }

setChildFrame() 方法中调用子元素的 layout() 方法来确定自己的位置。我们看到 childTop 这个值是逐渐增大的,这是为了在垂直方向,子元素是一个接一个排列的而不是重叠的。


(3)View的draw流程

Viewdraw 流程很简单,先来看看 Viewdraw() 方法:

 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;   
        // 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);
        // 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)

你可能感兴趣的:(View 的工作原理)