[Kotlin / Compose] Donut Chart에 Animation 적용하기

Subeen·2024년 7월 11일
0

Compose

목록 보기
18/20

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
    }
}
profile
개발 공부 기록 🌱

0개의 댓글