8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JetpackCompose のAlertDialog で時間を溶かした

Last updated at Posted at 2021-07-25

JetpackCompose でAlertDialog を使うときに失敗した話です

まとめると

  • AlertDialog を使うときはText のみで単純な構成にするのが無難
  • TextField を置きたいときはDialog を使おう
  • そもそもダイアログが必要か考えよう

どんな失敗?

テキスト入力をするダイアログをAlertDialogを使って作ると、レイアウトが崩れてしまうことがありました

@Composable
fun MyDialog4(onDismiss: () -> Unit) {
    val (text, setText) = remember { mutableStateOf("") }
    AlertDialog(
        onDismissRequest = onDismiss,
        text = {
            TextField(
                value = text,
                onValueChange = setText,
                modifier = Modifier.padding(32.dp),
            )
        },
        confirmButton = { ... },
    )
}

環境

  • Android Studio: Arctic Fox 2020.3.1 RC 1
  • Kotlin: 1.5.10
  • androidx.compose.*: 1.0.0-rc02

なぜ起きた?

このレイアウト崩れが起きた原因を探るため、AlertDialog の中身を見てみましょう
すると、ColumnScope.AlertDialogBaselineLayout にたどり着きます
ここでは、受け取ったタイトルとテキストを配置しています

@Composable
internal fun ColumnScope.AlertDialogBaselineLayout(
    title: @Composable (() -> Unit)?,
    text: @Composable (() -> Unit)?
) {
    Layout(
        {
            title?.let { title ->
                Box(...) {
                    title()
                }
            }
            text?.let { text ->
                Box(...) {
                    text()
                }
            }
        },
        Modifier.weight(1f, false)
    ) { measurables, constraints ->
        val titlePlaceable = measurables.firstOrNull { it.layoutId == "title" }?.measure(
            constraints.copy(minHeight = 0)
        )
        val textPlaceable = measurables.firstOrNull { it.layoutId == "text" }?.measure(
            constraints.copy(minHeight = 0)
        )

        // このあたりでタイトルとテキストの位置を計算
        ...

        layout(layoutWidth, layoutHeight) {
            titlePlaceable?.place(0, titlePositionY)
            textPlaceable?.place(0, textPositionY)
        }
    }
}

より詳しく見ていくと、タイトル・テキストそれぞれについて、ベースラインを見ながら縦方向の位置決めをしています(横方向は単純なため本稿では割愛)
細かい内容は実際のコードを読んでもらえればと思いますが、ここでの重要な点はベースラインの位置が特定の位置に来るように配置していることです。(特定の位置は、sp 依存の固定値)

// Place the title so that its first baseline is titleOffset from the top
val titlePositionY = titleOffset - firstTitleBaseline

これを知っていないと、いくつかの問題が起きることがあります

top 方向のpadding が効かない

ベースラインの位置を特定の位置に合わせようとするため、top にpadding を指定しても効きません
また、大きな値を入れてしまうとテキストそのものが潰れてしまいます

pad = 0 16 28
@Composable
fun MyDialog5(onDismiss: () -> Unit) {
    val pad = // 表中の値
    AlertDialog(
        onDismissRequest = onDismiss,
        title = null,
        text = {
            Text(text = "MyDialog5", modifier = Modifier.padding(top = pad.dp))
        },
        confirmButton = { CancelButton(onDismiss) },
    )
}

TextTextFieldでズレが起きる

TextTextFieldでは上端からベースラインまでの距離が異なります
そのため、単純に配置するだけでもズレが生じます

Text TextField
@Composable
fun MyDialog2(onDismiss: () -> Unit) {
    AlertDialog(
        onDismissRequest = onDismiss,
        title = null,
        text = { Text(text = "MyDialog2") },
        confirmButton = { CancelButton(onDismiss) },
    )
}

@Composable
fun MyDialog3(onDismiss: () -> Unit) {
    val (text, setText) = remember { mutableStateOf("") }
    AlertDialog(
        onDismissRequest = onDismiss,
        text = { TextField(value = text, onValueChange = setText) },
        confirmButton = { CancelButton(onDismiss) },
    )
}

解決案

解決案としては以下のような例が考えられます

Dialog を使う

自分でDialog から実装してしまいます
先ほど実際にAlertDialog の中身を見た人はわかると思いますが、これはDialog を使って実装されているため、これを真似ることで同じようなダイアログを自由に実現できます
タイトルなどを配置する場合、AlertDialog で設定されていたalpha やtextStyle についても自前で設定する必要があることに注意します

@Composable
fun MyDialog6(onDismiss: () -> Unit) {
    val (text, setText) = remember { mutableStateOf("") }
    Dialog(onDismissRequest = onDismiss) {
        Surface {
            Column {
                TextField(
                    value = text,
                    onValueChange = setText,
                    modifier = Modifier.padding(32.dp),
                )
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(all = 8.dp),
                ) {
                    CancelButton(
                        onClick = onDismiss,
                        modifier = Modifier
                            .align(Alignment.BottomEnd),
                    )
                }
            }
        }
    }
}

ダイアログを使わない

解決策とは言えないかもしれませんが、そもそもそのダイアログは必要でしょうか?
複雑なUI が必要であれば、ダイアログという考えから脱却し、通常の画面に配置するというのも手の一つです
また、ダイアログはユーザの操作をブロックするUI であり、その利用には慎重になる必要があります

参考リンク

公式ドキュメント
マテリアルデザインガイドライン
今回作成したサンプルプロジェクト、ダイアログについてはここ

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?