之前遇到一个问题,想要实现一个子view超出父view的效果,用了clipChildren属性,但是它不生效。
写了demo改了一些条件,发现子view的宽度固定的时候,clipChildren才会生效,子View的宽能够超出父View,wrap_content和match_parent时就不行。
好像可以理解,wrap_content和match_parent时测量的结果都和父view的宽的有关,测量的时候子view的宽度就限定不能超出父view,绘制出来的子view怎么可能超出呢。
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 |
理解了为什么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 {
...
}
}
...
}
}
使用clipChilren,子view必须设置一个固定的size