热门IT资讯网

Android View 的工作流程和原理

发表于:2024-11-27 作者:热门IT资讯网编辑
编辑最后更新 2024年11月27日,参考资料 << Android 开发艺术探索 >> 欢迎访问我的个人博客 传送门前言在日常开发中,我们每天都在和各种 View 打交道,比如TextView,Button等,我们直接拿过来就可以使用,

参考资料 << 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 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;        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 中使用

                  

显示效果如下:

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

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

                

效果如下:

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

之前说过,一般情况下 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() = 23901-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 测量过程,需要童鞋们认真揣摩。

0