浅析Android中View的测量布局流程

前言

掌握Android中View的工作机制有助于日常的UI开发工作,实现具有不同样式和交互的UI界面。如何在屏幕上呈现各种各样的视图元素 正是Android中的View工作机制解决的问题,主要包括:View的大小如何确定、View的位置如何确定以及View内容对应的渲染数据如何生成。

本文围绕前两个问题(View大小确定和View位置确定),对Android中View的工作机制进行分析,在知其然的同时,也要知其所以然,这样才能实现出自定义的UI效果。此外,View渲染数据的生成涉及到的内容较多,后面再单独分析。

View工作流程的触发时机

之前在《浅析Android中的Choreographer工作原理》中分析得出,每一次App进程在请求调度VSYNC信号之后,VSYNC信号会通过Socket发送到App进程,此时会触发View的工作流程,即测量、布局以及绘制。

一般来说,应用进程请求VSYNC信号分为两种情况,一种是启动一个Activity之后默认请求,一种是调用API更新View时主动请求。无论是哪种情况,最终在接收到VSYNC信号之后的处理流程是一样的。下面将结合源码对View的工作流程进行分析。

确定View的尺寸(onMeasure)

测量环节用于计算View的宽和高,即View的成员变量mMeasuredWidthmMeasuredHeight的值。

测量环节分为两部分,一部分是在View树上分发测量任务,一部分是对单个View进行具体测量工作。

测量流程

首先,看下测量流程开始的入口方法performTraversals,通过一系列的条件判断和处理最终得到了窗口的宽desiredWindowWidth和高desiredWindowHeight,并将其传入measureHierarchy方法。

java 复制代码
	// android.view.ViewRootImpl
	private void performTraversals() {
        // cache mView since it is used so much below...
        final View host = mView;
        if (host == null || !mAdded) {
            return;
        }

        mIsInTraversal = true;
        mWillDrawSoon = true;
        boolean windowSizeMayChange = false;
        WindowManager.LayoutParams lp = mWindowAttributes;

        int desiredWindowWidth;
        int desiredWindowHeight;

        WindowManager.LayoutParams params = null;
        CompatibilityInfo compatibilityInfo =
                mDisplay.getDisplayAdjustments().getCompatibilityInfo();
        if (compatibilityInfo.supportsScreen() == mLastInCompatMode) {
            params = lp;
            mFullRedrawNeeded = true;
            mLayoutRequested = true;
            // ...
        }

        Rect frame = mWinFrame;
        if (mFirst) {
            mFullRedrawNeeded = true;
            mLayoutRequested = true;

            final Configuration config = getConfiguration();
            if (shouldUseDisplaySize(lp)) {
                // NOTE -- system code, won't try to do compat mode.
                Point size = new Point();
                mDisplay.getRealSize(size);
                desiredWindowWidth = size.x;
                desiredWindowHeight = size.y;
            } else if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
                    || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                // For wrap content, we have to remeasure later on anyways. Use size consistent with
                // below so we get best use of the measure cache.
                final Rect bounds = getWindowBoundsInsetSystemBars();
                desiredWindowWidth = bounds.width();
                desiredWindowHeight = bounds.height();
            } else {
                // After addToDisplay, the frame contains the frameHint from window manager, which
                // for most windows is going to be the same size as the result of relayoutWindow.
                // Using this here allows us to avoid remeasuring after relayoutWindow
                desiredWindowWidth = frame.width();
                desiredWindowHeight = frame.height();
            }

            // ...
            host.dispatchAttachedToWindow(mAttachInfo, 0);
            mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
            dispatchApplyInsets(host);
        } else {
            desiredWindowWidth = frame.width();
            desiredWindowHeight = frame.height();
            if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
                if (DEBUG_ORIENTATION) Log.v(mTag, "View " + host + " resized to: " + frame);
                mFullRedrawNeeded = true;
                mLayoutRequested = true;
                windowSizeMayChange = true;
            }
        }

        // ...

        // Execute enqueued actions on every traversal in case a detached view enqueued an action
        getRunQueue().executeActions(mAttachInfo.mHandler);

        // ...

        boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
        if (layoutRequested) {
            if (!mFirst) {
                if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
                        || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                    windowSizeMayChange = true;

                    if (shouldUseDisplaySize(lp)) {
                        // NOTE -- system code, won't try to do compat mode.
                        Point size = new Point();
                        mDisplay.getRealSize(size);
                        desiredWindowWidth = size.x;
                        desiredWindowHeight = size.y;
                    } else {
                        final Rect bounds = getWindowBoundsInsetSystemBars();
                        desiredWindowWidth = bounds.width();
                        desiredWindowHeight = bounds.height();
                    }
                }
            }

            // 开始计算DecorView以及其子View的宽高
            windowSizeMayChange |= measureHierarchy(host, lp, mView.getContext().getResources(),
                    desiredWindowWidth, desiredWindowHeight);
        }
	}

measureHierarchy方法中又通过getRootMeasureSpec方法构建了用于约束根View的测量规格childWidthMeasureSpecchildHeightMeasureSpec,最后调用performMeasure方法开始对根View进行实际的测量工作。当通过各种情况的处理之后得到了DecorView的可用宽高之后,通过调用measureHierarchy方法从DecorView开始分发测量工作。

到这里可以看到主要方法有getRootMeasureSpecperformMeasuregetRootMeasureSpec方法后面再分析,下面先对performMeasure方法进行分析,主要梳理出测量过程的大致流程是什么样的。

java 复制代码
	// android.view.ViewRootImpl
	private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        boolean windowSizeMayChange = false;

        boolean goodMeasure = false;
        // 宽屏适配逻辑
        if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            final DisplayMetrics packageMetrics = res.getDisplayMetrics();
            res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
            int baseSize = 0;
            if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
                baseSize = (int)mTmpValue.getDimension(packageMetrics);
            }
            if (baseSize != 0 && desiredWindowWidth > baseSize) {
            	// 结合窗口可用的宽高以及窗口的布局参数来计算测量规格,用于后续子View的测量
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width, lp.privateFlags);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height, lp.privateFlags);
                // 开始具体的测量以及分发工作
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                
                if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                    goodMeasure = true;
                } else {
                    // Didn't fit in that size... try expanding a bit.
                    baseSize = (baseSize+desiredWindowWidth)/2;
                    
                    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width, lp.privateFlags);
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    
                    if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                        goodMeasure = true;
                    }
                }
            }
        }

        if (!goodMeasure) {
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width,
                    lp.privateFlags);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height,
                    lp.privateFlags);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                windowSizeMayChange = true;
            }
        }
        return windowSizeMayChange;
    }
	
	private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
        	// 调用DecorView#measure方法在View树上从根节点开始进行递归测量
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

可以看到performMeasure方法直接调用了View#measure方法来对DecorView的宽高进行测量,这里需要注意的是,View#measure是一个final方法,从方法的注释也可以看出一个View的实际测量工作是在onMeasure方法中实现的,而onMeasure方法是允许子类进行重写的,其实就是基于模版方法模式实现的。

java 复制代码
	/**
     * 此方法用于确定View应该占用多大的区域,父View在宽高上对View进行了限制。
     * 实际的测量工作交由onMeasure方法进行实现,子类可以对onMeasure进行重写来实现自定义的测量逻辑。
     * @param widthMeasureSpec 父View对当前View施加的宽度约束
     * @param heightMeasureSpec 父View对当前View施加的高度约束
     */
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        // 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);

		// 如果强制布局或者需要布局时,对View尝试进行测量
		// 	1. 是否强制布局取决于是否对View的标记位进行设置
		// 	2. 是否需要布局取决于测量规格是否发生变化以及是否为EXACTLY模式或者尺寸约束是否发生变化
        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();
			
			// 如果强制布局或者缓存中没有之前的测量结果或者忽略测量缓存,则调用onMeasure进行测量;
			// 否则,直接从缓存中取之前的测量结果,
            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
    }
    
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

onMeasure方法是protected修饰的,因此运行时的逻辑由子类是否重写决定。我们先看下DecorView中的onMeasure方法的实现,该方法是最终执行的方法。

java 复制代码
	// com.android.internal.policy.DecorView
	@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final Resources res = getContext().getResources();
        final DisplayMetrics metrics = res.getDisplayMetrics();
        final boolean isPortrait = getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;

        final int widthMode = getMode(widthMeasureSpec);
        final int heightMode = getMode(heightMeasureSpec);

        boolean fixedWidth = false;
        mApplyFloatingHorizontalInsets = false;
        // 处理窗口的布局宽度为wrap_content模式的情况,视情况重新生成widthMeasureSpec
        if (widthMode == AT_MOST) {
            final TypedValue tvw = isPortrait ? mWindow.mFixedWidthMinor : mWindow.mFixedWidthMajor;
            if (tvw != null && tvw.type != TypedValue.TYPE_NULL) {
                final int w;
                if (tvw.type == TypedValue.TYPE_DIMENSION) {
                    w = (int) tvw.getDimension(metrics);
                } else if (tvw.type == TypedValue.TYPE_FRACTION) {
                    w = (int) tvw.getFraction(metrics.widthPixels, metrics.widthPixels);
                } else {
                    w = 0;
                }
                
                final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
                if (w > 0) {
                    widthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            Math.min(w, widthSize), EXACTLY);
                    fixedWidth = true;
                } else {
                    widthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            widthSize - mFloatingInsets.left - mFloatingInsets.right,
                            AT_MOST);
                    mApplyFloatingHorizontalInsets = true;
                }
            }
        }

        mApplyFloatingVerticalInsets = false;
        // 处理窗口的布局高度为wrap_content模式的情况,视情况重新生成heightMeasureSpec
        if (heightMode == AT_MOST) {
            final TypedValue tvh = isPortrait ? mWindow.mFixedHeightMajor
                    : mWindow.mFixedHeightMinor;
            if (tvh != null && tvh.type != TypedValue.TYPE_NULL) {
                final int h;
                if (tvh.type == TypedValue.TYPE_DIMENSION) {
                    h = (int) tvh.getDimension(metrics);
                } else if (tvh.type == TypedValue.TYPE_FRACTION) {
                    h = (int) tvh.getFraction(metrics.heightPixels, metrics.heightPixels);
                } else {
                    h = 0;
                }
                
                final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
                if (h > 0) {
                    heightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            Math.min(h, heightSize), EXACTLY);
                } else if ((mWindow.getAttributes().flags & FLAG_LAYOUT_IN_SCREEN) == 0) {
                    heightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            heightSize - mFloatingInsets.top - mFloatingInsets.bottom, AT_MOST);
                    mApplyFloatingVerticalInsets = true;
                }
            }
        }
        
		// 1.调用FrameLayout的onMeasure,遍历所有子View并对其进行测量(调用子View的measure方法)
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int width = getMeasuredWidth();
        boolean measure = false;

        widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, EXACTLY);

        if (!fixedWidth && widthMode == AT_MOST) {
            final TypedValue tv = isPortrait ? mWindow.mMinWidthMinor : mWindow.mMinWidthMajor;
            final float availableWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                    res.getConfiguration().screenWidthDp, metrics);
            if (tv.type != TypedValue.TYPE_NULL) {
                final int min;
                if (tv.type == TypedValue.TYPE_DIMENSION) {
                    min = (int) tv.getDimension(metrics);
                } else if (tv.type == TypedValue.TYPE_FRACTION) {
                    min = (int) tv.getFraction(availableWidth, availableWidth);
                } else {
                    min = 0;
                }

                if (width < min) {
                    widthMeasureSpec = MeasureSpec.makeMeasureSpec(min, EXACTLY);
                    measure = true;
                }
            }
        }

        // TODO: Support height?

        if (measure) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }

可以看到DecorView#onMeasure中调用了FrameLayout#onMeasure来遍历DecorView下所有子View并对其进行测量(调用子Viewmeasure方法),这样也就开启了新一轮的测量流程。当所有子View完成测量之后,将会计算DecorView自身的宽高。

java 复制代码
	// android.widget.FrameLayout
    @Override
    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) {
            	// 测量子View,每个子View的可用宽高是一样的,因为父布局是FrameLayout
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); 
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                // 更新子View中宽度(包括自身的margin)的最大值
                maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                // 更新子View中高度(包括自身的margin)的最大值
                maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                // 如果开启对布局宽或高为match_parent模式的测量时,需要在完成所有子View测量之后,对这些match_parent的子View再次遍历测量
                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());
        }
		// 确定DecorView的测量宽高
        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);
            }
        }
    }

	// android.view.ViewGroup
    /**
     * 结合父容器的测量约束以及自身的padding和margin来测量child,必须在getChildMeasureSpec中处理布局参数为权重的情况
     *
     * @param child 待测量的子View
     * @param parentWidthMeasureSpec 父容器施加的宽度约束
     * @param widthUsed 父容器已经使用的宽度
     * @param parentHeightMeasureSpec 父容器施加的高度约束
     * @param heightUsed 父容器已经使用的高度
     */
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
		// 减去了父容器的padding以及子View的margin
        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);
    }

测量流程的整体时序图如下:

测量流程的整体流程图如下:

总结一下,View的测量过程其实就是从DecorView开始调用其measure方法,如果当前View包含子View,则在onMeasure方法中遍历所有的子View,对每一个子View调用measure方法进行测量。以此往复,一层一层遍历下去,直至所有View都完成测量。对于包含子View的容器来说,其自身的宽高在所有子View完成遍历测量之后进行确定。

其实View的测量流程与View的触摸事件分发流程有相似之处,都是采用类似深度遍历的算法进行逻辑的分发,毕竟作用的目标都是同一个View树。

测量计算

MeasureSpec(测量规格)

上面通过源码分析了测量分发的流程,其中每一次遍历父容器中的子View进行测量时,都会有MeasureSpec出现,源码注释说明了MeasureSpec是父容器对子View施加的测量约束。因此在分析具体的测量计算逻辑之前,需要了解MeasureSpec的组成以及创建过程。

MeasureSpec本身是一个工具类,通过MeasureSpec可以对测量约束进行装包和拆包,装包就是将测量规格中的尺寸和模式封装成一个int值,拆包就是将测量规格的尺寸和模式从int值中提取出来。

java 复制代码
    /**
     * 一个MeasureSpec封装了父容器传递下来的布局约束,每一个MeasureSpec代表了一个宽度或者高度的约束.
     * 一个MeasureSpec由size和mode组成. mode取值如下:
     * 1. UNSPECIFIED:父容器没有对子View施加任何限制,子View自由决定自己的尺寸。
     * 2. EXACTLY:父容器确定了子View的具体尺寸,无论子View想要占用多大的空间,最终都会受到父容器的限制。
     * 3. AT_MOST:子View决定自己的尺寸,但是不能超过父容器给定的尺寸限制。
     */
    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << 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 getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }

        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

上面源码分析的过程中涉及的MeasureSpec相关的方法主要有两个:ViewRootImpl#getRootMeasureSpecViewGroup#getChildMeasureSpec。前者用于计算施加于DecorView下的直接子View的测量约束,后者则用于计算非DecorView下的直接子View的测量约束。两者的逻辑存在部分差异,下面根据源码看下测量约束生成的具体逻辑。

java 复制代码
	/**
     * 基于窗口的布局参数为窗口中的DecorView计算其测量规格。
     *
     * @param windowSize 窗口可用的宽/高.
     * @param measurement 窗口的布局参数中的宽/高.
     * @param privateFlags 窗口的布局参数中的标记位.
     * @return 用于测量根View(DecorView)的测量规格.
     */
    private static int getRootMeasureSpec(int windowSize, int measurement, int privateFlags) {
        int measureSpec;
        final int rootDimension = (privateFlags & PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT) != 0 ? MATCH_PARENT : measurement;
        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;
    }

DecorView的测量约束的生成由窗口的尺寸和窗口的LayoutParams共同决定:

  • 布局参数为MATCH_PARENT:测量约束的size为窗口大小,测量规格的modeEXACTLY
  • 布局参数为WRAP_CONTENT:测量约束的size为窗口大小,测量规格的modeAT_MOST
  • 布局参数为固定的数值:测量约束的size为布局参数对应的具体值,测量规格的modeEXACTLY
java 复制代码
    /**
     * 负责处理measureChildren的复杂部分:计算出测量规格并传递给具体的子View。这个方法为测量具体子View的宽/高计算出正确的测量规格。
     *
     * 结合父容器施加的测量约束以及子View的布局参数来获取最合适的测量约束。例如,如果子View明确自己的宽/高或者声明了MATCH_PARENT来要求占满父容器的剩余空间,则父容器应该要求子View的尺寸为一个具体值。
     *
     * @param spec 父容器对当前View的约束
     * @param padding 已用的大小,其包括了父容器的padding以及子View的margin
     * @param childDimension 子View的布局参数
     * @return 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) {
        // 父容器约束子View的尺寸为具体值
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 子View要求占满父容器剩余的可用空间
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 子View希望自己确定大小,但是大小不能超过父容器剩余的可用空间
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父容器约束子View的尺寸不能超过父容器的剩余可用空间
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 子View想要占满父容器的剩余可用空间,但是父容器自己的尺寸也还没有确定下来.
                // 只能约束子View不能超过父容器的剩余可用空间.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 子View希望自己确定大小,但是大小不能超过父容器剩余的可用空间
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父容器对子View的尺寸没有任何约束
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                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);
    }

ViewViewViewGroup)的测量规格的计算规则如下:

  1. 如果子View的布局参数为具体值,则无论父View施加的测量规格是什么,子View的测量规格的size都是子View布局参数中指定的值,测量规格的mode都是EXACTLY
  2. 如果子View的布局参数为MATCH_PARENT,则子View的测量规格的size都是父View测量规格约束下的剩余可用空间,测量规格的mode与父View的测量规格的mode保持一致;
  3. 如果子View的布局参数为WRAP_CONTENT,则子View的测量规格的size都是父View测量规格约束下的剩余可用空间,测量规格的mode都是AT_MOST

总结一下:

  1. View如果指定自身布局参数的值为具体数值,则父View施加的测量规格对子View的测量规格没有任何影响;
  2. View如果指定自身布局参数为WRAP_CONTENT,则子View的测量规格的modeAT_MOST,子View的测量规格的size为父View的剩余可用空间;
  3. View如果指定自身布局参数为MATCH_PARENT,则子View的测量规格的mode与父View的测量规格的mode保持一致(毕竟是match_parent了嘛),子View的测量规格的size为父View的剩余可用空间(毕竟是match_parent了嘛);

需要注意的是,DecorView的测量规格的生成过程只和窗口的尺寸和布局参数有关,而子View的测量规格的生成过程则和父View的测量规格以及子View自身的布局参数有关;

最后根据测量规格对View的测量宽高进行确定,View的测量宽高取决于测量规格以及最小值。

java 复制代码
    /**
     * 对view及其内容进行测量并确定测量后的宽高,这个方法是在measure(int, int)方法中调用的,子类应该重写这个方法来提供准确高效的测量实现。
     *
     * 当重写此方法时必须调用setMeasuredDimension(int, int)来存储此View测量之后的宽高,如果没有这么做会导致measure(int, int)方法抛出IllegalStateException异常。
     *
     * 基类的实现默认是:当父View传递下来的MeasureSpec没有对View的宽高进行约束时将背景的宽高或者mMinWidth/mMinHeight作为默认的宽高,否则将父View传递下来的MeasureSpec的大小作为默认的宽高。
     *
     * 如果重写此方法,必须保证测量的宽高不能比view的最小宽高小,即必须大于等于getSuggestedMinimumWidth()和getSuggestedMinimumHeight()方法的返回值。
     *
     * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
     *                         The requirements are encoded with
     *                         {@link android.view.View.MeasureSpec}.
     * @param heightMeasureSpec vertical space requirements as imposed by the parent.
     *                         The requirements are encoded with
     *                         {@link android.view.View.MeasureSpec}.
     */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

    /**
     * 返回一个默认大小。如果MeasureSpec没有施加约束的话,则直接返回参数size,否则,返回MeasureSpec中的size。
     *
     * @param size Default size for this view
     * @param measureSpec Constraints imposed by the parent
     * @return The size this view should be.
     */
    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: // wrap_content模式的处理和match_parent以及具体值的处理是一样的。
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
    
    /**
     * 返回view应该设置的最小高度,最小高度是背景和最小高度参数两者中的最大值。
     */
    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

    }

    /**
     * 返回view应该设置的最小宽度,最小宽度是背景和最小宽度度参数两者中的最大值。
     */
    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

上面可以看到getDefaultSize方法中,对于MeasureSpec.AT_MOST的处理和MeasureSpec.EXACTLY的处理是一样的,因此,对于wrap_content来说,需要特殊处理否则将会等同于match_parent

自定义View需要解决的问题

从测量流程的分析中,总结出自定义View需要注意的事项:

  1. 自定义Viewwrap_content

    从上面的源码分析中可以得知,onMeasure的默认实现是将wrap_content等同于match_parent进行处理,因此自定义View时需要注意对这种情况的处理。

    解决办法是通过对onMeasure方法进行重写,实现对wrap_content模式的特殊处理,即判断布局参数是否为wrap_content,是的话则调用setMeasuredDimension方法并传入具体数值。

  2. 自定义Viewpadding参数

    通过上面源码的分析可以知道,容器类型的View比如FrameLayoutLinearLayout都会通过ViewGroupmeasureChildWithMargins方法将自身的padding和子Viewmargin所占用的空间作为已经使用的空间,因此传递给子View的可用空间可以完全用于子View自身内容。

    如果自定义View不是容器类型的View,则只需要在绘制时候考虑自身的padding即可。

    如果自定义View是容器类型的View,则需要在onMeasure方法中处理自身的padding和子Viewmargin

确定View的位置(onLayout)

布局流程用于确定View在其父容器中的位置,即View的成员变量mLeftmTopmRight以及mBottom的值。完成布局之后,View最终的宽和高也就随之确定了,即View的成员变量mWidthmHeight

从上面的源码中可以看到,performTraversals方法中在完成测量过程之后会调用performLayout方法开始进行布局,通过布局来对这个界面中所有的View的位置进行确定。performLayout方法中直接调用了DecorView#layout开启了布局过程。由于DecorView及其父类FrameLayout都没有重写layout方法,因此调用的是View#layout方法。

java 复制代码
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        // ... 
		try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
			// ...
		} finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        mInLayout = false;
	}

View#layout中调用setFrame方法将View的四个坐标确定之后,接着调用onLayout来对内部的子View进行布局的分发。一般来说,参数r为参数lView的测量宽度的和,参数b等于参数t加上View的测量高度。

java 复制代码
    /**
     * Assign a size and position to a view and all of its
     * descendants
     *
     * <p>This is the second phase of the layout mechanism.
     * (The first is measuring). In this phase, each parent calls
     * layout on all of its children to position them.
     * This is typically done using the child measurements
     * that were stored in the measure pass().</p>
     *
     * <p>Derived classes should not override this method.
     * Derived classes with children should override
     * onLayout. In that method, they should
     * call layout on each of their children.</p>
     *
     * @param l Left position, relative to parent
     * @param t Top position, relative to parent
     * @param r Right position, relative to parent
     * @param b Bottom position, relative to parent
     */
    @SuppressWarnings({"unchecked"})
    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);
            // ...
        }
		
		// ...
    }

    /**
     * Assign a size and position to this view.
     *
     * This is called from layout.
     *
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     * @return true if the new size and position are different than the
     *         previous ones
     * {@hide}
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            invalidate(sizeChanged);

            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

            mPrivateFlags |= PFLAG_HAS_BOUNDS;
			// ...
        }
        return changed;
    }

View#onLayout是空实现,从注释可以看出这个方法是给自定义View进行重写的,注释说明了这个方法中需要对根据需要对各个子View进行布局处理。

java 复制代码
    /**
     * Called from layout when this view should
     * assign a size and position to each of its children.
     *
     * Derived classes with children should override
     * this method and call layout on each of
     * their children.
     * @param changed This is a new size or position for this view
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     */
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

可以看下FrameLayout中的onLayout的实现,并根据布局的特性进行了特殊处理,即结合布局的特点计算每个子View的实际位置。

java 复制代码
	@Override
    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);
            }
        }
    }

总结来看,布局流程相比测量流程要简单很多,对于容器类型的自定义View需要根据布局的特点计算各个子View的位置,并将布局分发给各个子View即可。

结语

到这里,我们从源码角度对每一次VSYNC信号到达之后的测量布局流程进行了梳理和理解,总结起来就是,测量是用于确定View的宽高,布局是为了确定View在屏幕上展示的位置,而测量和布局都需要在View树进行分发处理,从而将所有View最终在屏幕上展示的位置以及大小确定下来。但是,仅仅完成测量和布局,用户还是不能看到视图元素,还需要对View的绘制流程进行处理,最终将生成的视图数据交由屏幕进行刷新之后才能被用户看见。

对于View的绘制,Android不同版本的实现也不同,分为软件绘制和硬件绘制,后面将会结合源码对View的绘制进行梳理分析。

相关推荐
liangmou212125 分钟前
解释小部分分WPI函数(由贪吃蛇游戏拓展)
android·游戏·c#
亚瑟-灰太狼1 小时前
memory泄露分析方法(Binder,Window,View篇)
android
l and2 小时前
Git 行尾换行符,导致无法进入游戏
android·git
程序媛小果2 小时前
基于Django+python的Python在线自主评测系统设计与实现
android·python·django
梁同学与Android3 小时前
Android --- 在AIDL进程间通信中,为什么使用RemoteCallbackList 代替 ArrayList?
android
Frank_HarmonyOS5 小时前
【无标题】Android消息机制
android
凯文的内存7 小时前
Android14 OTA升级速度过慢问题解决方案
android·ota·update engine·系统升级·virtual ab
VinRichard7 小时前
Android 常用三方库
android
Aileen_0v08 小时前
【玩转OCR | 腾讯云智能结构化OCR在图像增强与发票识别中的应用实践】
android·java·人工智能·云计算·ocr·腾讯云·玩转腾讯云ocr
江上清风山间明月11 小时前
Flutter DragTarget拖拽控件详解
android·flutter·ios·拖拽·dragtarget