Android View 的工作流程和原理

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android View 的工作流程和原理相关的知识,希望对你有一定的参考价值。

参考资料 << 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 测量过程,需要童鞋们认真揣摩。

以上是关于Android View 的工作流程和原理的主要内容,如果未能解决你的问题,请参考以下文章

Android UI绘制之View绘制的工作原理

Android进阶知识——View的工作原理

Android进阶知识——View的工作原理

Android进阶知识——View的工作原理

Android——View的工作原理

Android艺术开发探索第四章——View的工作原理(上)