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) },
)
}
Text
とTextField
でズレが起きる
Text
とTextField
では上端からベースラインまでの距離が異なります
そのため、単純に配置するだけでもズレが生じます
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 であり、その利用には慎重になる必要があります
参考リンク
公式ドキュメント
マテリアルデザインガイドライン
今回作成したサンプルプロジェクト、ダイアログについてはここ