Android View 原理

Android View原理

前言:本文从源码出发并总结了 View 的底层绘制机制,希望能帮助到大家。

1. 了解view

简单说 view 是 android 在屏幕上提供的一块矩形的显示区域,在这块矩形区域上我们可以响应各种屏幕事件(点击,拖拽等)以及显示内容。

下图为 android 中 view 以及各参数关系示意图。其中 padding 为内边距,layout_margin 为外边距。虽然 padding 、layout_margin 都为 View 的 xml 属性,但是实际上在视图绘制的时候,padding 是由 View 本身来绘制的,layout_margin 是由该 View 的父容 ViewGroup 来绘制的。(实际上,所有属性上带有 layout_ 前缀的基本都由其父容器决定。)

在Android中,View机制与CSS盒子模型类似却也有区别。View 和 BoxModel 均有内外边距及边框等设定,但是对于padding属性的计算方式有所不同。View 的 layout_width(layout_height)属性不包括 padding 的计算即 View 的实际大小与 padding 无关,假设一个View的大小确定之后,padding只是单纯从内部在重新计算实际内容与该 View 已确定边界的距离;但是 CSS 中 padding 的大小会影响盒子的最终大小,即 padding 为外部扩充,会增加盒子大小,即边框内容的大小由 height(width)和 padding 共同决定。下图为Android View 模型示意图。
在这里插入图片描述

2. Android View 绘制流程

2.1 .View 总体绘制流程

说到 View 绘制,大致分为3大步。

  1. 测量 (measure -> onMeasure)
  2. 布局(layout -> onLayout)
  3. 绘制(draw -> dispatchDraw)

大致如图所示:
在这里插入图片描述
我们在自定义 View 时,需要做的是就是重写 onMeasure、onLayout、onDraw 方法。

2.2 先从一幅图说起

在这里插入图片描述

  1. 一个 Activity 与一个 PhoneWindow 关联
  2. 一个 PhoneWindow 由一个 DecorView 组成
  3. DecorView 实际上是一个 FrameLayout 容器
  4. DecorView 由一个子 View 组成,该子 View 是一个垂直的 LinearLayout 容器,包含两个子元素,一个是TitleView(ActionBar的容器),另一个是ContentView(窗口内容的容器)
  5. ContentView 实际上也是一个 FrameLayout 容器(通常 setContentView( layoutID )就是设置它的内容

2.3 setContentView( int resourceID )

当 Activity 创建后,根据其生命周期首先执行 Oncreate 方法,其中 setContentView 方法并不是进行视图 View 的绘制,而是进行 DecorView(顶级视图) 的初始化和 resourceID xml 文件所代表 View 树中所有 View 的初始化创建。setContentView 最后会调用 mLayoutInflater.inflate 来创建了自定义 xml 中的布局视图,添加到 DecorView 中。

大致流程如下图所示:
setContentView

对于inflate方法细节,参考View的inflate详解
总而言之,setContentView 并没有进行 View 的绘制,而是进行 View 的初始化创建。

2.4 measure 开始测量

上面说到 setContentView 方法不进行 View 的绘制,那么接下来谈谈 View 的绘制流程。我们先来谈谈绘制流程的第一步也是最复杂的一步:测量。顾名思义,大致为计算出 View 矩形区域宽高的过程。

2.4.1 ViewRoot 与 DecorView

ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带,View 的绘制流程是通过 ViewRoot 开始的。在 ActivityThread 中,当 Activity 对象被创建后,会将 DecorView 添加到 PhoneWindow 中,同时创建 ViewRootImpl 对象,并将 ViewRootImpl 对象与 DecorView 对象关联。关联过后,ViewRootImpl 类的 requestLayout() 方法会被调用,最终使得 ViewRootImpl 的 performTraversals()方法被调用。详细见源码。

View 的绘制流程是从 ViewRoot 的 performTraversals 方法开始的,下图为 performTraversals 方法简化流程。如图所示,performTraversals 方法回先后调用 performMeasure performLayout performDraw三个方法分别完成测量、布局、绘制过程。对于 ViewGroup 来说,在 onMeasure (onLayout,onDraw)方法中再调用其孩子的 measure(layout,draw)方法。

在这里插入图片描述

如下图所示,在 DecorView 的测量过程中,首先需要获取其自身的 MeasureSpec 之后才能执行测量,为了获取其 MeasureSpec ,需要传入两个参数:

  1. 父容器的 MeasureSpec
  2. 自身的 layout_width,layout_height 属性(LayoutParams)

在这里插入图片描述

2.4.2 MeasureSpec 与 LayoutParams

每一个 View 的 MeasureSpec 的确定,是由自身 View 的LayoutParams 与 其父 View 的 MeasureSpec 共同决定的。

2.4.2.1 MeasureSpec

为了更好地理解 View 的测量过程,我们需要理解 MeasureSpec 。顾名思义,MeasureSpec 看起来像“测量规格”或者“测量说明书”。不管怎样翻译,它决定了该 View 的测量过程。

MeasureSpec 实际上代表的是一个32位的 int 值。

  • specMode,由高2位表示
  • specSize,由低30位表示

specMode 由3类:

  1. UNSPECIFIED(父容器对自身 View 没有任何限制,要多大给多大,一般用于系统内部)
  2. EXACTLY(父容器已经检测出自身 View 需要的精确大小,这个时候 View 的最终大小就是 SpecSize 的值,可以理解为精确模式)
  3. AT_MOST(父容器提供了一个可用大小即 SpecSize,自身 View 的大小不能大于这个值,可以理解为最大值模式)

在这里特别说明一处:对于specMode是对于自身 View 的一种尺寸约束,是自身 View 的一种属性,并不是为了决定该 View 的子 View 而专门设置的,如“父容器已经检测出 View 需要的精确大小,这个时候 View 的最终大小就是 SpecSize 的值”这句描述,代表自身 View 的精确尺寸为 specSize ,描述中的父容器指的是自身 View 的上层容器。特别提示,防止误解。

2.4.2.2 LayoutParams

LayoutParams 顾名思义“布局参数”,其中包含了一系列参数(例如 xml 属性:layout_width )。最基本的就是宽高参数:

  1. LayoutParams.MATCH_PARENT
  2. LayoutParams.WARP_CONTENT
  3. 固定大小(比如100 dp)
2.4.3 自身 View MeasureSpec 的确定

对于 View,其 MeasureSpec 的确定由getChildMeasureSpec方法完成的,其参数就是其父 View 的 MeasureSpec 与自身 View 的 LayoutParams
其中 padding 传递的值是父View已经使用的尺寸

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

根据源码,我们可以总结出如下图的一张表来说明其中关系

在这里插入图片描述

由此可以看出如何决定一个 View 的 MeasureSpec。例如当父 View 的 specMode 为 AT_MOST 且自身 View 的 LayoutParams 为 match_parent 时,自身 View 的 measureSpec = AT_MOST + parentSize 。

2.4.4 ViewGroup 与 View 的测量
2.4.4.1 measure

2.2 提及过,顶层视图 DecorView 实际上是一个 FrameLayout,本质上是 ViewGroup 。那么接着 2.4.1 中谈到的 DecorView 的绘制,我们继续分析流程,在获取到 DecorView 的 measureSpec 之后,接下来开始执行 measure 方法,由于 measure 方法是一个 final 型方法,实际上不管是 ViewGroup 还是 View ,执行的都是 View 类中的方法,如下是其源码。

measure 方法说明: measure 方法是 View 类中的一个 final 型方法,它不可以被重写。

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }

        // Suppress sign extension for the low bytes
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

        // Optimize layout by avoiding an extra EXACTLY pass when the view is
        // already measured as the correct size. In API 23 and below, this
        // extra pass is required to make LinearLayout re-distribute weight.
        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

        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 方法的前面部分,是在进行是否需要重新测量的判断,重点是接下来的这部分代码。

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

对于这部分代码的理解:如果需要重新测量,那么首先判断 cache 缓存中是否存在相应 View,如果存在则直接从缓存中获取 measureSpec,然后设置(setMeasuredDimensionRaw((int) (value >> 32), (int) value)),如果缓存中不存在,则执行onMeasure方法。

回顾 2.1 所讲的,对应着流程图。实际上我们可以知道 measure 方法实际上做的工作是:判断是否需要重新测量,如果需要,则执行onMeasure。接下来我们就进行 onMeasure 的分析。

2.4.4.2 onMeasure

onMeasure 是需要我们在自定义 View 时需要重写的方法。对于容器类 View 和普通 View,它有不同的实现要点。

View 默认的 onMeasure

话不多说,先看源码:

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

顾名思义:setMeasuredDimension是进行 measureSpec 的设置,源码如下:

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

对于setMeasuredDimensionRaw方法,是不是有印象了,它对应了 2.4.4.1 的不需要重新测量的步骤。
我们在继续看他的参数,这里我们只看一个参数就行了。getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),源码如下:

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

对于其第一个形参,它一般用于系统内部,这里不再分析。在这里我们清楚地可以看见:对于 AT_MOST 和 EXACTLY 这两种模式,它们的 result 结果都是 specSize。这意味着,如果不加区分,这两种模式对应的尺寸是一样的,所以,这里我们需要重写 OnMeasure 方法来达到目的。

ViewGroup 的 onMeasure

对于 ViewGroup 的 onMeasure,需要做的工作比 View 的要复杂一点。由于 Android 内部有若干 ViewGroup 的实现,这里我们选 FrameLayout(DecorView 就是一个FrameLayout) 进行具体分析。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();

        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        // Account for padding too
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                final int childWidthMeasureSpec;
                if (lp.width == LayoutParams.MATCH_PARENT) {
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }

                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) {
                    final int height = Math.max(0, getMeasuredHeight()
                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }

                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

由于代码较长,不进行细致地分析。首先从第一个 for 循环上看,不难得出这里是在对所有孩子进行一一测量。其中我们注意一下measureChildWithMargins方法,如下是它的源码。在该方法中,通过getChildMeasureSpec(该方法具体见 2.4.3 )来获取子 View 的 measureSpec,在得到 MeasureSpec 之后,通过child.measure(childWidthMeasureSpec, childHeightMeasureSpec)将父 View 地测量过程递归到了子 View 的测量。等所有的孩子都测量完之后,即for循环结束,之后在执行setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),resolveSizeAndState(maxHeight, heightMeasureSpec,childState << MEASURED_HEIGHT_STATE_SHIFT))来设置自己的 measureSpec。分析到这就可以大概地明白测量流程了,后面的源码就不再分析了。

padding 与 layout_margin:通过分析getChildMeasureSpec的参数可知,对于任何一个 ViewGroup 而言,在它的 onMeasure 过程中包括了其子 View(如果有子 View 的话)的 margin(外边距)和自身 padding(内边距)的计算。对于 View 而言,其 onMeasure 过程中不包括 padding 属性的处理,即 paddind 是被 specSize 所包含进去的,如 1 中图所示,所以对于 padding 属性,我们需要在绘制(ondDraw)过程中体现出来。

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        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);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
onMeasure 实现要点

View 的 onMeasure 实现要点:对于 AT_MOST 和 EXACTLY 这两种模式,它们的 result 结果都是 specSize,实现时需要考虑这方面的处理。如图:

在这里插入图片描述

对于 ViewGroup onMeasure 的实现要点:可以参考下列流程进行简单实现(一般建议继承 ViewGroup 的实现自类们(FrameLayout等等))

在这里插入图片描述

2.5 layout 开始布局

2.5.1 layout 过程

在 measure 过程完成后,接下来就是进行布局环节。布局,顾名思义是指确定视图的位置。当一个 ViewGroup 的位置确定了,它会在 onLayout 方法中遍历所有的子元素并调用其 layout 方法,在 layout 中 onLayout 又会被调用。

layout 方法确定 View 自身位置,而 onLayout 方法则会确定所有子元素位置,所以如果是自定义普通 View,则不需要重写 onLayout 方法。

所有 View 的 执行的 layout 方法都是同一个(View 类中的 layout 方法,ViewGroup 的了 layout 方法也是间接调用了这个方法),这个方法用来确定自己在父容器里的位置。源码如下:其主要是通过 setFrame(l, t, r, b)方法来确定自身位置的,接着还会调用onLayout 方法来确定其子元素位置。

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

        final boolean wasLayoutValid = isLayoutValid();

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

        if (!wasLayoutValid && isFocused()) {
            mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
            if (canTakeFocus()) {
                // We have a robust focus, so parents should no longer be wanting focus.
                clearParentsWantFocus();
            } else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
                // This is a weird case. Most-likely the user, rather than ViewRootImpl, called
                // layout. In this case, there's no guarantee that parent layouts will be evaluated
                // and thus the safest action is to clear focus here.
                clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                clearParentsWantFocus();
            } else if (!hasParentWantsFocus()) {
                // original requestFocus was likely on this view directly, so just clear focus
                clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
            }
            // otherwise, we let parents handle re-assigning focus during their layout passes.
        } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
            mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
            View focused = findFocus();
            if (focused != null) {
                // Try to restore focus as close as possible to our starting focus.
                if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
                    // Give up and clear focus once we've reached the top-most parent which wants
                    // focus.
                    focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                }
            }
        }

        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }

由于一般只有 ViewGroup 才需要重写 onLayout 方法,所以我们来看下具体的一个实现(LinearLayout)。看源码时我们不用细致,我们找关键地方理解即可。如final int width = child.getMeasuredWidth();final int height ==child.getMeasuredHeight();最后child.layout(childLeft, childTop, childLeft + width, childTop + height);,由此我们可以看出 layout 的4个入口参数是什么了。width 与 height 分别是测量过程的结果 specSize。

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();

        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;

                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }

                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }

                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }
2.5.2 onLayout 实现要点

在这里插入图片描述

由于 layout 与 measure 机理类似,且比 measure 简单,所以不做过多阐述。

2.6 draw 开始绘制

终于到了这最后的一步,绘制。

draw 过程就最简单了,它的作用就是将 View 绘制到屏幕上。主要遵循以下步骤:

  1. 绘制背景(background.draw(canvas))
  2. 绘制自己(onDraw)
  3. 绘制孩子(dispatchDraw)
  4. 绘制装饰(onDrawScrollBars)

这些可以根据源码看出来:

    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        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;

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

唯一注意的是 draw 的绘制传递是通过 dispatchDraw 来的,dispatchDraw 会遍历所有孩子的 draw 方法,所以,自定义 View 的时候需要重写 onDraw 方法。

体会

第一次写博客,用了心也花了时间精力去完成。希望以后能在博客上记录自己所学,巩固提升。

参考与引用:

  1. https://www.jianshu.com/p/060b5f68da79
  2. 《Android开发艺术探索》
  3. https://blog.csdn.net/boyeleven/article/details/82759753
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值