Search code examples
kotlinanimationcanvasandroid-jetpack-composeandroid-animation

Using Jetpack Compose, how do you create an arc that decreases in length when a timer value decreases?


Using Jetpack Compose, I'm trying to create an animated arc. The animation that occurs involves the sweep angle of the arc decreasing in size when the value of a timer decreases. This is a pretty common animation, for example just look at the timer app on your phone.

Despite it's common use I still can't figure out how to implement it. I have successfully developed a feature where the arc sweep angle animates down but it only starts at 360 degrees if the timer is equal to 100. If the time starts at less than 100 than the sweep angle will be less than 360.

I need the sweep angle to start at 360 regardless of the starting time. Here is the code

Column(
modifier = Modifier.fillMaxSize()
){

var time by remember { mutableIntStateOf(100) }

var sweepAngle by mutableFloatStateOf(360f)

val animatedSweepAngle = animateFloatAsState(
       targetValue = countDownTimerSweepAngle,
   )

// Function That Starts The Timer
LaunchedEffect(time){
    while (playTime > 0) {
        time --
        sweepAngle = (time / 100f) * 360
        delay(1000)
        }
}

// Time
Text(text = time.toString())

// Arc
Box(
    modifier = Modifier
        .size(100.dp)
        .background(black)
        .drawBehind{
             drawArc(
                color = white,
                startAngle = 90f,
                sweepAngle = animatedSweepAngle.value,
                useCenter = false,
                style = Stroke(
                    width = 3.dp.toPx(),
                    cap = StrokeCap.Round
                   )
                )
            }
    )

}

If you run this code everything works fine but if you change the value of the time then it will fall apart. How do I fix this? Thanks


Solution

  • Your issue is that the sweepAngle calculation depends on time / 100f * 360, which assumes time starts at 100. If time starts at a different value (e.g., 50), the initial sweepAngle is not 360.

    Instead of hardcoding 100 as the full time, you should dynamically base the sweepAngle calculation on the initial time value.

    I think this would resolve your issue:

    @Composable
    fun TimerArcScreen(startingTime: Int) {
       var time by remember { mutableIntStateOf(startingTime) }
       val totalTime = startingTime // Store the initial time to normalize progress
    
       var sweepAngle by remember { mutableFloatStateOf(360f) }
    
       // Animate the sweep angle smoothly
       val animatedSweepAngle by animateFloatAsState(
        targetValue = sweepAngle,
        animationSpec = tween(durationMillis = 500, easing = LinearEasing)
    )
    
    // Function That Starts The Timer
    LaunchedEffect(time) {
        while (time > 0) {
            time--
            sweepAngle = (time / totalTime.toFloat()) * 360f // Normalize based on the starting time
            delay(1000)
        }
    }
    
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center
    ) {
        // Time Display
        Text(text = time.toString(), style = MaterialTheme.typography.headlineMedium)
    
        // Arc
        Box(
            modifier = Modifier
                .size(150.dp)
        ) {
            Canvas(modifier = Modifier.fillMaxSize()) {
                drawArc(
                    color = Color.White,
                    startAngle = 270f, // Start from the top
                    sweepAngle = animatedSweepAngle,
                    useCenter = false,
                    style = Stroke(
                        width = 8.dp.toPx(),
                        cap = StrokeCap.Round
                    )
                )
            }
        }
    }
    

    }