浅析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的绘制进行梳理分析。

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui