2022年12月7日水曜日

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

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


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

content 内の Composable が「ラベル、値、区切り線、ラベル、値、区切り線、...」のようになっている前提の場合、このようなコードで実現できる。
  1. @Composable  
  2. fun LabelValueTable(  
  3.     modifier: Modifier = Modifier,  
  4.     content: @Composable () -> Unit,  
  5. ) {  
  6.     Layout(  
  7.         content = content,  
  8.         modifier = modifier  
  9.     ) { measurables, constraints ->  
  10.         val layoutWidth = constraints.maxWidth  
  11.   
  12.         val labelMeasurables = mutableListOf<Measurable>()  
  13.         val valueMeasurables = mutableListOf<Measurable>()  
  14.         val dividerMeasurables = mutableListOf<Measurable>()  
  15.   
  16.         measurables.forEachIndexed { index, measurable ->  
  17.             when (index % 3) {  
  18.                 0 -> labelMeasurables.add(measurable)  
  19.                 1 -> valueMeasurables.add(measurable)  
  20.                 2 -> dividerMeasurables.add(measurable)  
  21.             }  
  22.         }  
  23.   
  24.         val constraintsForLabel = constraints.copy(minWidth = 0, minHeight = 0)  
  25.         val labelPlaceables = labelMeasurables.map { it.measure(constraintsForLabel) }  
  26.         val widthOfLabel = labelPlaceables.maxOf { it.width }  
  27.   
  28.         val constraintsForValue = constraintsForLabel.copy(maxWidth = layoutWidth - widthOfLabel)  
  29.         val valuePlaceables = valueMeasurables.map { it.measure(constraintsForValue) }  
  30.   
  31.         val dividerPlaceables = dividerMeasurables.map { it.measure(constraintsForLabel) }  
  32.   
  33.         val heights = labelPlaceables.mapIndexed { index, labelPlaceable ->  
  34.             val valuePlaceable = valuePlaceables.getOrNull(index)  
  35.             max(labelPlaceable.height, valuePlaceable?.height ?: 0)  
  36.         }  
  37.   
  38.         layout(  
  39.             width = constraints.maxWidth,  
  40.             height = max(  
  41.                 heights.sum() + dividerPlaceables.sumOf { it.height },  
  42.                 constraints.minHeight  
  43.             ),  
  44.         ) {  
  45.             var top = 0  
  46.   
  47.             labelPlaceables.forEachIndexed { index, labelPlaceable ->  
  48.                 val rowHeight = heights[index]  
  49.   
  50.                 labelPlaceable.placeRelative(  
  51.                     x = 0,  
  52.                     y = top + (rowHeight - labelPlaceable.height) / 2  
  53.                 )  
  54.   
  55.                 val valuePlaceable = valuePlaceables.getOrNull(index)  
  56.                 valuePlaceable?.placeRelative(  
  57.                     x = widthOfLabel,  
  58.                     y = top + (rowHeight - valuePlaceable.height) / 2  
  59.                 )  
  60.   
  61.                 val dividerPlaceable = dividerPlaceables.getOrNull(index)  
  62.                 dividerPlaceable?.placeRelative(  
  63.                     x = 0,  
  64.                     y = top + rowHeight  
  65.                 )  
  66.   
  67.                 top += rowHeight + (dividerPlaceable?.height ?: 0)  
  68.             }  
  69.         }  
  70.     }  
  71. }  
まずラベル部分を measure して、全てのラベルから最大の width(= widthOfLabel) を計算する。

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

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

値や区切り線に何も表示しないところは Spacer() をおけばいい。
  1. LabelValueTable(  
  2.     modifier = modifier.fillMaxWidth()  
  3. ) {  
  4.     Text(  
  5.         text = "名前",  
  6.         modifier = Modifier.padding(16.dp)  
  7.     )  
  8.     Text(  
  9.         text = "山田 太郎",  
  10.         modifier = Modifier.padding(16.dp)  
  11.     )  
  12.     Divider()  
  13.   
  14.     Text(  
  15.         text = "bio",  
  16.         modifier = Modifier.padding(16.dp)  
  17.     )  
  18.     Text(  
  19.         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",  
  20.         modifier = Modifier.padding(16.dp)  
  21.     )  
  22.     Divider()  
  23.   
  24.     Text(  
  25.         text = "label",  
  26.         modifier = Modifier.padding(16.dp)  
  27.     )  
  28.     Column(  
  29.         modifier = Modifier.padding(16.dp)  
  30.     ) {  
  31.         Text(  
  32.             text = "headline",  
  33.             style = MaterialTheme.typography.bodyLarge,  
  34.         )  
  35.         Text(  
  36.             text = "subtitle",  
  37.             style = MaterialTheme.typography.bodySmall,  
  38.             color = MaterialTheme.colorScheme.onSurfaceVariant,  
  39.         )  
  40.     }  
  41.     Divider(  
  42.         modifier = Modifier.padding(bottom = 24.dp)  
  43.     )  
  44.   
  45.     Text(  
  46.         text = "生年月日",  
  47.         modifier = Modifier.padding(16.dp)  
  48.     )  
  49.     Row(  
  50.         modifier = Modifier.padding(16.dp),  
  51.         verticalAlignment = Alignment.CenterVertically,  
  52.     ) {  
  53.         Text(  
  54.             text = "1990-01-01",  
  55.             modifier = Modifier.weight(1f)  
  56.         )  
  57.         IconButton(onClick = { /*TODO*/ }) {  
  58.             Icon(imageVector = Icons.Default.Edit, contentDescription = "edit")  
  59.         }  
  60.     }  
  61.     Divider()  
  62.   
  63.     Text(  
  64.         text = "性別",  
  65.         modifier = Modifier.padding(16.dp)  
  66.     )  
  67.     Text(  
  68.         text = "男性",  
  69.         modifier = Modifier.padding(16.dp)  
  70.     )  
  71. }  


ParentDataModifier で位置を指定する場合

ParentDataModifier を使って Cell の位置を指定する場合、このようなコードで実現できる。
  1. @Composable  
  2. fun LabelValueTable(  
  3.     modifier: Modifier = Modifier,  
  4.     content: @Composable LabelValueTableScope.() -> Unit,  
  5. ) {  
  6.     Layout(  
  7.         content = { LabelValueTableScopeInstance.content() },  
  8.         modifier = modifier  
  9.     ) { measurables, constraints ->  
  10.         val layoutWidth = constraints.maxWidth  
  11.   
  12.         val labelMeasurables = mutableListOf<Measurable>()  
  13.         val valueMeasurables = mutableListOf<Measurable>()  
  14.         val dividerMeasurables = mutableListOf<Measurable>()  
  15.   
  16.         measurables.forEach { measurable ->  
  17.             when (measurable.parentData) {  
  18.                 is LabelIndex -> labelMeasurables.add(measurable)  
  19.                 is ValueIndex -> valueMeasurables.add(measurable)  
  20.                 is DividerIndex -> dividerMeasurables.add(measurable)  
  21.             }  
  22.         }  
  23.   
  24.         val map = mutableMapOf<Int, Triple<Placeable?, Placeable?, Placeable?>>()  
  25.   
  26.         val constraintsForLabel = constraints.copy(minWidth = 0, minHeight = 0)  
  27.         val labelPlaceables = labelMeasurables.map { it.measure(constraintsForLabel) }  
  28.         val widthOfLabel = labelPlaceables.maxOf { it.width }  
  29.         labelMeasurables.forEachIndexed { index, measurable ->  
  30.             val columnIndex = (measurable.parentData as LabelIndex).columnIndex  
  31.             map[columnIndex] = Triple(labelPlaceables[index], nullnull)  
  32.         }  
  33.   
  34.         val constraintsForValue = constraintsForLabel.copy(maxWidth = layoutWidth - widthOfLabel)  
  35.         val valuePlaceables = valueMeasurables.map { it.measure(constraintsForValue) }  
  36.         valueMeasurables.forEachIndexed { index, measurable ->  
  37.             val columnIndex = (measurable.parentData as ValueIndex).columnIndex  
  38.             map[columnIndex] = map.getOrDefault(columnIndex, Triple(nullnullnull))  
  39.                 .copy(second = valuePlaceables[index])  
  40.         }  
  41.   
  42.         val dividerPlaceables = dividerMeasurables.map { it.measure(constraintsForLabel) }  
  43.         dividerMeasurables.forEachIndexed { index, measurable ->  
  44.             val columnIndex = (measurable.parentData as DividerIndex).columnIndex  
  45.             map[columnIndex] = map.getOrDefault(columnIndex, Triple(nullnullnull))  
  46.                 .copy(third = dividerPlaceables[index])  
  47.         }  
  48.   
  49.         val list = map.toList()  
  50.             .sortedBy { it.first }  
  51.             .map { it.second }  
  52.   
  53.         val heights = list.map {  
  54.             max(it.first?.height ?: 0, it.second?.height ?: 0)  
  55.         }  
  56.   
  57.         layout(  
  58.             width = constraints.maxWidth,  
  59.             height = max(  
  60.                 heights.sum() + dividerPlaceables.sumOf { it.height },  
  61.                 constraints.minHeight  
  62.             ),  
  63.         ) {  
  64.             var top = 0  
  65.   
  66.             list.forEachIndexed { index, triple ->  
  67.                 val (labelPlaceable, valuePlaceable, dividerPlaceable) = triple  
  68.                 val rowHeight = heights[index]  
  69.   
  70.                 labelPlaceable?.placeRelative(  
  71.                     x = 0,  
  72.                     y = top + (rowHeight - labelPlaceable.height) / 2  
  73.                 )  
  74.   
  75.                 valuePlaceable?.placeRelative(  
  76.                     x = widthOfLabel,  
  77.                     y = top + (rowHeight - valuePlaceable.height) / 2  
  78.                 )  
  79.   
  80.                 dividerPlaceable?.placeRelative(  
  81.                     x = 0,  
  82.                     y = top + rowHeight  
  83.                 )  
  84.   
  85.                 top += rowHeight + (dividerPlaceable?.height ?: 0)  
  86.             }  
  87.         }  
  88.     }  
  89. }  
  90.   
  91.   
  92. @Immutable  
  93. interface LabelValueTableScope {  
  94.     @Stable  
  95.     fun Modifier.label(columnIndex: Int): Modifier  
  96.   
  97.     @Stable  
  98.     fun Modifier.value(columnIndex: Int): Modifier  
  99.   
  100.     @Stable  
  101.     fun Modifier.divider(columnIndex: Int): Modifier  
  102. }  
  103.   
  104. internal object LabelValueTableScopeInstance : LabelValueTableScope {  
  105.   
  106.     @Stable  
  107.     override fun Modifier.label(columnIndex: Int): Modifier {  
  108.         return this.then(LabelIndex(columnIndex))  
  109.     }  
  110.   
  111.     @Stable  
  112.     override fun Modifier.value(columnIndex: Int): Modifier {  
  113.         return this.then(ValueIndex(columnIndex))  
  114.     }  
  115.   
  116.     @Stable  
  117.     override fun Modifier.divider(columnIndex: Int): Modifier {  
  118.         return this.then(DividerIndex(columnIndex))  
  119.     }  
  120. }  
  121.   
  122. @Immutable  
  123. private data class LabelIndex(val columnIndex: Int) : ParentDataModifier {  
  124.     override fun Density.modifyParentData(parentData: Any?): Any? {  
  125.         return this@LabelIndex  
  126.     }  
  127. }  
  128.   
  129. @Immutable  
  130. private data class ValueIndex(val columnIndex: Int) : ParentDataModifier {  
  131.     override fun Density.modifyParentData(parentData: Any?): Any? {  
  132.         return this@ValueIndex  
  133.     }  
  134. }  
  135.   
  136. @Immutable  
  137. private data class DividerIndex(val columnIndex: Int) : ParentDataModifier {  
  138.     override fun Density.modifyParentData(parentData: Any?): Any? {  
  139.         return this@DividerIndex  
  140.     }  
  141. }  
measurable.parentData からラベル、値、区切り線のどれで、column index が何かを取得する。

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

使う側では Modifier.label(), Modifier.value(), Modifier.divider() を使って位置を指定する。
  1. LabelValueTable(  
  2.     modifier = modifier.fillMaxWidth()  
  3. ) {  
  4.     Text(  
  5.         text = "名前",  
  6.         modifier = Modifier  
  7.             .padding(16.dp)  
  8.             .label(0)  
  9.     )  
  10.     Text(  
  11.         text = "山田 太郎",  
  12.         modifier = Modifier  
  13.             .padding(16.dp)  
  14.             .value(0)  
  15.     )  
  16.     Divider(  
  17.         modifier = Modifier.divider(0)  
  18.     )  
  19.   
  20.     Text(  
  21.         text = "bio",  
  22.         modifier = Modifier  
  23.             .padding(16.dp)  
  24.             .label(1)  
  25.     )  
  26.     Text(  
  27.         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",  
  28.         modifier = Modifier  
  29.             .padding(16.dp)  
  30.             .value(1)  
  31.     )  
  32.     Divider(  
  33.         modifier = Modifier.divider(1)  
  34.     )  
  35.   
  36.     Text(  
  37.         text = "label",  
  38.         modifier = Modifier  
  39.             .padding(16.dp)  
  40.             .label(2)  
  41.     )  
  42.     Column(  
  43.         modifier = Modifier  
  44.             .padding(16.dp)  
  45.             .value(2)  
  46.     ) {  
  47.         Text(  
  48.             text = "headline",  
  49.             style = MaterialTheme.typography.bodyLarge,  
  50.         )  
  51.         Text(  
  52.             text = "subtitle",  
  53.             style = MaterialTheme.typography.bodySmall,  
  54.             color = MaterialTheme.colorScheme.onSurfaceVariant,  
  55.         )  
  56.     }  
  57.     Divider(  
  58.         modifier = Modifier  
  59.             .padding(bottom = 24.dp)  
  60.             .divider(2)  
  61.     )  
  62.   
  63.     Text(  
  64.         text = "生年月日",  
  65.         modifier = Modifier  
  66.             .padding(16.dp)  
  67.             .label(3)  
  68.     )  
  69.     Row(  
  70.         verticalAlignment = Alignment.CenterVertically,  
  71.         modifier = Modifier  
  72.             .padding(16.dp)  
  73.             .value(3),  
  74.     ) {  
  75.         Text(  
  76.             text = "1990-01-01",  
  77.             modifier = Modifier.weight(1f)  
  78.         )  
  79.         IconButton(onClick = { /*TODO*/ }) {  
  80.             Icon(imageVector = Icons.Default.Edit, contentDescription = "edit")  
  81.         }  
  82.     }  
  83.     Divider(  
  84.         modifier = Modifier.divider(3)  
  85.     )  
  86.   
  87.     Text(  
  88.         text = "性別",  
  89.         modifier = Modifier  
  90.             .padding(16.dp)  
  91.             .label(4)  
  92.     )  
  93.     Text(  
  94.         text = "男性",  
  95.         modifier = Modifier  
  96.             .padding(16.dp)  
  97.             .value(4)  
  98.     )  
  99. }  

2022年12月6日火曜日

ParentDataModifier

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

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

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


ParentDataModifier を実装した独自 Modifier を用意することができます。
  1. @Immutable  
  2. interface MyLayoutScope {  
  3.     @Stable  
  4.     fun Modifier.myLayoutData(index: Int): Modifier  
  5. }  
  6.   
  7. internal object MyLayoutScopeInstance : MyLayoutScope {  
  8.   
  9.     @Stable  
  10.     override fun Modifier.myLayoutData(index: Int): Modifier {  
  11.         return this.then(MyLayoutData(index = index))  
  12.     }  
  13. }  
  14.   
  15. @Immutable  
  16. private data class MyLayoutData(val index: Int) : ParentDataModifier {  
  17.     override fun Density.modifyParentData(parentData: Any?): Any? {  
  18.         return this@MyLayoutData  
  19.     }  
  20. }  
  21.     
  22. val Measurable.index: Int?  
  23.     get() = (parentData as? MyLayoutData)?.index