Android 源码分析二 View 测量

第一篇说完 View 建立,接着讲讲 View 的测量和布局。先讲讲总体思想,View 的 测量是自上而下,一层一层进行。涉及到的核心方法就是 View 中的 measure() layout() 对于咱们来讲,更应该关心的就是 onMeasure()onLayout() 的回调方法。本文着重关注测量相关代码,至于 layout ,这个是 ViewGroup 的具体逻辑。android

onMeasure

说到 onMeasure() 方法就必须提一嘴它涉及到的测量模式。以及模式对子 view 的约束。程序员

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

这是 ViewonMeasure() 方法默认实现,这里又涉及到三个重要的方法, setMeasuredDimension()getDefaultSize()setMeasuredDimension() 这个方法很是重要,它是咱们设置测量宽高值的官方惟一指定方法。也是咱们在 onMeasure() 方法中必须调用的方法。若是你想了下,本身彷佛在 onMeasre() 没有手动调用过该方法,而且也没有啥异常,不要犹豫,你必定是调用了 super.onMeasure() ,setMeasuredDimension()最终会完成对 measureHeightmeasureWidth 赋值,具体操做往下看。markdown

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
复制代码

setMeasuredDimension() 中调用私有的 setMeasuredDimensionRaw() 方法完成对 mMeasuredWidthmMeasuredHeight 赋值,而后更新 flag 。app

getSuggestedMinimumWidth/Height

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

}
复制代码

这两个方法的默认实现就是去获取 View 设置的背景和最小值中最小的那个。背景设置就不用说了,至于这个宽高最小值,其实就是经过 xml 中 minWidth 或者 API 动态设置。ide

getDefaultSize()

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}
复制代码

这个方法也比较重要,由于它涉及到测量模式。先分析下参数,输入的第一个 size 是刚刚获取的最小值。第二个就是父布局回调过来的测量参数。布局

经过上面能够看到,测量模式一共有三种。MeasureSpec.UNSPECIFIED MeasureSpec.AT_MOST MeasureSpec.EXACTLYpost

若是是 MeasureSpec.UNSPECIFIED ,那么就直接使用获取的最小值。若是是其余两种模式,那么就从测量参数中获取对应的 size。注意,在这个方法中,根本没有对 AT_MOST 和 EXACTLY 作区分处理。spa

MeasureSpec 测量模式和size

经过上面 getDefaultSize() 方法咱们已经看到 MeasureSpec 中包含有测量模式和对应 size。那么它是怎么作到一个 int 类型,表示两种信息呢?程序员的小巧思上线。code

一个 int 类型,32位。这里就是使用了高2位来表示测量模式,低 30 位用来记录 size。orm

//左移常量 shift 有转变的意思 并且在 Kotlin 中 左移使用 shl() 表示
private static final int MODE_SHIFT = 30;
//二进制就是这样11000...000
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
//00 000...000
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//01 000...000
public static final int EXACTLY     = 1 << MODE_SHIFT;
//10 000...000
public static final int AT_MOST     = 2 << MODE_SHIFT;  
复制代码

接着看看是怎么赋值和取值的呢。

public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                    @MeasureSpecMode int mode) {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
复制代码

看着是否是比较高大上?都说了这是程序员小巧思,代码固然比较溜。这里涉及到与或非三种运算。直接举个例子吧,好比我要建立一个 size 为 16 模式是 EXACTLY 的 MeasureSpec 那么就是这样的。

size    对应   00 000... 1111
    mode    对应   01 000... 0000
    mask    对应   11 000... 0000
    ~mask   对应   00 111... 1111
    size & ~mask  00 000... 1111 = size
    mode & mask   01 000... 0000 = mode
    size | mode   01 000... 1111 = 最终结果
复制代码

经过这么一对应,结果很是清晰,有没有以为 makeMeasureSpec() 方法中前两次 & 操做都是很无效的?其实它能保证 mode 和 size 不越界,不会互相污染。反正你也别瞎传值。赋值时,方法上已经对两个参数都有输入限制。

再说完三种模式定义以后,接着就须要考虑 xml 中的 宽高指定最后是怎么转换为对应的 模式。好比说,咱们写 wrap_content, 那么对应的测量模式究竟是怎样的呢?

举个例子,好比说以下的一个布局。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/parent_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    android:background="@color/colorAccent">

    <ProgressBar
        android:id="@+id/child"
        android:layout_width="20dp"
        android:layout_height="20dp"
        android:layout_gravity="center"
        android:indeterminate="true"
        android:indeterminateTint="@color/colorPrimary"
        android:indeterminateTintMode="src_in"/>

</FrameLayout>
复制代码

效果经过预览就能看到,FrameLayout 占据全屏,ProgressBar 居中显示,size 就是 20 dp 。

ProgressBaronMeasure() 方法以下:

@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int dw = 0;
    int dh = 0;

    final Drawable d = mCurrentDrawable;
    if (d != null) {
        dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
        dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
    }

    updateDrawableState();

    dw += mPaddingLeft + mPaddingRight;
    dh += mPaddingTop + mPaddingBottom;

    final int measuredWidth = resolveSizeAndState(dw, widthMeasureSpec, 0);
    final int measuredHeight = resolveSizeAndState(dh, heightMeasureSpec, 0);
    setMeasuredDimension(measuredWidth, measuredHeight);
}
复制代码

能够看到,ProgressBar 复写了 View 的 onMeasure() 方法,而且没有调用 super 。因此,最上面那一套分析对于它无效。所以,它也本身在最后调用了 setMeasuredDimension() 方法完成一次测量。在这里,又涉及到一个 View 的静态方法 -- resolveSizeAndState()

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}
复制代码

入参 size 是背景大小,MeasureSpeconMeasure() 方法传入,参数由 parent 指定。 state 的问题先不考虑,咱们这里主要看 size 。对比刚刚说过的 getDefaultSize() , 这个方法已经将 AT_MOSTEXACTLY 作了区分处理,一共又四种状况。

AT_MOST 下,若是测量值小于背景大小,即 View 须要的 size 比 parent 能给的最大值还要大。这个时候仍是设置为 测量值,而且加入了 MEASURED_STATE_TOO_SMALL 这个状态。若是测量值大于背景大小,正常状况也就是这样,这时候就设置为背景大小。EXACTLY 下,那就是测量值。UNSPECIFIED 下,就是背景 size。

###数值传递

上面其实都是说的是 ViewonMeasure 中测量本身的状况,可是,parent 传入的 MeasureSpec 参数究竟是怎么确认的呢?child 设置 match_parent 或者 wrap_content 或者 精确值,会影响对应的 MeasureSpec 的模式和 size 吗?

带着这些问题,咱们看看 FrameLayoutonMeasure() 方法的部分实现。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    // 若是本身的宽高有一个不是精确值,measureMatchParentChildren flag 就 为 true
    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) {
            // 经过本身的 MeasureSpec 测量child
            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());
            // 若是 child 是 match_parent 可是 本身又不是一个精确值,那就要从新再次测量
            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 肯定本身的size
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
    // 最后,以前有指定 match_parent 的 child 须要根据最新的宽高值进行再次测量
    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);
                // match_parent 的状态更改成 精确值
                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                        width, MeasureSpec.EXACTLY);
            } else {
                // 其余状况 getChildMeasureSpec() 从新肯定 MeasureSpec
                childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                        lp.leftMargin + lp.rightMargin,
                        lp.width);
            }
            // 肯定高度代码同上,省略
            ...

            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}
复制代码

在第一次测量 child 时,调用了 measureChildWithMargins() 方法,该方法中,最后会调用 getChildMeasureSpec() 方法,在第二次确认宽高时,也是经过这个方法肯定相关的 MeasureSpec 。 能够看出,**getChildMeasureSpec() 是一个很是重要的静态方法。**它的做用是根据 parent 的相关参数 和 child 的相关参数,肯定 child 相关的 MeasureSpec 生成。在这里,三种测量模式和 xml 中的 match_parent wrap_content 或者 具体值 在这里产生关联。

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

代码是这样的,为便于理解,制做了如下这个表格,能够对号入座。

Parent(pSize)
------
Child(size)
EXACTLY AT_MOST UNSPECIFIED
EXACTLY EXACTLY (size) EXACTLY (size) EXACTLY (size)
MATCH_PARENT EXACTLY (pSize) AT_MOST (pSize) UNSPECIFIED (pSize)
WRAP_CONTENT AT_MOST (pSize) AT_MOST (pSize) UNSPECIFIED (pSize)

经过这个方法,就生成了最后用于测量 child 的相关 MeasureSpec 。接着就能够调用 child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 让 child 开始测量本身,最后就会回调到 child 的 onMeasure() 方法中。

上面这个布局,若是直接 setContentView() 加载的话,那么在 FrameLayout 中,FrameLayoutMeasureSpecEXACTLY + pSize 这种状况。

###LayoutParameter 特征类

上面的宽高信息是从 LayoutParameter 这个类中取出来的。 这个类能够说是至关重要,没有它的话,咱们写的 xml 相关属性就没法转化为对应的代码。在这里继续抛出一个问题,在 LinearLayout 布局中咱们能够直接使用 layout_weight 属性,可是若是改成 FrameLayout 以后,这个属性就会没效果;同时,FrameLayout 中定义的 gravity 属性,在 LinearLayout 中也没有效果。为何呢?代码层面到底实现的呢?

这就是 LayoutParams 的做用,LayoutParameter 定义在 ViewGroup 中,是最顶级,它有不少子类,第一个子类就是 MarginLayoutParams ,其余具体实现跟着具体的 ViewGroup ,好比说 FrameLayout.LayoutParameter LinearLayout.LayoutParameter 或者 RecyclerView.LayoutParameter

ViewGroup 中定义了生成 LayoutParams 的方法 generateLayoutParams(AttributeSet attrs)

// ViewGroup
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}

//ViewGroup.LayoutParams
public LayoutParams(Context c, AttributeSet attrs) {
    TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
    setBaseAttributes(a,
            R.styleable.ViewGroup_Layout_layout_width,
            R.styleable.ViewGroup_Layout_layout_height);
    a.recycle();
}

//ViewGroup.LayoutParams
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
    width = a.getLayoutDimension(widthAttr, "layout_width");
    height = a.getLayoutDimension(heightAttr, "layout_height");
}
复制代码

经过上面的代码,全部的 ViewGroup 都有 generateLayoutParams() 的能力。在默认的 ViewGroup 中,它只关心最基础的宽高两个参数。接着对比 FrameLayoutLinearLayout, 看看相关方法。

//FrameLayout.LayoutParams
public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
    super(c, attrs);

    final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FrameLayout_Layout);
    gravity = a.getInt(R.styleable.FrameLayout_Layout_layout_gravity, UNSPECIFIED_GRAVITY);
    a.recycle();
}
//LinearLayout.LayoutParams
public LayoutParams(Context c, AttributeSet attrs) {
    super(c, attrs);
    TypedArray a =
            c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);

    weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
    gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);

    a.recycle();
}
复制代码

能够看到,在 FrameLayout 中 额外解析了 gravity ,在 LinearLayout 中 额外解析了 weightgravity

视图异常缘由

回到上篇文章 View 的建立过程当中的 Layoutinflater.inflate() 方法。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        ...
        try {
            ...
                // Temp is the root view that was found in the xml
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                if (root != null) {
                    // Create layout params that match root, if supplied
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        temp.setLayoutParams(params);
                    }
                }
               ...
            }
    }
}
复制代码

这里有一个大坑须要填一下。LayoutInflater.inflate() 方法中,须要咱们指定 parent ,若是不指定,会出现啥状况呢,就是 LayoutParams 没有被建立出来。最后在 addView() 方法中:

public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    // inflate 的时候并无生成相关 LayoutParams
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        // 没有生成的话,就建立一个 default LayoutParams
        params = generateDefaultLayoutParams();
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
复制代码

默认的 LayoutParams 只会设置 宽高信息,至于刚刚说的 gravity weight 这些属性就被丢弃,若是你 inflate() 的顶层布局真的带有这些属性,很差意思,就这样丢失了。 这也是有人抱怨本身 inflate 布局时,布局样式异常的一个重要缘由。要避免这个问题,就要作到 inflate 时必定要传入对应的 parent 。不要有 inflate(R.layout.xx,null) 这种写法,并且这种写法,目前 Studio 会直接警告你。

inflate 的场景其实不太多,在 Fragment 或者 建立 ViewHolder 时,系统都会将对应的 parent 传给你,这个好解决。可是在使用 WindowManager.addView() PopupWindow Dialog 时,可能很差找到对应的 parent。这时候咋办呢?这个时候能够拿 window.decorView 或者,你直接 new 一个具体的 ViewGroup 都行。

到这里,关于 LayoutParams 彷佛就说完了。 inflate() 这个大 bug 彷佛也解决了。

Dialog 视图异常

建立过 Dialog 或者 DialogFragment 的小伙伴都清楚,Dialog 布局中,你写 match_parent 是没有效果,结果老是 wrap_content 的样子。经过上面一波分析,一开始我觉得是 inflate 那个错误,而后,即便我指定上对应的 parent ,想固然觉得布局能够符合预期。结果仍是老样子。

为何会这样呢?

这又要回到刚刚上面 getChildMeasureSpec() 方法和表格中。咱们每次写 match_parent 时,默认 parent 是什么 size 呢?固然想固然就是屏幕宽高那种 size。 在 Dialog 中,会建立对应的 PhoneWindowPhoneWindow 中有对应的 DecorViewDecorView 并非直接添加咱们布局的根 View,这里还有一个 mContentParent ,这才是展示咱们添加 View 的直接领导,老爹。在 PhoneWindow 中建立 mContentParent 时,有这么一个判断。

protected ViewGroup generateLayout(DecorView decor) {
    if (mIsFloating) {
        setLayout(WRAP_CONTENT, WRAP_CONTENT);
        setFlags(0, flagsToUpdate);
    }
}
复制代码

而咱们使用各类样式的 Dialog 时,其实会加载默认的 style ,最基本的 dialog style 中,分明写了这么一个默认属性。

<style name="Base.V7.Theme.AppCompat.Light.Dialog" parent="Base.Theme.AppCompat.Light">
    ...
    <item name="android:windowIsFloating">true</item>
    ...
</style>
复制代码

这两个代码放一块,问题开始转化。当 parent(decorView) 为 精确值,child(mContentParent) 为 wrap_content 时,最后在 child 中对应的 MeasureSpec 是什么样呢? 查上面的表就知道,这个时候的 child measureSpec 应该是 AT_MOST + pSize 。 当 parent (mContentParent) 为 AT_MOST ,child (填充布局) 为 match_parent 时,最后 child 中对应的 MeasureSpec 是什么样呢? 继续查表,显然,这里也是 AT_MOST + pSize 这种状况。注意,这里就和上面第一次分析的 EXACTLY + pSize 不同了。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/parent_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    android:background="@color/colorAccent"
    android:clipChildren="false">

    <ProgressBar
        android:id="@+id/progressbar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:indeterminate="true"
        android:indeterminateTint="@color/colorPrimary"
        android:indeterminateTintMode="src_in"/>

</FrameLayout>
复制代码

假设在 Dialog 中咱们就填充如上布局。结合上面 FrameLayout 分析, child 的 size 要再次测量。关键在 FrameLayout onMeasure() 方法最后的 setMeasuredDimension()方法中会调用 resolveSizeAndState()

setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
        resolveSizeAndState(maxHeight, heightMeasureSpec,
                childState << MEASURED_HEIGHT_STATE_SHIFT));

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}
复制代码

第一次是 EXACTLY ,因此就是 pSize 。 这一次是 AT_MOST ,因此就成了 childSize 。那最后效果其实就是 wrap_content 。到这里 Dialog 显示异常从代码上分析完成。那么须要怎么解决呢? 首先能够从根源上,将 windowIsFloating 设置为 false 。

//styles.xml
<style name="AppTheme.AppCompat.Dialog.Alert.NoFloating" parent="Theme.AppCompat.Light.Dialog.Alert">
    <item name="android:windowIsFloating">false</item>
</style>
//DialogFragment
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setStyle(android.support.v4.app.DialogFragment.STYLE_NO_TITLE, R.style.AppTheme_AppCompat_Dialog_Alert)
}
复制代码

退而求其次,既然它默认设置为 wrap_content ,那么咱们能够直接设置回来啊。

//DialogFragment
override fun onStart() {
    super.onStart()
    dialog?.window?.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
}
复制代码

到这里,咱们也能回答一个问题,若是 parent 指定为 wrap_content 。child 指定为 match_parent 那么最后,child 到底有多大? 这个其实就是上面这个问题,若是要回答得简单,那么就是它就是 View 本身的 最小值。

要详细说的话,若是 View 没有复写 onMeasure() 方法,那就是默认 onMeasure() 方法中 getDefaultSize() 的返回值,就是 pSize 。

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}
复制代码

若是是其余控件,好比说刚刚说的 ProgressBar,其实就是 resolveSizeAndState() 或者测量出来的最小值。 咱们自定义 View 时视图预览发现它总会填充父布局,缘由就是你没有复写 onMeasure() 方法。还有就是在写布局时,尽可能避免 parent 是 wrap_content , child 又是 match_parent 的状况,这样 parent 会重复测量,形成没必要要的开销。

总结

View 的测量是一个博弈的过程,最核心方法就是 setMeasuredDimension(),具体值则须要 parent 和 child 相互协商。数值的传递和肯定依赖于 MeasureSpecLayoutParams,填充布局时 inflate() 方法 root 参数不要给空,这样会致使填充布局一些参数丢失,Dialog 老是 wrap_content ,这是由于默认带有 windowIsFloating 的属性 。

相关文章
相关标签/搜索