固定展开
基本上,大部分情况我需要的BottomSheetDialog是固定的,而不是可以延展的。
那么,可以在onCreateView的函数体里面:
dialog.asOrNull<BottomSheetDialog>()?.behavior?.let { behavior->
behavior.skipCollapsed = true
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
inline fun <reified Obj> Any?.asOrNull(): Obj? {
return if (this is Obj) {
this
} else {
null
}
}
含有EditText
如果内容是一个EditText,不能设置沉浸式,否则键盘无法弹出。即如下相关代码不能设置到dialogFragment上:
WindowCompat.setDecorFitsSystemWindows
WindowCompat.getInsetsController....isAppearanceLightStatusBars 和 isAppearanceLightNavigationBars
statusBarColor = Color.TRANSPARENT
自定义Toast的显示父控件
因为android有不少自定义类似ios的toast方式,从顶部下弹一个toast的自定义View。大概率是通过寻找DecorView的方式实现。
那么, 对于从DialogFragment里面弹起的我们需要寻找一个合理的父控件。
override fun findToastViewGroup(): ViewGroup? {
//rootView是你在onCreateView里面创建的我们自身的根布局。
rootView?.let { tdv->
val design_bottom_sheet = tdv.parent.asOrNull<ViewGroup>()
design_bottom_sheet?.let { dbs->
val coordinator = dbs.parent.asOrNull<ViewGroup>() //coordinator
return coordinator?.parent.asOrNull() //container
}
}
return null
}
无法在dismiss之前得到监听
研究了源码,确实无法保证系统的拖动关闭dismiss在我们想要结束我们的一些逻辑之后,再让他dismiss。
因此:
public class BeforeDismissBottomSheetDialog extends BottomSheetDialog {
public interface BeforeDismissCallback {
void onBeforeDismiss(BeforeDismissBottomSheetDialog dialog);
}
private BeforeDismissCallback beforeDismissCallback;
public void setBeforeDismissCallback(BeforeDismissCallback beforeDismissCallback) {
this.beforeDismissCallback = beforeDismissCallback;
}
public BeforeDismissBottomSheetDialog(@NonNull Context context) {
super(context);
}
public BeforeDismissBottomSheetDialog(@NonNull Context context, int theme) {
super(context, theme);
}
protected BeforeDismissBottomSheetDialog(@NonNull Context context, boolean cancelable, @Nullable OnCancelListener cancelListener) {
super(context, cancelable, cancelListener);
}
@Override
public void dismiss() {
if (beforeDismissCallback != null) {
beforeDismissCallback.onBeforeDismiss(this);
}
super.dismiss();
}
}
然后,再在onCreateDialog中:
<style name="CustomBottomSheetStyle" parent="Widget.Material3.BottomSheet.Modal">
<item name="shapeAppearance">@null</item>
</style>
<style name="MyBottomSheetDialogTheme" parent="Theme.Material3.Light.BottomSheetDialog">
<item name="bottomSheetStyle">@style/CustomBottomSheetStyle</item>
<item name="enableEdgeToEdge">true</item>
<item name="android:windowTranslucentNavigation">true</item>
<item name="android:navigationBarColor">#ffffff</item>
</style>
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val sheetDialog = BeforeDismissBottomSheetDialog(requireContext(), R.style.MyBottomSheetDialogTheme)
createdDialog = sheetDialog
//看情况设定不允许延展 sheetDialog.behavior.skipCollapsed = true
//看情况设定不允许延展 sheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
sheetDialog.setBeforeDismissCallback { //能在关闭之前干完想干的事情了。
onDismissBlock?.invoke(this)
onDismissBlock = null
}
sheetDialog.setOnShowListener { // 通过这个来判断内容的显示是最万无一失的。
onShownBlock?.invoke(this)
onShownBlock = null
}
return sheetDialog
}
另外值得一提的是,由于是Fragment特性,onStart,onCreateDialog, onViewCreated,外加一堆的dialog的onShow等逻辑,均可能产生真实的内容并没有显示。setOnShowListener是最保险的。
自适应高度和限定最大高度
生命周期为:
onCreateDialog -> 内部创建mDialog -> onCreateView -> onViewCreated
所以想要拿到dialog
则可以在onCreateView阶段。
自适应高度,我们只需要给布局的高度设定为wrap_content即可。
但是限定最大高度,一直是个问题,经过各种资料和实践如下最为稳妥,最好是放在onCreateView里面。因为maxHeight的设定,并不会执行什么刷新渲染的逻辑,而在这里设定,是还没有渲染之前。
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
///.....
//activity的来源,也可以是你全局的top activity
val screenTriple = activity?.window?.getScreenSizeWithStatusAndNavHeight(1)
val maxHeight:Int = screenTriple?.first?.y ?: Int.MAX_VALUE
val height = this.yourParseHeight ?: 0 //如果你传入的yourParseHeight存在则优先使用
fixHeight = min(height, maxHeight)
if (fixHeight > 0) {
binding.root.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, fixHeight)
} else {
dialog.asOrNull<BottomSheetDialog>()?.behavior?.let { behavior->
behavior.maxHeight = screenTriple?.first?.y ?: WindowManager.LayoutParams.MATCH_PARENT //不太可能走到?:后面去。
}
}
}
fun Window.getScreenSizeWithStatusAndNavHeight(displayMode:Int, portrait:Boolean = true): Triple<Point, Int, Int> {
val point = Point()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val metrics = windowManager.currentWindowMetrics
val bounds = metrics.bounds
point.x = bounds.width()
point.y = bounds.height()
if (displayMode > 0) {
val windowInsets = metrics.windowInsets
val insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars() or WindowInsets.Type.displayCutout())
//需要减去状态栏和导航栏高度
if (portrait) {
point.y -= insets.top
if(displayMode == 2) point.y -= insets.bottom
return Triple(point, insets.top, insets.bottom)
} else {
point.x = bounds.width() - insets.left
if(displayMode == 2) point.y -= insets.right
return Triple(point, insets.left, insets.right)
}
}
return Triple(point, 0, 0)
} else {
if (displayMode == 0) {
windowManager.defaultDisplay.getSize(point)
} else {
windowManager.defaultDisplay?.getRealSize(point) //todo 低版本没有实现 mode=1的情况。
}
return Triple(point, 0, 0) //todo 低版本没有实现 mode=1的情况。
}
}
含有滚动内容
主要就是滑动能够往上滑动,但是往下滑动就会出现滑动冲突。到底是BottomSheetDialog自身往下拉还是RecyclerView的内部下拉发生冲突。
我尝试了使用NestedScrollView效果不理想,最终通通改成recyclerView。哪怕是一些图文结合,或者长文字,都可以分割为多个item(通过viewHolder的布局也是wrap高度来处理)使用RecyclerView来组合实现滑动。
这样做的原因是:
通常我们想要在Dialog上显示较高可滑动的东西,并非要它的性能和回收item机制。而且我们需要自适应内容的高度。所以RecyclerView大概率需要设置height为wrap_content。众所周知,RecyclerView设置wrap其实就是放弃了他的item复用机制了。这是题外话。
另外,千万不要给它设置如下属性,否则会出现滑动冲突。
android:nestedScrollingEnabled="false"
android:overScrollMode="never"