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