Android Compose Dialog 目前有个Bug,不响应软键盘的AdjustResize。 Compose Dialog中如果包含输入框,在键盘弹出时Compose View的高度并未发生变化,导致Dialog内容无法滚动。

官方可能希望我们使用 Modifier.imePadding() 解决这个问题,可惜的是它不能。问题有了两个:

  1. 必须使用 decorFitsSystemWindows = true,在 Android 11 以下时,会导致Dialog的背景层消失。
  2. 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
    }
}

References