시배's Android

TiTi Side Project | PieChart 컴포넌트 구현하기 본문

Android/TiTi Side Project

TiTi Side Project | PieChart 컴포넌트 구현하기

si8ae 2024. 2. 7. 11:01

TdsPieChart는 TiTI 프로젝트에서 사용되는 커스텀한 Pie Chart입니다. 각각의 task에 대한 누적시간을 퍼센트로 나타내며, 각 task 사이에는 1도의 여백으로 검은색을 화면에 표시합니다. 또한 애니메이션 효과를 지원하여 사용자에게 부드러운 시각적 효과를 제공합니다.

@Composable
fun TdsPieChart(
    modifier: Modifier = Modifier,
    taskData: List<TdsTaskData>,
    colors: List<Color>,
    containsDonut: Boolean = false,
    animationSpec: AnimationSpec<Float> = TweenSpec(durationMillis = 500),
) {
    val transitionProgress = remember(taskData) {
        Animatable(initialValue = 1f)
    }

    LaunchedEffect(taskData) {
        transitionProgress.animateTo(
            targetValue = 1f,
            animationSpec = animationSpec,
        )
    }

    TdsPieChart(
        modifier = modifier,
        taskData = taskData,
        colors = colors,
        progress = transitionProgress.value,
        containsDonut = containsDonut,
    )
}

각각의 task에 대한 애니메이션 효과를 부여하기 위해 Animatable을 사용하여 0부터 1까지의 애니메이션 progress를 가지도록 transitionProgress를 정의합니다. 이렇게 정의된 progress는 Pie Chart를 그릴 때 사용됩니다.

@Composable
private fun TdsPieChart(
    modifier: Modifier = Modifier,
    taskData: List<TdsTaskData>,
    colors: List<Color>,
    progress: Float,
    containsDonut: Boolean = false,
) {
    var startAngle = 270f
    val density = LocalDensity.current

    BoxWithConstraints(
        modifier = modifier,
        contentAlignment = Alignment.Center,
    ) {
        val radius = with(density) { (min(maxWidth, maxHeight) / 3).toPx() }
        val centerX = with(density) { (maxWidth / 2).toPx() }
        val centerY = with(density) { (maxHeight / 2).toPx() }
        val holeRadiusPercent = if (containsDonut) {
            0.5f
        } else {
            0f
        }
        val holeRadius = radius * holeRadiusPercent
        val holeRadiusDp = with(density) { holeRadius.toDp() }

        Canvas(modifier = Modifier.fillMaxSize()) {
            taskData.forEachIndexed { index, pie ->
                val sweepAngle = (pie.progress * 360 - 1) * progress

                drawArc(
                    color = colors[index % colors.size],
                    startAngle = startAngle,
                    sweepAngle = sweepAngle,
                    useCenter = false,
                    topLeft = Offset(centerX - radius, centerY - radius),
                    size = Size(radius * 2, radius * 2),
                    style = Stroke(width = radius - holeRadius),
                )

                startAngle += sweepAngle

                drawArc(
                    color = Color.Black,
                    startAngle = startAngle,
                    sweepAngle = 1 * progress,
                    useCenter = false,
                    topLeft = Offset(centerX - radius, centerY - radius),
                    size = Size(radius * 2, radius * 2),
                    style = Stroke(width = radius - holeRadius),
                )

                startAngle += 1
            }
        }

        if (containsDonut) {
            TdsTaskResultList(
                modifier = Modifier.size(holeRadiusDp * 2),
                taskData = taskData,
                isSpacing = false,
                height = holeRadiusDp * 2 / 5,
                colors = colors,
            )
        }
    }
}

 

TdsPieChart 컴포저 내에서 BoxWithConstraints를 사용하는 이유는 부모 요소에서 주어진 modifier의 크기를 활용하여 Pie Chart의 크기와 위치를 조절하기 위함입니다. BoxWithConstraints는 자식 요소에 대한 크기 제약을 부여하고, 자식 요소가 해당 제약을 활용하여 적절한 크기와 위치를 계산할 수 있도록 도와줍니다.

여기서 maxWidthmaxHeight는 부모 요소에서 제공된 가로 및 세로 최대 크기를 나타냅니다. 이 값을 사용하여 Pie Chart의 반지름과 중심 위치를 계산합니다. 이때, 반지름은 최대 너비와 높이 중 작은 값의 1/3으로 설정하여 원이 화면에 잘 들어가도록 합니다.

 

다음으로, forEach 루프를 사용하여 각 task에 대한 Arc를 그려나갑니다.

각 task에 대해 시작 각도와 호의 각도를 계산하고, drawArc 함수를 사용하여 해당 호를 그립니다. 여기서 사용되는 속성들은 다음과 같습니다.

  • color: 호의 색상을 지정합니다.
  • startAngle: 호의 시작 각도를 지정합니다. 여기서는 이전 task의 끝 부분에서부터 시작합니다.
  • sweepAngle: 호의 각도를 지정합니다. 이 값은 각 task의 진행도에 비례하도록 계산됩니다.
  • useCenter: 호의 중심을 사용할지 여부를 결정합니다. 여기서는 false로 설정하여 호의 중심을 사용하지 않습니다.
  • topLeft, size: 호가 그려질 위치와 크기를 지정합니다. 여기서는 BoxWithConstraints를 통해 계산된 중심 위치와 반지름을 사용합니다.
  • style: 호의 선 스타일을 지정합니다. 여기서는 검은색 여백을 위해 Stroke를 사용합니다.

이렇게 계산된 각 task에 대한 호는 Canvas를 통해 화면에 그려지며, 사용자에게 시각적으로 효과적으로 표현됩니다.