포켓몬 슬립의 마이페이지에는 주간 수면 시간에 대한 막대그래프를 보여준다. 이 막대 그래프를 만들어볼 것이다.
@Composable
fun SleepDurationGraph(
modifier: Modifier = Modifier,
xLabelCount: Int,
xLabel: @Composable (Int) -> Unit,
bar: @Composable (Int) -> Unit,
barLabel: @Composable (Int) -> Unit = {},
yLabelsInfo: List<YLabel>,
yLabel: @Composable (yLabel: YLabel) -> Unit,
minYPosition: Int = 0,
maxYPosition: Int = 100,
) {
val xLabels = @Composable {
repeat(xLabelCount) {
xLabel(it)
}
}
val yLabels = @Composable {
repeat(yLabelsInfo.size) { index ->
yLabel(yLabelsInfo[index])
}
}
val durationBar = @Composable {
repeat(xLabelCount) {
bar(it)
}
}
val barLabels = @Composable {
repeat(xLabelCount) {
barLabel(it)
}
}
val density = LocalDensity.current
val xAxisHeight = with(density) {
2.dp.roundToPx()
}
val groupArea = @Composable {
GraphArea(
modifier = Modifier.fillMaxSize(),
xLabelCount = xLabelCount,
hideEdgeXTicker = true,
maxYPosition = maxYPosition,
minYPosition = minYPosition,
yAxisWidth = 0,
xAxisHeight = xAxisHeight,
yLabelPositions = yLabelsInfo.map { it.position }
)
}
}
xLabel, ylabel 정보, 막대 그래프 각각에 대한 정보를 받아와서 준비한다.
GroupArea는 여기에서 어떻게 그렸는지 설명해놓았다.
이제 만들어놓은 각 요소들을 배치해야 한다.
Layout(
contents = listOf(xLabels, yLabels, groupArea, durationBar, barLabels),
modifier = modifier
.padding(start = 16.dp, top = 16.dp, end = 24.dp, bottom = 16.dp)
) {
(xLabelsMeasurables, yLabelsMeasurables, graphAreaMeasurable, durationBarMeasurables, barLabelsMeasurables),
constraints ->
val adjustedConstraints = constraints.copy(
minWidth = 0,
minHeight = 0
)
val labelGraphPadding = 12.dp.roundToPx()
var totalWidth = 0
var xLabelMaxWidth = 0
val xLabelPlaceables = xLabelsMeasurables.map { measurable ->
val placeable = measurable.measure(adjustedConstraints)
totalWidth += placeable.width
xLabelMaxWidth = maxOf(xLabelMaxWidth, placeable.width)
placeable
}
var yLabelMaxWidth = 0
val yLabelsPlaceables = yLabelsMeasurables.map { measurable ->
val placeable = measurable.measure(adjustedConstraints)
yLabelMaxWidth = maxOf(yLabelMaxWidth, placeable.width)
placeable
}
val groupAreaWidth = constraints.maxWidth - yLabelMaxWidth - labelGraphPadding
val groupAreaHeight = constraints.maxHeight - xLabelPlaceables.first().height - labelGraphPadding
val groupAreaPlaceable = graphAreaMeasurable.first().measure(
constraints.copy(
minWidth = groupAreaWidth,
maxWidth = groupAreaWidth,
minHeight = groupAreaHeight,
maxHeight = groupAreaHeight
)
)
val xLabelSpace = (groupAreaWidth - xLabelMaxWidth * xLabelCount) / xLabelCount
val durationBarPlaceables = durationBarMeasurables.map { measurable ->
val barParentData = measurable.parentData as TimeGraphParentData
val barHeight = barParentData.duration
measurable.measure(
constraints.copy(
minWidth = xLabelPlaceables.first().width * 9 / 10,
maxWidth = xLabelPlaceables.first().width * 9 / 10,
minHeight = (groupAreaHeight - xAxisHeight) *
(barHeight - minYPosition) / (maxYPosition - minYPosition),
maxHeight = (groupAreaHeight - xAxisHeight) *
(barHeight - minYPosition) / (maxYPosition - minYPosition)
)
)
}
val barLabelsPlaceables = barLabelsMeasurables.map { measurable ->
measurable.measure(adjustedConstraints)
}
layout(constraints.maxWidth, constraints.maxHeight) {
groupAreaPlaceable.place(yLabelMaxWidth + labelGraphPadding, 0)
val widthBaseline = yLabelMaxWidth + labelGraphPadding + xLabelSpace / 2
var width = widthBaseline
val heightBaseline = groupAreaHeight - xAxisHeight
xLabelPlaceables.forEachIndexed { index, it ->
it.place(
width + xLabelMaxWidth / 2 - it.width / 2,
constraints.maxHeight - it.height
)
val durationPlaceable = durationBarPlaceables[index]
durationPlaceable.place(
width,
heightBaseline - durationPlaceable.height
)
val barLabelPlaceable = barLabelsPlaceables[index]
val textHeight = heightBaseline - durationPlaceable.height - 12 - barLabelPlaceable.height
barLabelPlaceable.place(
width + durationPlaceable.width / 2 - barLabelPlaceable.width / 2,
textHeight
)
width += xLabelMaxWidth + xLabelSpace
}
yLabelsPlaceables.forEachIndexed { index, it ->
val height = heightBaseline - (groupAreaHeight - xAxisHeight) *
(yLabelsInfo[index].position - minYPosition) / (maxYPosition - minYPosition)
it.place(
yLabelMaxWidth / 2 - it.width / 2,
height + xAxisHeight / 2 - it.height / 2
)
}
}
}
groupArea의 y축 가이드라인이랑 yLabel의 위치가 맞도록 고려해야 잘 계산해줘야 한다.
@Stable
fun Modifier.timeGraphBar(
duration: Int,
): Modifier {
return then(
TimeGraphParentData(
duration = duration,
)
)
}
class TimeGraphParentData(
val duration: Int,
) : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): TimeGraphParentData =
this@TimeGraphParentData
}