专栏名称: 俞其荣
向前跑 迎着冷眼和嘲笑 与 http://yuqirong.me 保持同步更新
目录
相关文章推荐
TomXu  ·  想起来字节某高管早年OKR里提到要mappi ... ·  19 小时前  
TomXu  ·  今天潘乱给我展示了 NotebookLM ... ·  昨天  
亚马逊全球开店  ·  客单价拉高100美金,退货率反降40%!他靠 ... ·  2 天前  
51好读  ›  专栏  ›  俞其荣

View的工作原理

俞其荣  · 简书  ·  · 2017-09-20 20:32

正文

请到「今天看啥」查看全文


measurespec

在这里,我们小结一下。对于 DecorView 来说,其 MeasureSpec 是由窗口的大小和自身的 LayoutParams 来共同决定的;而对于普通的 View 来说,其 MeasureSpec 是由父容器的 MeasureSpec 和自身的 LayoutParams 共同决定的。

measure过程

ViewRootImpl

performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec)

分析 measure 过程,我们的起点就是在 ViewRootImpl performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) 方法中:

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            // 进行测量
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

performMeasure 中调用了 measure 方法。说到底,DecorView 只是一个所以我们又要进入 View 类中去看下。

View

measure(int widthMeasureSpec, int heightMeasureSpec)

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...

        if (forceLayout || needsLayout) {
            ...
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                // 调用 onMeasure
                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;
            }

            ...
    }

View measure 方法内部是调用了 onMeasure 。所以我们还要接着跟进到 onMeasure 中才行。另外, measure 方法是用 final 修饰的,所以子类是无法进行重写的。

FrameLayout

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

这里小提一下,我们都知道 DecorView 其实是一个 FrameLayout ,所以 onMeasure 应该在 FrameLayout 中去看:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        // 判断当前 framelayout 布局的宽高是否至少一个是 match_parent 或者精确值 ,如果是则置 measureMatchParent 为 false .
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        // 遍历不为 GONE 的子 view
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                // 对每一个子 View 进行测量
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                // 寻找子 View 中宽高的最大者,因为如果 FrameLayout 是 wrap_content 属性
                // 那么它的宽高取决于子 View 中的宽高最大者
                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());
                // 如果 FrameLayout 为 wrap_content 且 子 view 的宽或高为 match_parent ,那么就添加到 mMatchParentChildren 中
                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));

        // 子View中设置为match_parent的个数
        count = mMatchParentChildren.size();
        // 若 FrameLayout 为 wrap_content 且 count > 1
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                // 如果子 View 的宽度是 match_parent 属性,那么对 childWidthMeasureSpec 修改:
                // 把 widthMeasureSpec 的宽度修改为:framelayout总宽度 - padding - margin,模式设置为 EXACTLY
                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);
                }
                //对于这部分的子 View 需要重新进行 measure 过程
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

如果上面 FrameLayout onMeasure 流程没看懂的话也没关系。其实总的来说重要的就只有遍历 child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 这个方法,这是将父容器的 measure 过程传递到子 View 中。

ViewGroup

measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)

可能有些人也有疑问,在上面 measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0) 后也没看到有 child.measure 的方法啊,这是因为在 measureChildWithMargins 中内部调用了 child.measure

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        // getChildMeasureSpec 我们上面分析过了
        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);
        // measure 传递给子 View
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

这下明白了吧?父容器就是遍历调用了 child.measure 这个方法将 measure 过程传递给每一个子 View 的。虽然不同的父容器 onMeasure 方法都不一样,但是相同的是,他们都会遍历调用 child.measure

View

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

上面我们也讲过, measure 方法内部其实是调用了 onMeasure ,所以子 View 被父容器调用了 measure 后,也会调用属于自己的 onMeasure 方法。那么我们就直接看向 View onMeasure 方法:

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

onMeasure 方法只有一句代码,所以重点就是 getDefaultSize(int size, int measureSpec) 咯。

getSuggestedMinimumWidth() 内部逻辑:

  1. 若没有设置背景,就是 android:minWidth 的值;
  2. 若有设置背景,就是 max(android:minWidth, 背景 Drawable 的原始宽度)

getSuggestedMinimumHeight() 也是同理。

getDefaultSize(int size, int measureSpec)

    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;
        // 直接返回 specSize
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

从上面我们可以看到:

  • 若是 UNSPECIFIED ,则直接返回的就是 getSuggestedMinimumWidth/getSuggestedMinimumHeight 的值;
  • 若是 AT_MOST/EXACTLY ,直接用的就是 specSize 。

而根据我们之前总结出来的表可知,只要 view 不指定固定大小,那么无论是 AT_MOST 还是 EXACTLY ,都是按照 parentSize 来的。

这也是为什么我们在自定义 View 时,如果不重写 onMeasure(int widthMeasureSpec, int heightMeasureSpec) ,wrap_content 和 match_parent 效果一样的原因。







请到「今天看啥」查看全文


推荐文章
观察者网  ·  工信部否认要禁止个人VPN业务
7 年前