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) ) }

2022年12月6日火曜日

ParentDataModifier

ParentDataModifier は、親の Layout にデータを提供するための Modifier です。
ParentDataModifier の Density.modifyParentData() で返したデータが IntrinsicMeasurable.parentData に格納されます。

Measureable が IntrinsicMeasurable を実装しているので、MeasurePolicy の MeasureScope.measure() でこのデータを利用できます。 Layout( content = content, modifier = modifier ) { measurables, constraints -> measurables.forEach { val parentData = measurable.parentData ... } ... } ParentDataModifier interface を実装している Modifier として
  • LayoutId
  • BoxChildData
  • LayoutWeightImpl
  • HorizontalAlignModifier
  • VerticalAlignModifier
  • SiblingsAlignedModifier
などがあります。

この中で LayoutId は自分の Layout でも使うことができます。 Layout( content = { Icon( imageVector = Icons.Default.Home, contentDescription = "home", modifier = Modifier.layoutId("icon") ) Text( text = "home", modifier = Modifier.layoutId("text") ) }, modifier = modifier ) { measurables, constraints -> val iconMeasurable = measurables.first { it.layoutId == "icon" } val textMeasurable = measurables.first { it.layoutId == "text" } ... } Modifier.layoutId() は LayoutId を適用した Modifier を返すメソッドです。 @Stable fun Modifier.layoutId(layoutId: Any) = this.then( LayoutId( layoutId = layoutId, inspectorInfo = debugInspectorInfo { name = "layoutId" value = layoutId } ) ) LayoutId は ParentDataModifier と LayoutIdParentData を実装した Modifier で、Density.modifyParentData() では LayoutId 自身を返します。 @Immutable private class LayoutId( override val layoutId: Any, inspectorInfo: InspectorInfo.() -> Unit ) : ParentDataModifier, LayoutIdParentData, InspectorValueInfo(inspectorInfo) { override fun Density.modifyParentData(parentData: Any?): Any? { return this@LayoutId } ... } interface LayoutIdParentData { val layoutId: Any } Measurable.layoutId は parentData から Modifier.layoutId() で渡した layoutId を取得する便利メソッドです。 val Measurable.layoutId: Any? get() = (parentData as? LayoutIdParentData)?.layoutId

ParentDataModifier を実装した独自 Modifier を用意することができます。 @Immutable interface MyLayoutScope { @Stable fun Modifier.myLayoutData(index: Int): Modifier } internal object MyLayoutScopeInstance : MyLayoutScope { @Stable override fun Modifier.myLayoutData(index: Int): Modifier { return this.then(MyLayoutData(index = index)) } } @Immutable private data class MyLayoutData(val index: Int) : ParentDataModifier { override fun Density.modifyParentData(parentData: Any?): Any? { return this@MyLayoutData } } val Measurable.index: Int? get() = (parentData as? MyLayoutData)?.index