Compose를 사용하여 도넛형 그래프를 그리는 컴포저블 함수를 만들었는데, 그래프를 그릴 때 각 조각의 각도를 애니메이션을 통해 그려지도록 수정하였다.
데이터의 합을 계산하고, 각 데이터 값을 전체 데이터 합으로 나누어 각도를 계산한다.
val total = data.sum() /* * 비율을 360도로 변환 * 도넛형 그래프는 원형이므로 전체 원은 360도이다. */ val angles = data.map { it / total * 360f }
각 데이터 조각에 대해 Animatable 객체를 생성하여 초기 각도를 0으로 설정하며, data가 변경될 때마다 리스트를 재생성하여 그래프를 그린다.
val angleList = remember(data) { angles.map { Animatable(0f) } }
LaunchedEffect(data) { // data가 변경될 때마다 애니메이션을 다시 시작
angleList.forEachIndexed { index, value ->
launch {
value.snapTo(0f) // 애니메이션 시작 전 각도를 0으로 설정
value.animateTo( // 각도를 목표 각도까지 애니메이션을 적용
targetValue = angles[index],
animationSpec = tween( // 애니메이션을 정의할 때 사용하는 함수로 시간에 따라 값이 변경되는 함수로 애니메이션 시작 값에서 목표 값까지 일정 속도로 변화한다.
durationMillis = 1000, // 애니메이션이 1초 동안 지속
easing = LinearOutSlowInEasing // 애니메이션의 속도 곡선을 설정
)
)
}
}
}
Canvas(modifier = modifier.height(graphHeight.dp)) { // 캔버스를 생성하여 그래프 그리기
val strokeWidth = graphHeight.dp.toPx() / 4 // 도넛형 그래프의 두께
val radius = (graphHeight.dp.toPx() - strokeWidth) / 2 // 도넛형 그래프의 반지름 계산
// 도넛형 그래프의 중심 좌표 설정
val centerX = size.width / 2f
val centerY = radius + strokeWidth / 2
drawGraph(colors, radius, strokeWidth, centerX, centerY, angleList)
}
var startAngle = -90f // 그래프의 시작 각도 설정
angleList.forEachIndexed { index, value ->
val color = colors[index] // 해당 조각의 색상 설정
val rangeAngle = value.value // 애니메이션 된 각도 가져오기
drawArc(
color = color,
startAngle = startAngle, // 시작 각도
sweepAngle = rangeAngle, // 그려질 각도
useCenter = false, // 도넛형 그래프이므로 중심을 사용하지 않는다
style = Stroke(width = strokeWidth), // 선의 두께를 설정
// 조각의 크기와 위치를 설정
topLeft = Offset(centerX - radius, centerY - radius),
size = androidx.compose.ui.geometry.Size(radius * 2, radius * 2)
)
startAngle += rangeAngle // 다음 조각의 시작 각도 업데이트
}
@Composable
internal fun AccountBookGraph(
modifier: Modifier = Modifier,
colors: List<Color>, // 데이터 조각마다 적용할 색상 리스트
data: List<Float>, // 그래프를 그릴 데이터 리스트
graphHeight: Int // 그래프의 높이
) {
val total = data.sum()
val angles = data.map { it / total * 360f }
val angleList = remember(data) { angles.map { Animatable(0f) } }
LaunchedEffect(data) {
angleList.forEachIndexed { index, value ->
launch {
value.snapTo(0f)
value.animateTo(
targetValue = angles[index],
animationSpec = tween(
durationMillis = 1000,
easing = LinearOutSlowInEasing
)
)
}
}
}
Canvas(modifier = modifier.height(graphHeight.dp)) {
val strokeWidth = graphHeight.dp.toPx() / 4
val radius = (graphHeight.dp.toPx() - strokeWidth) / 2
val centerX = size.width / 2f
val centerY = radius + strokeWidth / 2
drawGraph(colors, radius, strokeWidth, centerX, centerY, angleList)
}
}
private fun DrawScope.drawGraph(
colors: List<Color>,
radius: Float,
strokeWidth: Float,
centerX: Float,
centerY: Float,
angleList: List<Animatable<Float, AnimationVector1D>>
) {
var startAngle = -90f
angleList.forEachIndexed { index, value ->
val color = colors[index]
val rangeAngle = value.value
drawArc(
color = color,
startAngle = startAngle,
sweepAngle = rangeAngle,
useCenter = false,
style = Stroke(width = strokeWidth),
topLeft = Offset(centerX - radius, centerY - radius),
size = androidx.compose.ui.geometry.Size(radius * 2, radius * 2)
)
startAngle += rangeAngle
}
}