Android——View的工作流程——单一View的measure过程

目录结构

一、measure 过程

measure 过程

1. measure()

测量单一 view 大小的入口方法是 View 类的measure()方法,在该方法中会调用 View 类的onMeasure()方法,我们直接看onMeasure()方法的实现即可。

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 & 0xffffffff L;
    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 & 0xffffffff L); // suppress sign extension
}

2. onMeasure()

onMeasure()方法中主要有两个方法setMeasuredDimension()getDefultSize()

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

3. getDefaultSize()

通过 View 类中的getDefultSize()方法测量 View 的尺寸大小。该方法逻辑如下所示。

view 宽/高测量逻辑
// size:传入 view 的默认宽/高
// measureSpec:传入 view 的测量规格
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;
}

4. setMeasuredDimension()

View 类中的setMeasuredDimension()方法存储测量后的大小(宽/高)

二、重点说明

1.getDefaultSize()中如何得到 view 的默认宽/高

其中,view 的默认宽度通过getSuggestedMinimumWidth()获得。

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

view 的默认高度通过getSuggestedMinimumHeight()获得。

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

上述两个方法原理相同,下面说明getSuggestedMinimumWidth()的逻辑:

  • 若 view 无背景图片:view的默认宽度 = mMinWidth。
    指定了android:minWidth属性,则mMinWidth为指定值;
    未指定该属性的值,则默认为0。
  • 若 view 有背景图片:view的默认宽度=max(mMinWidth , 背景图片的原始宽度)

那么,如何获得 Drawable 的原始宽度?见获得 Drawable 的原始宽度

2.getDefaultSize()中子 View 测量规格 MeasureSpec 的创建过程

MeasureSpec类是View类中的一个静态内部类。

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK = 0x3 << MODE_SHIFT;

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

}

测量规格 MeasureSpec 是一个 32 位的 int 值,前 2 位保存测量模式(specMode),后 30 位保存测量大小(specSize)。将这两个值打包成一个 int 是为了避免过多的对象内存分配。

View 的 MeasureSpec 的创建过程对于顶层 View(DecorView)和普通 View 有所不同。

(1) DecorView 的 MeasureSpec 创建
取决于窗口尺寸大小,和 View 自身的 LayoutParams

(2)普通 View 的 MeasureSpec 创建
取决于父容器的 MeasureSpec,和 View 自身的 LayoutParams

  • 创建时机

对于普通 View 来说,View 的 measure 过程由 ViewGroup 传递而来,ViewGroup 的measureChildWithMargins()方法如下。该方法中在对子元素进行 measure 之前,先调用方法getChildMeasureSpec()得到子元素的测量规格

// 测量ViewGroup的子View:child
protected void measureChildWithMargins(View child,
    int parentWidthMeasureSpec, int widthUsed,
    int parentHeightMeasureSpec, int heightUsed) {
    // 得到子View的LayoutParams参数
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    // 调用方法getChildMeasureSpec()得到子View的测量规格 childWidthMeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
        mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin +
        widthUsed, lp.width);
    // 调用方法getChildMeasureSpec()得到子View的测量规格 childHeightMeasureSpec
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
        mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
        heightUsed, lp.height);
    // 进行单一View的测量过程
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
  • 计算方法

因此,子元素 MeasureSpec 的创建过程是在 ViewGroup 的getChildMeasureSpec()方法中完成的。

/**
 * ViewGroup 类中的方法,用于根据父容器的MeasureSpec和子View自身的LayoutParams得到子View的MeasureSpec
 *
 * @param spec :父View的测量规格(MeasureSpec) 
 * @param padding :父view中已占用的空间大小
 * @param childDimension :当前view的布局参数(宽/高),是我们所说的LayoutParams相关参数
 * @return a MeasureSpec integer for the child:返回子View的测量规格(MeasureSpec)
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    // 父view的测量模式
    int specMode = MeasureSpec.getMode(spec);
    // 父view的测量大小
    int specSize = MeasureSpec.getSize(spec);
    //通过父view计算出的子view大小 = 父大小-边距(父要求的大小,但子view不一定用这个值)   
    int size = Math.max(0, specSize - padding);

    // 子view想要的实际大小和模式(需要计算)
    int resultSize = 0;
    int resultMode = 0;

    // 通过父view的MeasureSpec和子view自身的LayoutParams计算子view的测量规格
    switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:

            if (childDimension >= 0) {
                // 当子view的LayoutParams>0,即有确切的值时
                // 子view的测量大小为自身所指定的值,测量模式为EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 当子view的LayoutParams==-1为MATCH_PARENT时
                // 子view的测量大小为通过父view计算出的子view大小 ,测量模式为EXACTLY
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 当子view的LayoutParams==-2为MATCH_PARENT时
                // 子view的测量大小自己决定,但不能超过通过父view计算出的子view大小 ,测量模式为AT_MOST
                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
            // 当父view的模式为UNSPECIFIED时,父view不对view有任何限制,要多大给多大
        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() 方法的逻辑进行梳理。表中的parentSize为父容器中可使用的大小,childSize为子 View 指定的具体数值。

摘自《Android开发艺术探索》

由上表可知:
普通 View 的 MeasureSpec 由父容器的 MeasureSpec 和 View 自身的 LayoutParams 决定。针对不同的父容器和 View 自身不同的 LayoutParams,View 就可以有多种 MeasureSpec。

规律总结

参考文献

自定义View Measure过程 - 最易懂的自定义View原理系列(2)
任玉刚_Android开发艺术探索

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335