美文网首页Android自定义View
源码分析UI绘制三部曲之measure

源码分析UI绘制三部曲之measure

作者: Joker_Wan | 来源:发表于2019-12-09 12:57 被阅读0次

    众所周知,UI绘制三部曲是measure、layout、draw

    本篇我们分析View#measure

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
            ...
            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;
                }
    
                ...
            }
    
            mOldWidthMeasureSpec = widthMeasureSpec;
            mOldHeightMeasureSpec = heightMeasureSpec;
    
            mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                    (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
        }
    

    可以看到measure方法传入了两个参数widthMeasureSpec和heightMeasureSpec,分别是宽和高的测量规格, MeasureSpec是一个32位的int值,前2位是SpecMode,后30位是SpecSize。我们找到调用measure方法的地方,来看看这两个测量规格是如何获取的,通过查看源码,调用measure的地方在ViewRootImpl#performMeasure

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
            if (mView == null) {
                return;
            }
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
            try {
                mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
        }
    

    继续找performMeasure的调用地方在ViewRootImpl#performTraversals()

     private void performTraversals() {
            ...
                        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    
                        if (DEBUG_LAYOUT) Log.v(mTag, "Ooops, something changed!  mWidth="
                                + mWidth + " measuredWidth=" + host.getMeasuredWidth()
                                + " mHeight=" + mHeight
                                + " measuredHeight=" + host.getMeasuredHeight()
                                + " coveredInsetsChanged=" + contentInsetsChanged);
    
                         // Ask host how big it wants to be
                         // 看这里
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    
                        // Implementation of weights from WindowManager.LayoutParams
                        // We just grow the dimensions as needed and re-measure if
                        // needs be
                        int width = host.getMeasuredWidth();
                        int height = host.getMeasuredHeight();
                        boolean measureAgain = false;
            
            ...
     }
    

    可以看到childWidthMeasureSpec是通过getRootMeasureSpec(mWidth, lp.width)获取的,childHeightMeasureSpec是通过getRootMeasureSpec(mHeight, lp.height)获取的,mWidth表示窗口的宽,lp.width表示的是顶层View-DecorView布局属性的宽,mHeight同理表示的是窗口的高度,lp.height表示的是顶层View-DecorView布局属性的高,我们先来看下ViewRootImpl#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;
        }
    

    MeasureSpec.makeMeasureSpec()是用来打包SpecSize和SpecMode,可以看到这段代码根据DecorView的宽和窗口的宽来确定DecorView的measureSpec并返回,到这里我们总结下:DecorView的MeasureSpec由窗口大小和自身LayoutParams决定并遵循如下规则:

    • LayoutParams的宽高为LayoutParams.MATCH_PARENT:SepcMode为精确模式,SepcSize为窗口大小
    • LayoutParams的宽高为LayoutParams.WRAP_CONTENT:SepcMode为最大模式,SepcSize最大为窗口大小
    • LayoutParams的宽高为固定大小:SepcMode为精确模式,大小为LayoutParams的大小

    我们继续回到performMeasure方法中,起哄调用了View#measure,这里主要是对测量的一些优化工作,我们重点看onMeasure方法

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

    其中调用了setMeasuredDimension,继续跟进

        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方法,继续跟进

        private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
            mMeasuredWidth = measuredWidth;
            mMeasuredHeight = measuredHeight;
    
            mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
        }
    

    可以看到这里主要做的工作就是保存控件最终的宽高到mMeasuredWidth,mMeasuredHeight ,但DecorView是继承FragmentLayout,所以调用DecorView的onMeasure方法其实是调用FragmentLayout的onMeasure方法,跟进FragmentLayout#onMeasure

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

    onMeasure方法里面传过来的两个MeasureSpec就是当前容器自己MeasureSpec,可以看到其中通过一个for循环遍历子View ,并调用measureChildWithMargins,传入了child和自己的宽高测量规格,跟进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);
        }
    

    这段代码通过getChildMeasureSpec方法获取child的MeasureSpec,然后调用child的measure方法并且传入child的MeasureSpec来让child测量自己, 看下ViewGroup#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);
        }
    

    第一个参数spec是父容器的MeasureSpec,第二个参数padding表示父容器已经使用的空间,第三个参数childDimension就是child的布局参数里面对应的宽高尺寸,首先通过spec获取父容器的specMode和specSize,再根据这两个值来确定child自己的specMode和specSize,并调用makeMeasureSpec将specMode和specSize打包成MeasureSpec并返回,我们来总结下:子View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams决定,根据代码可知如下规则

    image.png

    我们继续回到FragmentLayout#onMeasure

       public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
       
            ...
       
            // 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));
    
           ...
    }
    

    在测量完子View之后,会得到自己的最大的宽和高,接着调用setMeasuredDimension方法来决定自身的宽高,至此ViewGroup的测量源码分析完毕。

    最后我们来总结下:

    View 测量代码调用顺序

    measure --> onMeasure --> setMeasuredDimension -->setMeasuredDimensionRaw(保存自己的宽高)

    ViewGroup 测量代码调用顺序

    measure --> onMeasure(测量子控件的宽高)--> setMeasuredDimension -->setMeasuredDimensionRaw(保存自己的宽高)

    DecorView的MeasureSpec由窗口大小和自身的LayoutParams决定
    子View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams决定

    相关文章

      网友评论

        本文标题:源码分析UI绘制三部曲之measure

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