clipChildren属性和MeasureSpec

clipChildren属性和MeasureSpec

1、背景

之前遇到一个问题,想要实现一个子view超出父view的效果,用了clipChildren属性,但是它不生效。

2、分析

写了demo改了一些条件,发现子view的宽度固定的时候,clipChildren才会生效,子View的宽能够超出父View,wrap_content和match_parent时就不行。
好像可以理解,wrap_content和match_parent时测量的结果都和父view的宽的有关,测量的时候子view的宽度就限定不能超出父view,绘制出来的子view怎么可能超出呢。

3、源码

3.1 measure

measure过程中有一个非常重要的类MeasureSpec:

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        /**
         * Creates a measure specification based on the supplied size and mode.
         *
         * The mode must always be one of the following:
         * 
    *
  • {@link android.view.View.MeasureSpec#UNSPECIFIED}
  • *
  • {@link android.view.View.MeasureSpec#EXACTLY}
  • *
  • {@link android.view.View.MeasureSpec#AT_MOST}
  • *
* *

Note: On API level 17 and lower, makeMeasureSpec's * implementation was such that the order of arguments did not matter * and overflow in either value could impact the resulting MeasureSpec. * {@link android.widget.RelativeLayout} was affected by this bug. * Apps targeting API levels greater than 17 will get the fixed, more strict * behavior.

* * @param size the size of the measure specification * @param mode the mode of the measure specification * @return the measure specification based on size and mode */
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); } } /** * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED * will automatically get a size of 0. Older apps expect this. * * @hide internal use only for compatibility with system widgets and older apps */ @UnsupportedAppUsage public static int makeSafeMeasureSpec(int size, int mode) { if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) { return 0; } return makeMeasureSpec(size, mode); } /** * Extracts the mode from the supplied measure specification. * * @param measureSpec the measure specification to extract the mode from * @return {@link android.view.View.MeasureSpec#UNSPECIFIED}, * {@link android.view.View.MeasureSpec#AT_MOST} or * {@link android.view.View.MeasureSpec#EXACTLY} */ @MeasureSpecMode public static int getMode(int measureSpec) { //noinspection ResourceType return (measureSpec & MODE_MASK); } /** * Extracts the size from the supplied measure specification. * * @param measureSpec the measure specification to extract the size from * @return the size in pixels defined in the supplied measure specification */ public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } static int adjust(int measureSpec, int delta) { final int mode = getMode(measureSpec); int size = getSize(measureSpec); if (mode == UNSPECIFIED) { // No need to adjust size for UNSPECIFIED mode. return makeMeasureSpec(size, UNSPECIFIED); } size += delta; if (size < 0) { Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size + ") spec: " + toString(measureSpec) + " delta: " + delta); size = 0; } return makeMeasureSpec(size, mode); } /** * Returns a String representation of the specified measure * specification. * * @param measureSpec the measure specification to convert to a String * @return a String with the following format: "MeasureSpec: MODE SIZE" */ public static String toString(int measureSpec) { int mode = getMode(measureSpec); int size = getSize(measureSpec); StringBuilder sb = new StringBuilder("MeasureSpec: "); if (mode == UNSPECIFIED) sb.append("UNSPECIFIED "); else if (mode == EXACTLY) sb.append("EXACTLY "); else if (mode == AT_MOST) sb.append("AT_MOST "); else sb.append(mode).append(" "); sb.append(size); return sb.toString(); } }

分别有3中模式:UNSPECIFIED、EXACTLY、AT_MOST,这三种模式可以用两个bit来表示,分别是00,01,10。
因为int型是32位,所以,可以用高两位表示mode,剩下的30位表示size。getSize和getMode方法乍一看有点摸不着头脑,但是用二进的思路再去梳理一遍就很容易可以理解了。

再看看测量过程中是如何使用这个类的。
这里以LinearLayout的垂直布局为例:

    // 入参是它的父类传递进来的
    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        mTotalLength = 0;

        // 父类的mode
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        // 遍历子view
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            ...
            nonSkippedChildCount++;
            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerHeight;
            }

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            totalWeight += lp.weight;

            // 子view根据weight设置高度
            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                // viewGroup高度固定+子view的weight已经可以确定每个子view的高度了,不需要再去测量
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {

                // 已经用掉的高度
                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);
            }
        }
    }

measureChildBeforeLayout会调用到measureChildWithMargins,可以理解成是把parentMeasureSpec转换成childMeasuresSpec的过程:

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

getChildMeasureSpec是这样的(为了便于理解,换了一下参数名):

    public static int getChildMeasureSpec(int parentSpec, int widthUsed, int childDimension) {
        int parentSpecMode = MeasureSpec.getMode(parentSpec);
        int parentSpecSize = MeasureSpec.getSize(parentSpec);

        // 子view可用size
        int size = Math.max(0, parentSpecSize - widthUsed);

        int resultSize = 0;
        int resultMode = 0;

        switch (parentSpecMode) {
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 子view的size不能超过父view提供给它的size
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

可以看到,除了父view是UNSPECIFIED的情况,在子view设置为match_parent或者wrap_content的时候,子view的宽度都不能超过父view给它提供的宽度。也就支持了第2点的分析。

这段代码里我们也可以总结出来父view和子view的MeasureSpec的一些关系:

子View Size/ 父级SpecMode EXACTLY AT_MOST UNSPECIFIED
具体尺寸设置 EXACTLY EXACTLY EXACTLY
match_parent EXACTLY AT_MOST UNSPECIFIED
wrap_content AT_MOST AT_MOST UNSPECIFIED

3.2 ClipChildren

理解了为什么clipChildren不生效,现在来看看这个属性是如何生效的。
其实也就是父view没有将画布裁剪了,子view可以在画布上完整绘制出来。

#View.java
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        if (!drawingWithRenderNode) {
            //若是父布局需要裁剪子布局,也就是说clipChildren==true,那么就需要对canvas进行裁剪
            if ((parentFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0 && cache == null) {
                if (offsetForScroll) {
                    //裁剪canvas与子布局大小一致
                    //sx,sy 是scroll值,没设置scroll时sx,sy都为0
                    canvas.clipRect(sx, sy, sx + getWidth(), sy + getHeight());
                } else {
                    ...
                }
            }
            ...
        }
    }

4、 总结

使用clipChilren,子view必须设置一个固定的size

你可能感兴趣的:(安卓基础,安卓踩坑合集,android,java,android,studio)