文章目录

    • 1. 基础概念
    • 2. MeasureSpec
      • 2.1 SpecMode 和 SpecSize
      • 2.2 MeasureSpec 和 LayoutParams
      • 2.3 margin 和 padding
    • 3. performMeasure
      • 3.1 View 的 measure 过程
      • 3.2 ViewGroup 的 measure 过程
    • 4. performLayout
    • 5. performDraw

  在 Android 的知识体系中,View 占据非常重要的作用,简单来理解,View 是 Android 在视觉上的呈现。为了更好的自定义控件,也就是自定义 View ,我们还需要掌握 View 的底层原理。在 Android 系统中 UI 显示的 3 要素是尺寸大小、位置和内容,分别对应于 View 的 measure、layout和 draw过程。

1. 基础概念

  • View 和 ViewRoot
    从名字来理解,“ViewRoot” 似乎是 “View 树的根” 。这很容易让人产生误解,因为 ViewRoot 并不属于 View 树的一份子。从源码实现上来看,ViewRoot 和 View 对象并没有任何“血缘”关系,它即非 View 的子类,也并非 View 的父类。更确切的说,ViewRoot 可以被理解为“ view 树的管理者”——它有一个 mView 的成员变量,指向的是它所管理的View 树的根。ViewRoot 的核心任务就是与 WindowManagerService 进行通信。

  • Activity 和 Window
    Activity 是支持 UI 显示的,那么它是直接管理 View 树或者 ViewRoot 呢?其实都不是,Activity 内部有一个Window的类型的成员成员 mWindow 。Window 就是“窗口”的意思, Window 是基类,根据不同的产品可以衍生出不同的子类——具体则是由系统在 Activity.attach 中调用 PolicyManager.makeNewWindow 决定的,目前版本的 Android 系统默认生成的都是 PhoneWindow。

  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 绘制在屏幕上。

2. MeasureSpec

  MeasureSpec 在 View 的测量过程中起到很大的作用,MeasureSpec 在很大程度上决定了一个 View 的尺寸规格,之所以说是很大程度上是因为这个过程还受父容器的影响,因为父容器影响 View 的 MeasureSpec 的创建过程。在测量的过程中,系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec,然后在根据这个MeasureSpec 来测量出 View 的宽/高。

2.1 SpecMode 和 SpecSize

  MeasureSpec 代表一个 32 为 int 值,高 2 为代表 SpecMode,低 30 位代表 SpecSize,SpecMode 是指测量模式,而 SpecSize 是指在某种测量模式下 View 的大小。SpecMode 和 SpecSize 是一个 int 值,一组 SpecMode 和 SpecSize 可以打包成一个 MeasureSpec,而一个 MeasureSpec 可以通过解包的形式来得出原始的 SpecMode 和 SpecSize,需要注意的是这里提到的 MeasureSpec 是指 MeasureSpec 所代表的 int 值,而并非 MeasureSpec 本身。

  SpecMode 分为三类,每一个类都表示特殊的含义,分别如下:

  • UNSPECIFIED : 父容器不对 View 有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。
  • EXACTLY : 父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和 具体的数组这两种模式。
  • AT_MOST : 父容器指定了一个可用大小即 SpecSize ,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 warp_content。

2.2 MeasureSpec 和 LayoutParams

  系统内部是通过 MeasureSpec 来进行 View 的测量,正常情况下,我们可以使用 View 指定 MeasureSpec ,但是我们还可以给 View 指定 LayoutParams,这使我们指定的 MeasureSpec 是不准确的,所以必须经过系统自己的测量。在 View 测量的时候,系统会将 LayoutParams 在父容器的约束下转换成对应的 MeasureSpec,然后再根据这个 MeasureSpec 来确定 View 测量后的宽/高。需要注意的是,MeasureSpec 不但由 LayoutParams 决定的,而且还和父容器一起才能决定 View 的 MeasureSpec。MeasureSpec 一旦确定后,omMeasure 中就可以确定 View 的测量的宽和高了。
  对于普通 View 来说,这里值我们布局中的 View ,View 的 measure 过程由 ViewGroup 传递而来,先看一下 ViewGroup 的 measureChildWithMargins 方法:

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

从代码可以看出,在 ViewGroup 中调用子元素的 measure 方法之前会先通过 getChildMeasureSpec 方法来得到子元素的 MeasureSpec 。从代码来看,很显然,子元素的 MeasureSpec 的值是由父容器的 MeasureSpec 、 子元素本身的 LayoutParams 和 View 的 margin 及 padding (下面有两者的区别)共同决定的。下面根据getChildMeasureSpec 方法的主要内容用表格表现出来:

childLayoutParams \ parentSpecMode EXACTLY AT_MOST UNSPECIFIED
dp/px EXACTLY
= childSize
EXACTLY
= childSize
EXACTLY
= childSize
match_parent EXACTLY
= parentSize
AT_MOST
<= parentSize
UNSPECIFIED
= 0
warp_content AT_MOST
<= parentSize
AT_MOST
<= parentSize
UNSPECIFIED
= 0

简要说明的,对于普通 View ,其 MeasureSpec 由父容器的 MeasureSpec 和 自身的 LayoutParams 来共同决定,那么针对不同的父容器和 View 本身不同的 LayoutParams,View 就可以有多种 MeasureSpec。具体概括就是:

  • 当 View 采用固定宽或高的时候,不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec都是精确模式并且View 的大小遵循 LayoutParams 中的大小。
  • 当 View 的宽或高是 match_parent 时,如果父容器的模式时精确模式,那么 View 也是精确模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么 View 也是最大模式并且其大小不会超过父容器的剩余空间。
  • 当 View 宽或高是 warp_content 时,不管父容器的 MeasureSpec 是什么,View 的模式总是最大化并且大小不会超过父容器的剩余空间。

2.3 margin 和 padding

  对于 View 类而言,它只有 padding,没有 margin。这是因为 padding 值的是“内容”区域与外围边框的距离,分为 left、right、top、bottom 四个方向。而 margin 则是“内容”内部进一步细化——即“内容”中各元素之间的间距。可想而知,一个 View (非ViewGroup)实例的“内容”本身就是不可分割的,不存在内部对象间距的说法。对于 ViewGroup 由多个子对象组成,它们之间有时需要 margin 属性来将彼此区分开来。
Android View 的工作原理浅析_第1张图片

3. performMeasure

  measure 过程要分情况来看,如果只是一个原始的 View ,那么通过 measure 方法就完成了其测量过程,如果是一个 ViewGroup ,除了完成自己的测量之外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个流程,下面针对这两种情况分别讨论。

3.1 View 的 measure 过程

View 的 measure 过程由其 measure 方法来完成,measure 方法是一个 final 类型的方法,这就意味着不能重写此方法,在 View 的 measure 方法中会去调用 View的 onMeasure 方法,因此只需要看 onMeasure 发实现即可, View 的 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;    }

  可以看出,getDefaultSize 这个方法的逻辑很简单,对于我们来说,我们只需要看 AT_MOST 和 EXACTLY 这两种情况。简单的理解就是,getDefaultSize 返回的大小就是 MeasureSpec 中的 specSize,而这个 SpecSize 就是 View 测量后的大小,这里多次提到测量后的大小,是因为 View 最终的大小是在 layout 阶段确定的,所以这里必须要加以区分,但是几乎所有情况下 View 的测量大小和最终大小是相同的。
  至于 UNSPECIFIED 这种情况,一般用于系统内部的测量过程,在这种情况下,View 大小为 getDefaultSize 的第一个参数 size,即宽和高为 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 这两个方法的返回值,它们的源码是:

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

  我们来分析 getSuggestedMinimumWidth 方法(height方法原理一样)。从代码可以看出如果 View 没有设置背景,那么 View 的宽度为 mMinWidth ,而 mMinWidth 对应于 android:minWidth 这个属性所指定的值。如果这个属性不指定,那么 mMinWidth 则默认为 0 ;如果 View 指定了背景,则 View 的宽度为 max(mMinWidth, mBackground.getMinimumWidth()),mBackground.getMinimumWidth 返回的就是 Drawable 的原始宽度,前提是这个 Drawable 有原始宽度,否则就放回 0 。那么 Drawable 在什么情况下有原始宽度呢?例如,ShapeDrawable 无原始宽和高,而 BitmapDrawable 有原始宽和高(图片的尺寸)。
  从 getDefaultSize 方法的实现来看,View 的宽和高由 specSize 决定,所以我们得出结论: 直接继承 View 的自定义控件需要重新写 onMeasure 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent 。 因为,如果 View 在布局中使用 wrap_content ,那么它的 specMode 是 AT_MOST ,在这种模式下,它的宽和高就等于 specSize。由上面的表格可以看出,View 的 specSize 是 parentSize ,而 parentSize 是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小。很显然,View 的宽和高就等于父容器当前剩余的空间大小,这种效果和布局中使用 match_parent 完全一致。解决办法为:

    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);int mWidth = 0;//默认的宽,自己定义int mHeight= 0;//默认的高,自己定义        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);                if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {            setMeasuredDimension(mWidth, mHeight);        } else if (widthSpecMode == MeasureSpec.AT_MOST) {            setMeasuredDimension(mWidth, heightSpaceSize);        } else if (heightSpecMode == MeasureSpec.AT_MOST){            setMeasuredDimension(widthSpaceSize, mHeight);        }    }

对于非 wrap_content 的情形,我们沿用系统的测量值即可,至于这个默认的内部宽和高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可。

3.2 ViewGroup 的 measure 过程

  对于 ViewGroup 来说,除了完成自己的 measure 过程以外,还会遍历去调用所有子元素的 measure 方法,各个子元素在递归去执行这个过程。和 View 不同的是,ViewGroup 是一个抽象类,因此它没有重写 View 的 onMeasure 方法,但是它提供了一个叫 measureChildren 的方法,如下:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {        final int size = mChildrenCount;        final View[] children = mChildren;        for (int i = 0; i < size; ++i) {            final View child = children[i];            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {                measureChild(child, widthMeasureSpec, heightMeasureSpec);            }        }    }

从上述代码中,ViewGroup 在 measure 时,会对每一个子元素进行 measure,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 方法来进行测量。getChildMeasureSpec 就是上述 2.2 中的表格的内容。
  我们知道,ViewGroup 并没有定义其测量的具体过程,这是因为 ViewGroup 是一个抽象类,其测量过程的 onMeasure 方法需要各个子类去具体实现,比如 LinearLayout、RelativeLayout 等,为啥 ViewGroup 不像 View 一样对其 onMeasure 方法统一的实现呢?那是因为不同的 ViewGroup 子类有不同的布局特性,这导致它们的测量细节各不相同,比如 LinearLayout 和 RelativeLayout 这两者的布局特性显然不同,因此 ViewGroup 无法做统一实现。

4. performLayout

  Layout 的作用是 ViewGroup 用来确定子元素的位置,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,在 layout 方法中 onLayout 方法又会被调用。Layout 过程和 measure 过程相比就简单多了,layout 方法确定 View 本身的位置,而 onLayout 方法则会确定所有子元素的位置,先看 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 的具体实现同样和具体的布局有关,所以 View 和 ViewGroup 均没有真正实现 onLayout 方法。

5. performDraw

Draw 的过程就比较简单了,它的作用是将 View 绘制到屏幕上。View 的绘制过程遵循如下几步:
(1)绘制背景 background.draw(canvas)
(2)绘制自己(onDraw)
(3)绘制 children(dispatchDraw)
(4)绘制装饰(onDrawScrollBars)

draw 方法的源码是:

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 事件就一层层的传递下去。View 有一个特殊的方法 setWillNotDraw,先来看看它的源码:

    /**     * If this view doesn't do any drawing on its own, set this flag to     * allow further optimizations. By default, this flag is not set on     * View, but could be set on some View subclasses such as ViewGroup.     *     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}     * you should clear this flag.     *     * @param willNotDraw whether or not this View draw on its own     */    public void setWillNotDraw(boolean willNotDraw) {        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);    }

  从 setWillNotDraw 这个方法的注释中可以看出,如果一个 View 不需要绘制任何内容,那么设置这个标记为 true 以后,系统会进行相应的优化。默认情况下,View 没有启用这个优化标记位,但是 ViewGroup 会默认启用这个优化标记位。这个标记位对实际开发的意义是:当我们的自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个 ViewGroup 需要通过 onDwon 来绘制内容时,我们需要显示的关闭 WILL_NOT_DRAW 这个标记位。

站在巨人的肩膀上:
《Android 开发艺术探究》——任玉刚

更多相关文章

  1. Android中通过资源文件获取drawable的几种方法
  2. ListView取消和自定义分割线的方法
  3. Android L Preview 源码同步方法
  4. Android中检测网络连接状况的方法
  5. Android WebView的使用方法总结
  6. Android JNI 开启子线程后调用 Activity 方法更新UI
  7. Android屏幕旋转时Activity不重新调用onCreate的方法
  8. Android Studio中隐藏状态栏和标题栏的方法

随机推荐

  1. Android身份证件识别的OCR技术SDK
  2. Android(安卓)项目编译过程
  3. APPS大乱斗:4大Android文件浏览器横评(一)
  4. 在Ubuntu上下载、编译和安装Android最新
  5. android常用调试工具fiddle、wireshark和
  6. Android(安卓)PK ios,是谁胜谁负
  7. 【Android(安卓)多语言切换简单实例分享
  8. Android周学习Step By Step(4)--界面布局
  9. Android(安卓)cardview覆盖问题
  10. android apk安装原理分析