参考资料 << Android 开发艺术探索 >> 欢迎访问我的个人博客 传送门

前言

在日常开发中,我们每天都在和各种 View 打交道,比如TextView,Button等,我们直接拿过来就可以使用,那么 Android 是怎么把 View 绘制到屏幕上呢,接下来我们结合源码来具体分析。

在具体结合源码分析前,先了解一个比较重要的概念 ViewRoot

ViewRoot

先看一张图 Android 窗口构成图解

ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 根布局 DecorView(看上图) 的纽带, View 的三大流程均是通过 ViewRoot 来完成的。在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联。

View 的绘制流程是从 ViewRoot 的 performTraversals 方法开始的,它经过 measure、layout 和 draw 三个过程才能最终将一个 View 绘制出来,其中 measure 用来测量 View 的宽和高,layout 用来确定 View 在父容器中的放置位置,而 draw 则负责将 View 绘制在屏幕上。针对 performTraversals的大致流程如下:

performTraversals 会依次调用 performMeasure、performLayout 和 performDraw 三个方法,这三个方法分别完成顶级 View 的 measure、layout 和 draw 这三大流程,其中在 performMeasure 中会调用 measure 方法,在 measure 方法中又会调用 onMeasure 方法,在 onMeasure 方法中则会对所有的子元素进行 measure 过程,这个时候 measure 流程就从父容器传递到子元素中了,这样就完成了一次 measure 过程。接着子元素会重复父容器的 measure 过程,如此反复就完成了整个 View 树的遍历。同理,performLayout 和 performDraw 的传递流程和 performMeasure 是类似的,唯一不同的是,performDraw 的传递过程是在 draw 方法中通过 dispatchDraw 来实现的,不过这并没有本质区别。

接下来结合源码来分析这三个过程。

Measure 测量过程

这里分两种情况,View 的测量过程和 ViewGroup 的测量过程。

View 的测量过程

View 的 测量过程由其 measure 方法来完成,源码如下:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) { //省略代码... if (forceLayout || needsLayout) { // first clears the measured dimension flag mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } else { long value = mMeasureCache.valueAt(cacheIndex); // Casting a long to int drops the high 32 bits, no mask needed setMeasuredDimensionRaw((int) (value >> 32), (int) value); mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } // flag not set, setMeasuredDimension() was not invoked, we raise // an exception to warn the developer if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { throw new IllegalStateException("View with id " + getId() + ": " + getClass().getName() + "#onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension } 

可以看到 measure 方法是一个 final 类型的方法,这意味着子类不能重写此方法。

在 13 行 measure 中会调用 onMeasure 方法,这个方法是测量的主要方法,继续看 onMeasure 的实现

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

setMeasuredDimension 方法的作用是设置 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; } 

可以看到要得到测量的尺寸需要用到 MeasureSpec,MeasureSpec 是什么鬼呢,敲黑板了,重点来了。 MeasureSpec 决定了 View 的测量过程。确切来说,MeasureSpec 在很大程度上决定了一个 View 的尺寸规格。 来看 MeasureSpec 类的实现

public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; /** @hide */ @IntDef({UNSPECIFIED, EXACTLY, AT_MOST}) @Retention(RetentionPolicy.SOURCE) public @interface MeasureSpecMode {} public static final int UNSPECIFIED = 0 << MODE_SHIFT; public static final int EXACTLY = 1 << MODE_SHIFT; public static final int AT_MOST = 2 << MODE_SHIFT; public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } public static int makeSafeMeasureSpec(int size, int mode) { if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) { return 0; } return makeMeasureSpec(size, mode); } @MeasureSpecMode public static int getMode(int measureSpec) { //noinspection ResourceType return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } static int adjust(int measureSpec, int delta) { final int mode = getMode(measureSpec); int size = getSize(measureSpec); if (mode == UNSPECIFIED) { // No need to adjust size for UNSPECIFIED mode. return makeMeasureSpec(size, UNSPECIFIED); } size += delta; if (size < 0) { Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size + ") spec: " + toString(measureSpec) + " delta: " + delta); size = 0; } return makeMeasureSpec(size, mode); } public static String toString(int measureSpec) { //省略... } } 

可以看出 MeasureSpec 中有两个主要的值,SpecMode 和 SpecSize, SpecMode 是指测量模式,而 SpecSize 是指在某种测量模式下的规格大小。

SpecMode 有三种模式:

  1. UNSPECIFIED 不限制:父容器不对 View 有任何限制,要多大给多大,这种情况比较少见,一般不会用到。

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

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

MeasureSpec 中三个主要的方法来处理 SpecMode 和 SpecSize

  1. makeMeasureSpec 打包 SpecMode 和 SpecSize
  2. getMode 解析出 SpecMode
  3. getSize 解析出 SpecSize

不知道童鞋们之前有没有注意到 onMeasure 有两个参数 widthMeasureSpec 和 heightMeasureSpec,那这两个值从哪来的呢,这两个值都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小,但是最外层的根视图 也就是 DecorView ,它的 widthMeasureSpec 和 heightMeasureSpec 又是从哪里得到的呢?这就需要去分析 ViewRoot 中的源码了,在 performTraversals 方法中调了 measureHierarchy 方法来创建 MeasureSpec 源码如下:

 private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) { int childWidthMeasureSpec; int childHeightMeasureSpec; childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); //省略代码... } 

里面调用了 getRootMeasureSpec 方法生成 MeasureSpec,继续查看 getRootMeasureSpec 源码

 private static int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; } 

通过上述代码,DecorView 的 MeasureSpec 的产生过程就很明确了,具体来说其遵守如下规则,根据它的 LayoutParams 中的宽和高的参数来划分。

  • LayoutParams.MATCH_PARENT:限制固定值,大小就是窗口的大小 windowSize
  • LayoutParams.WRAP_CONTENT:限制上限,大小不定,但是不能超过窗口的大小 windowSize
  • 固定大小:限制固定值,大小为 LayoutParams 中指定的大小 rootDimension

对于 DecorView 而言, rootDimension 的值为 lp.width 和 lp.height 也就是屏幕的宽和高,所以说 根视图 DecorView 的大小默认总是会充满全屏的。那么我们使用的 View 也就是 ViewGroup 中 View 的 MeasureSpec 产生过程又是怎么样的呢,在 ViewGroup 的测量过程中会具体介绍。

先回头看 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 是 AT_MOST 或 EXACTLY 就返回 specSize,这也是系统默认的行为。之后会在 onMeasure 方法中调用 setMeasuredDimension 方法来设定测量出的大小,这样 View 的 measure 过程就结束了,接下来看 ViewGroup 的 measure 过程。

ViewGroup 的测量过程

ViewGroup中定义了一个 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); } } } 

从上述代码来看,除了完成自己的 measure 过程以外,还会遍历去所有在页面显示的子元素, 然后逐个调用 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); } 

measureChild 的思想就是取出子元素的 LayoutParams,然后再通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View 的 measure 方法来进行测量。

那么 ViewGroup 是如何创建来创建子元素的 MeasureSpec 呢,我们继续看 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 = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); } 

上面的代码理解起来很简单,为了更清晰地理解 getChildMeasureSpec 的逻辑,这里提供一个表,表中对 getChildMeasureSpec 的工作原理进行了梳理,表中的 parentSize 是指父容器中目前可使用的大小,childSize 是子 View 的 LayoutParams 获取的值,从 measureChild 方法中可看出

final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); 

表如下:

通过上表可以看出,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams,就可以快速地确定出子元素的 MeasureSpec 了,有了 MeasureSpec 就可以进一步确定出子元素测量后的大小了。

至此,View 和 ViewGroup 的测量过程就告一段落了。来个小结。

MeasureSpec 的模式和生成规则 MeasureSpec 中 specMode 有三种模式:

  1. UNSPECIFIED 不限制:父容器不对 View 有任何限制,要多大给多大,这种情况比较少见,一般不会用到。

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

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

生成规则:

  1. 对于普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定。
  2. 对于不同 ViewGroup 中的不同 View 生成规则参照上表。

MeasureSpec 测量过程: measure 过程主要就是从顶层父 View 向子 View 递归调用 view.measure 方法,measure 中调 onMeasure 方法的过程。

说人话呢就是,视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在 XML 文件中指定视图的大小,然后视图本身会对最终的大小进行拍板。

那么测量过后,怎么获取 View 的测量结果呢 一般情况下 View 测量大小和最终大小是一样的,我们可以使用 getMeasuredWidth 方法和 getMeasuredHeight 方法来获取视图测量出的宽高,但是必须在 setMeasuredDimension 之后调用,否则调用这两个方法得到的值都会是0。为什么要说是一般情况下是一样的呢,在下文介绍 Layout 中会具体介绍。

Layout 布局过程

测量结束后,视图的大小就已经测量好了,接下来就是 Layout 布局的过程。上文说过 ViewRoot 的 performTraversals 方法会在 measure 结束后,执行 performLayout 方法,performLayout 方法则会调用 layout 方法开始布局,代码如下

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { mLayoutRequested = false; mScrollMayChange = true; mInLayout = true; final View host = mView; if (host == null) { return; } if (DEBUG_ORIENTATION || DEBUG_LAYOUT) { Log.v(mTag, "Laying out " + host + " to (" + host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")"); } try { host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); //...省略代码 } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } mInLayout = false; 

View 类中 layout 方法实现如下:

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); if (shouldDrawRoundScrollbar()) { if(mRoundScrollbarRenderer == null) { mRoundScrollbarRenderer = new RoundScrollbarRenderer(this); } } else { mRoundScrollbarRenderer = null; } mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLayoutChangeListeners != null) { ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)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; if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) { mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT; notifyEnterOrExitForAutoFillIfNeeded(true); } } 

layout 方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的,然后会调用 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop、mBottom 这四个值,View 的四个顶点一旦确定,那么 View 在父容器中的位置也就确定了,接着会调用 onLayout 方法,这个方法的用途是父容器确定子元素的位置,和 onMeasure 方法类似

onLayout 源码如下:

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

纳尼,怎么是个空方法,没错,就是一个空方法,因为 onLayout 过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置,我们继续看 ViewGroup 中的 onLayout 方法

@Override protected abstract void onLayout(boolean changed, int l, int t, int r, int b); 

可以看到,ViewGroup 中的 onLayout 方法竟然是一个抽象方法,这就意味着所有 ViewGroup 的子类都必须重写这个方法。像 LinearLayout、RelativeLayout 等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。所以呢我们如果要自定义 ViewGroup 那么就要重写 onLayout 方法。

public class TestViewGroup extends ViewGroup { public TestViewGroup(Context context, AttributeSet attrs) { super(context, attrs); } @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (getChildCount() > 0) { View childView = getChildAt(0); measureChild(childView, widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (getChildCount() > 0) { View childView = getChildAt(0); childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight()); } } } 

xml 中使用

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <com.will.testdemo.customview.TestViewGroup android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:color/holo_blue_bright" > <TextView android:id="@+id/tv_hello" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@color/colorPrimaryDark" android:text="@string/hello_world" android:textColor="@android:color/white" android:textSize="15sp" /> </com.will.testdemo.customview.TestViewGroup> </LinearLayout> 

显示效果如下:

不知道童鞋们发现了没,我给自定义的 ViewGroup 设置了背景色,看效果貌似占满全屏了,可是我在 xml 中设置的 wrap_content 啊,这是什么情况,我们回头看看 ViewGroup 中 View 的 MeasureSpec 的创建规则

从表中可看出因为 ViewGroup 的父布局设置的 match_parent 也就是限制固定值模式,而 ViewGroup 设置的 wrap_content,那么最后 ViewGroup 使用的是 父布局的大小,也就是窗口大小 parentSize,那么如果我们给 ViewGroup 设置固定值就会使用 我们设置的值,来改下代码。

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <com.will.testdemo.customview.TestViewGroup android:layout_width="200dp" android:layout_height="100dp" android:background="@android:color/holo_blue_bright" > <TextView android:id="@+id/tv_hello" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@color/colorPrimaryDark" android:text="@string/hello_world" android:textColor="@android:color/white" android:textSize="15sp" /> </com.will.testdemo.customview.TestViewGroup> </LinearLayout> 

效果如下:

表中的其他情况,建议童鞋们自己写下代码,会理解的更好。

之前说过,一般情况下 View 测量大小和最终大小是一样的,为什么呢,因为最终大小在 onLayout 中确定,我们来改下代码:

 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (getChildCount() > 0) { View childView = getChildAt(0); childView.layout(0, 0, childView.getMeasuredWidth()+100, childView.getMeasuredHeight()+200); } } 

显示效果

没错,onLayout 就是这么任性,所以要获取 View 的真实大小最好在 onLayout 之后获取。那么如何来获取 view 的真实大小呢,可以通过下面的代码来获取

 tv_hello.post(Runnable { log(" getMeasuredWidth() = ${tv_hello.measuredWidth}") log(" getWidth() = ${tv_hello.width}") } 

打印如下:

01-18 23:33:20.947 2836-2836/com.will.testdemo I/debugLog: getMeasuredWidth() = 239 01-18 23:33:20.947 2836-2836/com.will.testdemo I/debugLog: getWidth() = 339 

可以看到实际高度和测试的高度是不一样的,因为我们在 onLayout 中做了修改。

因为 View 的绘制过程和 Activity 的生命周期是不同步的,所以我们可能在 onCreate 中获取不到值。这里提供几种方法来获取

1.Activity 的 onWindowFocusChanged 方法

 override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) if (hasFocus){ //获取 view 的大小 } } 

2.view.post(runnable) 也就是我上面使用的方法 3.ViewTreeObserver 这里童鞋们搜索下就可以找到使用方法,篇幅较长就不举例子了

Draw 绘制过程

确定了 View 的大小和位置后,那就要开始绘制了,Draw 过程就比较简单,它的作用是将 View 绘制到屏幕上面。View 的绘制过程遵循如下几步:

  1. 绘制背景 background.draw (canvas)
  2. 绘制自己(onDraw)
  3. 绘制 children(dispatchDraw)
  4. 绘制装饰(onDrawScrollBars)
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); drawAutofilledHighlight(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); // Step 7, draw the default focus highlight drawDefaultFocusHighlight(canvas); if (debugDraw()) { debugDrawFocus(canvas); } // we're done... return; } //省略代码.. } 

View 的绘制过程的传递是通过 dispatchDraw 实现的,dispatchdraw 会遍历调用所有子元素的 draw 方法,如此 draw 事件就一层一层的传递下去。和 Layout 一样 View 是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制,重写 onDraw 方法。具体可参考 TextView 或者 ImageView 的源码。

最后

View 的工作流程和原理到这就分析完了,难点主要是 MeasureSpec 测量过程,需要童鞋们认真揣摩。