文章中间部分是问题的背景和解决过程,结尾可以查看设置BottomSheetDialogFragment高度的代码。
这篇文章算是对我上篇文章BottomSheetBehavior + ViewPager2 + MultipleRecyclerView的滑动问题的一个补充。在实现ViewPager2嵌套RecyclerView滑动后,我把自己mock的假数据换成真数据,发现dialog的高度会随着翻页动态调整,这样可不行,高度来回跳很影响用户使用。
于是开始上网找资料,同时mock不同数量的数据观察case,我发现dialog的高度并不是“随机”的,它会根据dialog中当前页的内容的多少来适配高度。
如图,黄色为展开高度expandedHeight
,也就是吸顶,蓝色为弹出高度peekHeight
,大概是在屏幕高度60%的位置,假设dialog中的内容高度为contentHeight
,那么dialog高度会根据contentHeight的大小出现以下几种情况:
contentHeight > expandedHeight
:那么弹出的高度会在peekHeight位置,上划会达到expandedHeight。expandedHeight > contentHeight > peekHeight
:弹出高度会在peekHeight,上划后不会达到expandedHeight位置,而是完全显示内容(也就是到达contentHeight位置)。peekHeight > contentHeight
:弹出的dialog高度就是内容高度(contentHeight),这个时候由于内容已经全部显示出来了,所以上划是没有效果的,这个位置即是它的弹出高度(peekHeight),也是它的“展开高度”。
总的来说,如果内容足够多,那么既可以达到弹出高度,上划也可以达到展开高度。如果内容不够多,则以展示全部内容为主,也就造成了dialog高度不固定。
查看源码也可以佐证这些情况,我们都知道BottomSheetDialogFragment的滑动逻辑是依靠BottomSheetBehavior实现的,先来看弹出高度的计算源码:
private int calculatePeekHeight() {
if (peekHeightAuto) {
int desiredHeight = max(peekHeightMin, parentHeight - parentWidth * 9 / 16);
return min(desiredHeight, childHeight) + insetBottom;
}
// Only make sure the peek height is above the gesture insets if we're not applying system
// insets.
if (!gestureInsetBottomIgnored && !paddingBottomSystemWindowInsets && gestureInsetBottom > 0) {
return max(peekHeight, gestureInsetBottom + peekHeightGestureInsetBuffer);
}
return peekHeight + insetBottom;
}
如果没有设置peekHeight,peekHeightAuto为true。可以看到在true逻辑中,首先取peekHeightMin
和parentHeight - parentWidth * 9 / 16
中的最大值,peekHeightMin的值固定为64dp,也就保证最小弹出高度为64dp,parentHeight - parentWidth * 9 / 16
也就是前面所说的大约屏幕高度60%的值。得到desiredHeight,接着再从desiredHeight
和childHeight
中取最小值即可得到peekHeight,childHeight可以理解为内容高度。
展开高度同理,主要计算逻辑如下:
public int getExpandedOffset() {
return fitToContents
? fitToContentsOffset
: Math.max(expandedOffset, paddingTopSystemWindowInsets ? 0 : insetTop);
}
回到随着翻页高度调整的问题上,由于每次翻页我都会调用requestLayout()
方法重新渲染布局,并且每一页内容的多少也不固定,就会出现高度变化的问题。
最后上解决代码,把该段代码添加到我们继承了BottomSheetDialogFragment的类中。
val bottomSheet = dialog?.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
if (bottomSheet != null) {
val params = bottomSheet.layoutParams as CoordinatorLayout.LayoutParams
params.width = FrameLayout.LayoutParams.MATCH_PARENT
params.height = FrameLayout.LayoutParams.MATCH_PARENT
bottomSheet.layoutParams = params
BottomSheetBehavior.from(bottomSheet).apply {
isFitToContents = false
expandedOffset = 100.dp.toInt()
peekHeight = 500.dp.toInt()
}
}
有两个需要注意的点,一是要把高度设为MATCH_PARENT
,二是一定要调用setFitToContents()
方法并设为false,否则如果内容很少的话会出现上半部分显示内容,下半部分透明的情况。