Android Compose Dialog 目前有个Bug,不响应软键盘的AdjustResize。 Compose Dialog中如果包含输入框,在键盘弹出时Compose View的高度并未发生变化,导致Dialog内容无法滚动。
官方可能希望我们使用 Modifier.imePadding()
解决这个问题,可惜的是它不能。问题有了两个:
- 必须使用
decorFitsSystemWindows = true
,在 Android 11 以下时,会导致Dialog的背景层消失。 - Dialog在键盘弹起或收起时会出现蜜汁抖动,抖动的距离看起来像是系统导航栏和状态栏的高度。
总归来说,imePadding 本身就是 Android 11 才新增的特性,键盘高度会通过 WindowInsets 分发。
Bug 原因
在查看 Compose Dialog 的源码时,猜测是下面的代码出现问题:
@Composable
private fun DialogLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
val placeables = measurables.fastMap { it.measure(constraints) }
val width = placeables.fastMaxBy { it.width }?.width ?: constraints.minWidth
val height = placeables.fastMaxBy { it.height }?.height ?: constraints.minHeight
layout(width, height) {
placeables.fastForEach { it.placeRelative(0, 0) }
}
}
}
constraints 参数中获取到的高度是屏幕高度,而不是内容高度。即使软键盘弹起后,也未发生变化。
Layout 的源码过于复杂,我并没有看懂。但是我觉得它可能是多余的,至少在大部分场景下应该是多余的。
然后我参照源码重新写了一个WorkaroundDialog,简化原有逻辑,暂时解决了无法响应AdjustResize软键盘的问题。
WorkaroundDialog
代码仓库: https://github.com/aitsuki/ComposeInputDialogWorkaround
@Composable
fun WorkaroundDialog(
dismissOnBackPress: Boolean = true,
dismissOnClickOutside: Boolean = true,
usePlatformWidth: Boolean = true,
onDismissRequest: () -> Unit,
content: @Composable () -> Unit
) {
val context = LocalContext.current
val dialog = remember {
WorkaroundDialogWrapper(
context = context,
usePlatformWidth = usePlatformWidth,
dismissOnBackPress = dismissOnBackPress,
dismissOnClickOutside = dismissOnClickOutside,
onDismissRequest = onDismissRequest,
content = content
)
}
DisposableEffect(Unit) {
dialog.show()
onDispose {
dialog.dismiss()
}
}
}
private class WorkaroundDialogWrapper(
context: Context,
val usePlatformWidth: Boolean,
val dismissOnBackPress: Boolean,
val dismissOnClickOutside: Boolean,
val onDismissRequest: () -> Unit,
content: @Composable () -> Unit,
) : ComponentDialog(context) {
init {
val window = window ?: error("Dialog has no window")
window.requestFeature(Window.FEATURE_NO_TITLE)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
window.setBackgroundDrawableResource(android.R.color.transparent)
val existingComposeView = window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as? ComposeView
if (existingComposeView != null) with(existingComposeView) {
setContent(content)
} else ComposeView(window.context).apply {
setContent(content)
setContentView(this)
}
if (usePlatformWidth) {
window.setLayout(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT
)
} else {
window.setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.WRAP_CONTENT
)
}
onBackPressedDispatcher.addCallback(this) {
if (dismissOnBackPress) {
onDismissRequest()
}
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
val result = super.onTouchEvent(event)
if (result && dismissOnClickOutside) {
onDismissRequest()
}
return result
}
}