美文网首页
Android——View的工作流程——单一View的measu

Android——View的工作流程——单一View的measu

作者: 四喜汤圆 | 来源:发表于2020-07-26 20:49 被阅读0次

    目录结构

    一、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开发艺术探索

    相关文章

      网友评论

          本文标题:Android——View的工作流程——单一View的measu

          本文链接:https://www.haomeiwen.com/subject/rcuglktx.html