为什么HorizontalScrollView嵌套一个子View,他的match_parent属性会失效
这个问题的出现是在写一个需求时候,示例图如下
demo链接 https://github.com/nbwzlyd/TabLayoutDemo
当然这个问题的解决方案很简单,没什么难度。下面记录一下原因。
- 问题的抛出
我们在开发的过程中,经常会用到HorizontalScrollView,通常情况下我们还需要在里面嵌套一个ViewGroup,并将其设置为match_parent,但是这个属性一定是无效的。我们需要重新设置
fillViewport属性为true。这样才会得到我们要的效果。
示例图:
fillViewport 为false的情况下:
fillViewport 为true的情况下:
一个简单的属性就可以让我们需要的效果起死回生。这个问题的解决不难,和ScrollView的解决方案完全一致。
秉承知其然并知其所以然的态度,我们来看看为什么这个属性设置了会得到我们的效果,但是我们今天逆向处理,我们来看看为什么默认情况下,我们得不到我们想要的效果。
解决思路
1.查看源码,看onMeasure中是不是特殊处理了。简单粗暴
我们假设你知道view的大概测量流程,测量方法在onMeasure方法中,所以我们直捣黄龙,看源码中onMeasure方法中的处理:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!mFillViewport) {
return;
}
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
if (widthMode == MeasureSpec.UNSPECIFIED) {
return;
}
if (getChildCount() > 0) {
final View child = getChildAt(0);
final int widthPadding;
final int heightPadding;
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
if (targetSdkVersion >= Build.VERSION_CODES.M) {
widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
} else {
widthPadding = mPaddingLeft + mPaddingRight;
heightPadding = mPaddingTop + mPaddingBottom;
}
int desiredWidth = getMeasuredWidth() - widthPadding;
if (child.getMeasuredWidth() < desiredWidth) {
final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
desiredWidth, MeasureSpec.EXACTLY);
final int childHeightMeasureSpec = getChildMeasureSpec(
heightMeasureSpec, heightPadding, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
代码很简单,在fillViewport为false的情况下,onMeasure方法没有对view的测量操作做任何的干预操作,直接调用的super.onMeasure();
难道是因为父View的测量工作并没有测量出子View的准确需要大小?我们看一眼父View。
public class HorizontalScrollView extends FrameLayout {......}
父View是FrameLayout。
emmm,不可能是父View测量工作出了问题。因为我们经常用这个控件,假设是父View的测量工作出错,虽然能解释通HorizontalScrollView导致子view的match_parent失效问题,但是这样的话我们用frameLayout控件就需要每次设置xx属性了,但是我们并没有这么设置过。(事实上这个时候我又把FrameLayout的onMeasure方法又看了一遍,冏,还debug了好几遍)。
- 思路2 widthMeasureSpec ,heightMeasureSpec
咨询了群里的大牛,有赞大佬馍哥。给了一个思路,看一下父View的widthMeasureSpec ,heightMeasureSpec 包括mode,包括size。这个思路我一开始没想到。我们知道,子view的大小以及测量模式是受自己父View大小影响的。很有可能是传给我们的这两个属性在HSV 和FL中不同,于是按照这个思路,我们来看看。
定义一个类,继承FL
public class CustomLayout extends FrameLayout {
private static final String TAG="CustomLayout";
public CustomLayout(Context context) {super(context);}
public CustomLayout(Context context, AttributeSet attrs) {super(context, attrs);}
public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, "onMeasure: "+(MeasureSpec.getMode(widthMeasureSpec)==MeasureSpec.EXACTLY)
+" size=="+MeasureSpec.getSize(widthMeasureSpec));
}
}
布局文件
<?xml version="1.0" encoding="utf-8"?>
<wiget.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:background="@color/colorAccent"
android:gravity="center"
android:text="1"
android:textColor="@android:color/white" />
<TextView
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:background="@color/colorPrimaryDark"
android:gravity="center"
android:text="2"
android:textColor="@android:color/white" />
</LinearLayout>
</wiget.CustomLayout>
运行,打印日志:
mode 是EXACTLY size 是720 ,我用的是720p的模拟器。
将继承的FL换成HSV,打印日志,很不幸,打印日志完全相同。
这也就说明,FL和HSV的measurespc是完全一致的,所以这个思路就死胡同了,不是这个原因引起的
其实仔细想一下,HSV是继承自FL的,本是同根生,measureSpec怎么会不一致呢,不过这个思路是很赞的,这篇文章也是记录一下解决问题的思路。
3.回归本质,我们继续在HSV中探索
@Override
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
ViewGroup.LayoutParams lp = child.getLayoutParams();
final int horizontalPadding = mPaddingLeft + mPaddingRight;
final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - horizontalPadding),
MeasureSpec.UNSPECIFIED);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
final int usedTotal = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin +
widthUsed;
final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
我们注意到,在HSV中重写了这两个方法,这个方法顾名思义,就是测量子view的大小的,所以,99%的问题就出现在这里了。
我们注意到,在measureChildWithMargins()中,有一段这样的代码
final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
代码中将子View的measureSpec的mode强制设置为UNSPECIFIED,这就说明我们无论怎么设置view的属性,在HorizontalScrollView中都会失效。设置100dp或者是match_parent.
既然原因找到了,我们就顺藤摸瓜,继续寻找。
这段代码的最后我们会调用
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
我们打开这段代码看:
嗯,基本看不懂。只记住他最后又会调用后onMeasure即可…
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
// 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);
if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
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
}
由于我们的子View是textView,所以view的测量工作就交给了TextView的onMeasure函数,至于里面的测量,我们就不看了,大致得到的size就是text的size。
分析一下这段函数,对我们理解更有帮助:
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
View的measure方法,里面调用的onMeasure方法,view的onMeasure如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
代码很简单,直接调用了:setMeasuredDimension();
看一下getDefaultSize()函数:
再看一下 getSuggestedMinimumWidth()函数
可以很清晰的看出来,当specMode为UNSPECIFIED时View的大小以传入的为准,那么传入值是多少呢?就是getSuggestedMinimumWidth(),这个值,当有背景时,以背景大小为准,否则就是以minWidth为准。
由于我们的TextView复写了OnMeasure方法,不适用这套,至于TextView的测量方式,看里面代码即可,就不贴了。
花絮
知道了原因,我们就有解决的方案,1就是设置fillViewport 为true。
2呢,就是重写measureChildWithMargins方法啦,但是一般我们不会这么做,这里只是一种思路罢了,代码如下:
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
...........
final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - usedTotal),
MeasureSpec.EXACTLY);
}
将UNSPECIFIED 改为 EXACTLY即可。
知道这些,我们可以解决很多scrollView嵌套导致显示不全的问题了。