[Android] Circular Progress Indicator

uuranus·2024년 10월 12일
0
post-thumbnail

기존 애니메이션 분석

포켓몬 슬립에 나오는 그 수면 시간 원형 progress indicator를 만들어볼 것이다.

원형 링 그리기

Canvas(
    modifier = modifier
        .fillMaxSize()
) {
    val ringWidth = size.width / 5f

    drawArc(
        color = trackColor,
        startAngle = 0f,
        sweepAngle = 360f,
        useCenter = false,
        style = Stroke(ringWidth)
    )

    drawArc(
        color = color,
        startAngle = -90f,
        sweepAngle = 360 * progress,
        useCenter = false,
        style = Stroke(ringWidth, cap = StrokeCap.Round)
    )
}

애니메이션 생성하기

progress는 호출한 바깥쪽에서 Ease에 맞춰서 값을 전달해주었다.

var progress by remember { mutableFloatStateOf(0f) }
val max = 0.92f

LaunchedEffect(Unit) {
    var time = 0.0f
    while (time <= 1.0f) {
        time += 0.01f
        progress = EaseInOutCirc.transform(time) * max
        delay(16L)
    }
}

Box(
    modifier = Modifier
        .fillMaxSize(),
    contentAlignment = Alignment.Center
) {
    CircularProgress(
        modifier = Modifier
            .width(200.dp)
            .aspectRatio(1f),
        progress = progress
    )
}

최종 progress 숫자 보여주기

프로그래스가 움직이면 최종 숫자와 spark표시가 나타난다.
isDone 변수를 통해서 progress가 끝나면 애니메이션을 시작하도록 했다.

LaunchedEffect(progress) {
    delay(200)
    isDone = true

    launch {
        fontScaleAnimatable.animateTo(
            targetValue = 1.0f,
            animationSpec = tween(200)
        )

        delay(100)

        sparkAlphaAnimatable.animateTo(
            targetValue = 1.0f,
            animationSpec = tween(200)
        )
    }

    launch {
        fontAlphaAnimatable.animateTo(
            targetValue = 1.0f,
            animationSpec = tween(200)
        )
    }
}

Canvas(
    modifier = modifier
        .fillMaxSize()
) {
    val ringWidth = size.width / 5f

    drawArc(
        color = trackColor,
        startAngle = 0f,
        sweepAngle = 360f,
        useCenter = false,
        style = Stroke(ringWidth)
    )

    drawArc(
        color = color,
        startAngle = -90f,
        sweepAngle = 360 * progress,
        useCenter = false,
        style = Stroke(ringWidth, cap = StrokeCap.Round)
    )

    if (isDone) {
        val style = textStyle.copy(
            fontSize = textStyle.fontSize * fontScaleAnimatable.value,
            color = color.copy(alpha = fontAlphaAnimatable.value)
        )

        val measured = textMeasure.measure(
            text = text,
            style = style,
            constraints = Constraints(
                maxWidth = size.width.toInt() * 2 / 3,
                maxHeight = size.height.toInt() * 2 / 3,
            )
        )

        drawText(
            textMeasurer = textMeasure,
            text = text,
            topLeft = Offset(
                (size.width - measured.size.width) / 2,
                (size.height - measured.size.height) / 2
            ),
            style = style
        )

        drawSparkEffect(Color(0xFFFFD356), sparkAlphaAnimatable.value)
    }
}

spark

private fun DrawScope.drawSparkEffect(color: Color, alpha: Float) {
    val numRays = 3
    val arcAngle = (-20f).toRadian()  // 각도 설정
    val longRayLength = (size.width / 2) * 0.95f
    val shortRayLength = (size.width / 2) * 0.6f

    val angleDiff = 23f.toRadian()
    val center = Offset(
        size.width / 2 + sin(arcAngle - angleDiff) * (size.width / 2),
        size.height / 2 - cos(arcAngle - angleDiff) * (size.width / 2),
    )

    val path = Path()

    val angleDistance = 5.5f.toRadian()

    for (i in 0 until numRays) {
        val angle = arcAngle - i * angleDiff

        val leftStart = Offset(
            (center.x + sin(angle - angleDistance) * longRayLength),
            (center.y - cos(angle - angleDistance) * longRayLength),
        )

        val leftEnd = Offset(
            (center.x + sin(angle - angleDistance) * shortRayLength),
            (center.y - cos(angle - angleDistance) * shortRayLength),
        )

        val rightStart = Offset(
            (center.x + sin(angle + angleDistance) * longRayLength),
            (center.y - cos(angle + angleDistance) * longRayLength),
        )

        val rightEnd = Offset(
            (center.x + sin(angle + angleDistance) * shortRayLength),
            (center.y - cos(angle + angleDistance) * shortRayLength),
        )

        path.apply {
            moveTo(leftStart.x, leftStart.y)

            lineTo(
                leftEnd.x,
                leftEnd.y
            )

            lineTo(
                rightEnd.x,
                rightEnd.y
            )

            lineTo(
                rightStart.x,
                rightStart.y
            )

            lineTo(
                leftStart.x,
                leftStart.y
            )
        }
    }

    drawPath(
        path = path,
        color = color.copy(alpha = alpha),

        )
}

중심점을 기준으로 star polygon 그렸을 때처럼 cos, sin 각도를 이용해서 네 꼭짓점을 계산해서 그려줬다.

최종 결과

깃허브 링크

https://github.com/uuranus/compose-animations

profile
Frontend Developer

0개의 댓글