前言
上一篇文章我们探索了一下View测量流程的源码,但是整篇文章都没提MeasureSpec.UNSPECIFIED,然后我在文章末给大家留了一个问题,不知道大家是否有自己去尝试过,评论里面也没有人和我互动聊聊…所以我今天索性就把答案和大家分享一下,顺便我们也把现象及原因也给捋一遍。
回顾
先给大家回忆一下留下的问题,给出两段代码:
场景①
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:layout_width="360dp"
android:layout_height="400dp"
android:layout_gravity="center"
android:background="@color/red" />
</ScrollView>
复制代码
场景②
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<View
android:layout_width="360dp"
android:layout_height="400dp"
android:layout_gravity="center"
android:background="@color/red" />
</FrameLayout>
</ScrollView>
复制代码
场景①展示效果如下:
场景②展示效果如下:
是的,你没看错,在场景①的时候,咱们给View设置的400dp没起作用,显示的空白页面,在场景②的时候就正常显示了。那到底肿么回事呢?找问题的原因最快的途径是从源码中找答案。
先看一下ScrollView这个类:
ScrollView
//ScrollView.java
//1
public class ScrollView extends FrameLayout {
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//2
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
}
复制代码
标注1处我们可以知道ScrollView继承自FrameLayout,
标注2处的onMeasure()方法也是调用的FrameLayout的onMeasure()方法。
从我写的上篇文章我们知道:FrameLayout会先遍历测量每个child,并传入了widthMeasureSpec和heightMeasureSpec,测量方法如下:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//1
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
//2
final int childHeightMeasureSpec=getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
复制代码
标注1和标注2出调的都是方法getChildMeasureSpec(),再次帮大家回顾下**getChildMeasureSpec()**方法计算子view的MeasureSpec的规则:
由上面的规则可知,如果FrameLayout里面包含一个View 宽高都是固定值的话,那么计算出的child的MeasureSpec应分别是widthMeasureSpec(size=360dp,mode=EXACTLY)、heightMeasureSpec(size=400dp,mode=EXACTLY),然后再将这俩measureSpec传递给View的measure方法,理应得出来View测量结果是宽高分别为360dp和400dp呀,那为什么没显示出来呢?
别着急,我们可以在Android Studio里面查看一下ScrollView的所有方法,瞅瞅还有啥猫腻没有:
我去。。这是什么,ScrollView竟然重写了measureChildWithMargins方法,点进去瞅瞅:
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//1
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed;
//2
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
复制代码
- 标注1出没啥特别的,正常构建childWidthMeasureSpec
- 重要是标注2处,在构建childHeightMeasureSpec的时候,ScrollView将mode指定成了MeasureSpec.UNSPECIFIED
标记成MeasureSpec.UNSPECIFIED这个模式的时候再传给子view,子view在这种mode下是如何计算自己的size呢:
//View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
//1
case MeasureSpec.UNSPECIFIED:
result = size;
break;
...
}
return result;
}
复制代码
标注1处说明heightMeasureSpec在MeasureSpec.UNSPECIFIED模式下,取得是size,那size是什么呢:
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
//Drawable.java
public int getMinimumHeight() {
final int intrinsicHeight = getIntrinsicHeight();
return intrinsicHeight > 0 ? intrinsicHeight : 0;
}
复制代码
上述代码就是size的取值:取的是XML设置的min_height和background Drawable的IntrinsicHeight的较大值。因为我们XML没设置相关属性 所以 getSuggestedMinimumHeight()方法返回的是0,所以我们看到场景① View的高度实际是0,显示空白。那场景②显示正常的原因应该很好找了,直接查看FramLayout相关源码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
//1
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) {
...
//2
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
复制代码
- 标注1处在设置FrameLayout大小的时候调用resolveSizeAndState方法
- resolveSizeAndState方法在标注2处返回了出入的size,这个size是maxHeight,maxHeight是FrameLayout在测量完所有子view/ViewGroup后取得最大高度,所以这个maxHeight就是400dp, 所以场景②能显示正常。
上面的几段代码把场景① 和场景②出现的缘由介绍清楚了,那么ScrollView/NestedScrollView为什么这么做呢?
根本原因
查阅ScrollView/NestedScrollView相关源码后我个人理解的原因是:
ScrollView的滑动是通过scrollBy实现的,但是滑动总得有个范围,范围的计算规则是拿到子view的真实高度后减去ScrollView的高度,就能拿到ScrollY的范围了。正常测量模式下,子view最多和父view大小一致,我们根本拿不到真实宽高,这个时候MeasureSpec.UNSPECIFIED测量模式作用就显现出来了,android提供的很多控件FrameLayout、LinearLayout等都能在这个测量模式下能测量出真实高度,在这些ViewGroup设置宽高也就是调用setMeasuredDimension方法时候,都是通过resolveSizeAndState方法来计算返回measureSpec的,个人认为算是一种共识,也算是给开发者自定义View提供另外一种可能性吧。
NestedScrollView包裹RecyclerView的坑
聊完根本原因,这里给大家个避坑指南:之前我见过挺多同学在做稍微复杂点的列表布局,会用NestedScrollView包裹RecyclerView实现一些上面添个头布局之类的,从上面的原因分析上我们可以知道RecyclerView这种情况下也会直接测量出真实高度,这会导致RecyclerView的所有item直接全部走了onBindViewholder()方法,RecyclerView的复用机制失效,如果数据量大的时候页面会很卡。。。有兴趣的同学可以自行打印日志查看或者查阅RecyclerView相关源码。
结论
禁止NestedScrollView包裹RecyclerView的用法、禁止NestedScrollView包裹RecyclerView的用法、禁止NestedScrollView包裹RecyclerView的用法 重要的话说三遍,如果列表复杂的话,推荐使用RecyclerView的多ItemType或者MergeAdapter之类,没方案的你来找我,我给你提供更多方案,哈哈哈?…
最后的最后又到了推荐环节:欢迎大家使用我写的库 超好用的高亮引导库 Github地址如下:github.com/hyy920109/H…