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