2023年3月16日木曜日

Compose の LottieAnimation でクリックしたときにアニメーションさせる

rememberLottieAnimatable を使います。 @Preview @Composable fun LottieAnimationSample() { val composition by rememberLottieComposition( LottieCompositionSpec.RawRes(R.raw.lottie_animation) ) val lottieAnimatable = rememberLottieAnimatable() val coroutineScope = rememberCoroutineScope() Box( contentAlignment = Alignment.Center, modifier = Modifier .fillMaxSize() .pointerInput(Unit) { detectTapGestures( onTap = { coroutineScope.launch { lottieAnimatable.animate(composition) } }, ) } ) { LottieAnimation( composition = composition, progress = { lottieAnimatable.value }, modifier = Modifier.size(72.dp), ) } }

2023年3月8日水曜日

Navigation Compose の optional arguments に null を渡したいときは query parameter 自体を消さないといけない

Navigation Compose の destination に optional arguments を設定する場合、nullable = true を設定します(もしくは defaultValue を設定する)。 composable( route = "detail?id={id}", arguments = listOf( navArgument("id") { type = NavType.StringType nullable = true } ) ) { この場合、NavHostController.navigate() に渡す文字列と id の値の関係は次のようになります。

destination 先での id の値
navController.navigate("detail")null
navController.navigate("detail?id=AAAAA")"AAAA"
navController.navigate("detail?id=")""
val id: String? = null
navController.navigate("detail?id=${id}")
"null"


注意しないといけないのは "detail?id=${id}" です。id 変数が null のとき、これは "detail?id=null" になり、destination 先では "null" という文字列が取得されてしまいます。

よって id 変数が null かどうかによって次のように navigate() に渡す文字列を変える必要があります。 if (id == null) { navController.navigate("detail") } else { navController.navigate("detail?id=${id}") }

挙動確認のコード @Composable fun NavigationSample() { val navController = rememberNavController() NavHost( navController = navController, startDestination = "home" ) { composable("home") { Column { Text("home") Button( onClick = { navController.navigate("detail") } ) { Text("detail") } Button( onClick = { navController.navigate("detail?id=AAAAA") } ) { Text("detail?id=AAAAA") } Button( onClick = { navController.navigate("detail?id=") } ) { Text("detail?id=") } val id: String? = null Button( onClick = { navController.navigate("detail?id=${id}") } ) { Text("detail?id=${id}") } } } composable( route = "detail?id={id}", arguments = listOf( navArgument("id") { type = NavType.StringType nullable = true } ) ) { Column { val arguments = requireNotNull(it.arguments) val id = arguments.getString("id") Text(text = "detail") Text(text = "id is null : ${id == null}") } } } }

2022年12月7日水曜日

ラベルと値が縦に並んでいて、値部分の start が揃っている Composable を作る

ラベルと値が縦に並んでいる表があって、その値の start の位置を合わせたい。
ConstraintLayout で Barrier を使うか、自分で Layout を作ることになる。


Composableの順番で位置を指定する場合

content 内の Composable が「ラベル、値、区切り線、ラベル、値、区切り線、...」のようになっている前提の場合、このようなコードで実現できる。 @Composable fun LabelValueTable( modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { Layout( content = content, modifier = modifier ) { measurables, constraints -> val layoutWidth = constraints.maxWidth val labelMeasurables = mutableListOf<Measurable>() val valueMeasurables = mutableListOf<Measurable>() val dividerMeasurables = mutableListOf<Measurable>() measurables.forEachIndexed { index, measurable -> when (index % 3) { 0 -> labelMeasurables.add(measurable) 1 -> valueMeasurables.add(measurable) 2 -> dividerMeasurables.add(measurable) } } val constraintsForLabel = constraints.copy(minWidth = 0, minHeight = 0) val labelPlaceables = labelMeasurables.map { it.measure(constraintsForLabel) } val widthOfLabel = labelPlaceables.maxOf { it.width } val constraintsForValue = constraintsForLabel.copy(maxWidth = layoutWidth - widthOfLabel) val valuePlaceables = valueMeasurables.map { it.measure(constraintsForValue) } val dividerPlaceables = dividerMeasurables.map { it.measure(constraintsForLabel) } val heights = labelPlaceables.mapIndexed { index, labelPlaceable -> val valuePlaceable = valuePlaceables.getOrNull(index) max(labelPlaceable.height, valuePlaceable?.height ?: 0) } layout( width = constraints.maxWidth, height = max( heights.sum() + dividerPlaceables.sumOf { it.height }, constraints.minHeight ), ) { var top = 0 labelPlaceables.forEachIndexed { index, labelPlaceable -> val rowHeight = heights[index] labelPlaceable.placeRelative( x = 0, y = top + (rowHeight - labelPlaceable.height) / 2 ) val valuePlaceable = valuePlaceables.getOrNull(index) valuePlaceable?.placeRelative( x = widthOfLabel, y = top + (rowHeight - valuePlaceable.height) / 2 ) val dividerPlaceable = dividerPlaceables.getOrNull(index) dividerPlaceable?.placeRelative( x = 0, y = top + rowHeight ) top += rowHeight + (dividerPlaceable?.height ?: 0) } } } } まずラベル部分を measure して、全てのラベルから最大の width(= widthOfLabel) を計算する。

constraints.maxWidth から widthOfLabel を引いた値を maxWidth とした Constrains で値部分を measure する。

あとは、ラベル、値、区切り線を配置する。

値や区切り線に何も表示しないところは Spacer() をおけばいい。 LabelValueTable( modifier = modifier.fillMaxWidth() ) { Text( text = "名前", modifier = Modifier.padding(16.dp) ) Text( text = "山田 太郎", modifier = Modifier.padding(16.dp) ) Divider() Text( text = "bio", modifier = Modifier.padding(16.dp) ) Text( text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam", modifier = Modifier.padding(16.dp) ) Divider() Text( text = "label", modifier = Modifier.padding(16.dp) ) Column( modifier = Modifier.padding(16.dp) ) { Text( text = "headline", style = MaterialTheme.typography.bodyLarge, ) Text( text = "subtitle", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } Divider( modifier = Modifier.padding(bottom = 24.dp) ) Text( text = "生年月日", modifier = Modifier.padding(16.dp) ) Row( modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( text = "1990-01-01", modifier = Modifier.weight(1f) ) IconButton(onClick = { /*TODO*/ }) { Icon(imageVector = Icons.Default.Edit, contentDescription = "edit") } } Divider() Text( text = "性別", modifier = Modifier.padding(16.dp) ) Text( text = "男性", modifier = Modifier.padding(16.dp) ) }

ParentDataModifier で位置を指定する場合

ParentDataModifier を使って Cell の位置を指定する場合、このようなコードで実現できる。 @Composable fun LabelValueTable( modifier: Modifier = Modifier, content: @Composable LabelValueTableScope.() -> Unit, ) { Layout( content = { LabelValueTableScopeInstance.content() }, modifier = modifier ) { measurables, constraints -> val layoutWidth = constraints.maxWidth val labelMeasurables = mutableListOf<Measurable>() val valueMeasurables = mutableListOf<Measurable>() val dividerMeasurables = mutableListOf<Measurable>() measurables.forEach { measurable -> when (measurable.parentData) { is LabelIndex -> labelMeasurables.add(measurable) is ValueIndex -> valueMeasurables.add(measurable) is DividerIndex -> dividerMeasurables.add(measurable) } } val map = mutableMapOf<Int, Triple<Placeable?, Placeable?, Placeable?>>() val constraintsForLabel = constraints.copy(minWidth = 0, minHeight = 0) val labelPlaceables = labelMeasurables.map { it.measure(constraintsForLabel) } val widthOfLabel = labelPlaceables.maxOf { it.width } labelMeasurables.forEachIndexed { index, measurable -> val columnIndex = (measurable.parentData as LabelIndex).columnIndex map[columnIndex] = Triple(labelPlaceables[index], null, null) } val constraintsForValue = constraintsForLabel.copy(maxWidth = layoutWidth - widthOfLabel) val valuePlaceables = valueMeasurables.map { it.measure(constraintsForValue) } valueMeasurables.forEachIndexed { index, measurable -> val columnIndex = (measurable.parentData as ValueIndex).columnIndex map[columnIndex] = map.getOrDefault(columnIndex, Triple(null, null, null)) .copy(second = valuePlaceables[index]) } val dividerPlaceables = dividerMeasurables.map { it.measure(constraintsForLabel) } dividerMeasurables.forEachIndexed { index, measurable -> val columnIndex = (measurable.parentData as DividerIndex).columnIndex map[columnIndex] = map.getOrDefault(columnIndex, Triple(null, null, null)) .copy(third = dividerPlaceables[index]) } val list = map.toList() .sortedBy { it.first } .map { it.second } val heights = list.map { max(it.first?.height ?: 0, it.second?.height ?: 0) } layout( width = constraints.maxWidth, height = max( heights.sum() + dividerPlaceables.sumOf { it.height }, constraints.minHeight ), ) { var top = 0 list.forEachIndexed { index, triple -> val (labelPlaceable, valuePlaceable, dividerPlaceable) = triple val rowHeight = heights[index] labelPlaceable?.placeRelative( x = 0, y = top + (rowHeight - labelPlaceable.height) / 2 ) valuePlaceable?.placeRelative( x = widthOfLabel, y = top + (rowHeight - valuePlaceable.height) / 2 ) dividerPlaceable?.placeRelative( x = 0, y = top + rowHeight ) top += rowHeight + (dividerPlaceable?.height ?: 0) } } } } @Immutable interface LabelValueTableScope { @Stable fun Modifier.label(columnIndex: Int): Modifier @Stable fun Modifier.value(columnIndex: Int): Modifier @Stable fun Modifier.divider(columnIndex: Int): Modifier } internal object LabelValueTableScopeInstance : LabelValueTableScope { @Stable override fun Modifier.label(columnIndex: Int): Modifier { return this.then(LabelIndex(columnIndex)) } @Stable override fun Modifier.value(columnIndex: Int): Modifier { return this.then(ValueIndex(columnIndex)) } @Stable override fun Modifier.divider(columnIndex: Int): Modifier { return this.then(DividerIndex(columnIndex)) } } @Immutable private data class LabelIndex(val columnIndex: Int) : ParentDataModifier { override fun Density.modifyParentData(parentData: Any?): Any? { return this@LabelIndex } } @Immutable private data class ValueIndex(val columnIndex: Int) : ParentDataModifier { override fun Density.modifyParentData(parentData: Any?): Any? { return this@ValueIndex } } @Immutable private data class DividerIndex(val columnIndex: Int) : ParentDataModifier { override fun Density.modifyParentData(parentData: Any?): Any? { return this@DividerIndex } } measurable.parentData からラベル、値、区切り線のどれで、column index が何かを取得する。

measure と配置部分でやっていることは同じ。

使う側では Modifier.label(), Modifier.value(), Modifier.divider() を使って位置を指定する。 LabelValueTable( modifier = modifier.fillMaxWidth() ) { Text( text = "名前", modifier = Modifier .padding(16.dp) .label(0) ) Text( text = "山田 太郎", modifier = Modifier .padding(16.dp) .value(0) ) Divider( modifier = Modifier.divider(0) ) Text( text = "bio", modifier = Modifier .padding(16.dp) .label(1) ) Text( text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam", modifier = Modifier .padding(16.dp) .value(1) ) Divider( modifier = Modifier.divider(1) ) Text( text = "label", modifier = Modifier .padding(16.dp) .label(2) ) Column( modifier = Modifier .padding(16.dp) .value(2) ) { Text( text = "headline", style = MaterialTheme.typography.bodyLarge, ) Text( text = "subtitle", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } Divider( modifier = Modifier .padding(bottom = 24.dp) .divider(2) ) Text( text = "生年月日", modifier = Modifier .padding(16.dp) .label(3) ) Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .padding(16.dp) .value(3), ) { Text( text = "1990-01-01", modifier = Modifier.weight(1f) ) IconButton(onClick = { /*TODO*/ }) { Icon(imageVector = Icons.Default.Edit, contentDescription = "edit") } } Divider( modifier = Modifier.divider(3) ) Text( text = "性別", modifier = Modifier .padding(16.dp) .label(4) ) Text( text = "男性", modifier = Modifier .padding(16.dp) .value(4) ) }