Jetpack Compose Arc/Circular Progress Bar Animation (Как перезапустить анимацию)

Как создать анимацию индикатора выполнения дуги, подобную этой

В настоящее время я уже использовал Canvas для рисования дуги и добавил анимацию в индикатор выполнения с помощью API animateFloatAsState. Но второе фото не то, что я ожидал.

[Jetpack Compose Arc/Circular Progress Bar Animation (Как перезапустить анимацию)]

// e.g. oldScore = 100f  newScore = 350f
// Suppose 250 points are into one level

@Composable
fun ArcProgressbar(
    modifier: Modifier = Modifier,
    oldScore: Float,
    newScore: Float,
    level: String,
    startAngle: Float = 120f,
    limitAngle: Float = 300f,
    thickness: Dp = 8.dp
) {

    var value by remember { mutableStateOf(oldScore) }

    val sweepAngle = animateFloatAsState(
        targetValue = (value / 250) * limitAngle,  // convert the value to angle
        animationSpec = tween(
            durationMillis = 1000
        )
    )

    LaunchedEffect(Unit) {
        delay(1500)
        value = newScore
    }

    Box(modifier = modifier.fillMaxWidth()) {

        Canvas(
            modifier = Modifier
                .fillMaxWidth(0.45f)
                .padding(10.dp)
                .aspectRatio(1f)
                .align(Alignment.Center),
            onDraw = {
                // Background Arc
                drawArc(
                    color = Gray100,
                    startAngle = startAngle,
                    sweepAngle = limitAngle,
                    useCenter = false,
                    style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
                    size = Size(size.width, size.height)
                )

                // Foreground Arc
                drawArc(
                    color = Green500,
                    startAngle = startAngle,
                    sweepAngle = sweepAngle.value,
                    useCenter = false,
                    style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
                    size = Size(size.width, size.height)
                )
            }
        )
        
        Text(
            text = level,
            modifier = Modifier
                .fillMaxWidth(0.125f)
                .align(Alignment.Center)
                .offset(y = (-10).dp),
            color = Color.White,
            fontSize = 82.sp
        )

        Text(
            text = "LEVEL",
            modifier = Modifier
                .padding(bottom = 8.dp)
                .align(Alignment.BottomCenter),
            color = Color.White,
            fontSize = 20.sp
        )
    }
}

Как я могу анимировать с самого начала, если процент прогресса превышает 100%, как на картинке. У кого-нибудь есть идеи? Спасибо!

Привет, я сделал еще один ответ, так как первый на самом деле не похож на опубликованный вами GIF, но я не хочу его удалять, поскольку вы уже приняли его, и для будущих читателей некоторые люди могут его увидеть и найти полезным. Это полный исходный код, который вы можете просто скопировать и вставить (желательно в отдельный файл .kt) без каких-либо хлопот.

z.g.y 23.11.2022 11:20
6
1
379
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я внес некоторые изменения в ваш код, чтобы использовать Animatable, поэтому мы всегда snap в начало, прежде чем анимировать наше значение target. Мы также исключили вычисления здесь, так как мы просто хотим заполнять весь прогресс каждый раз, когда обновляется оценка, в нашем случае до 300 (limitAngle), и использовали состояние newScore как key в LaunchedEffect, чтобы запускать анимацию каждый раз, когда она увеличивается. Не обращайте внимания на приращения +30, это просто произвольное значение, которое вы можете изменить, не влияя на анимацию.

@Composable
fun ArcProgressbar(
    modifier: Modifier = Modifier,
    newScore: Float,
    level: String,
    startAngle : Float = 120f,
    limitAngle: Float = 300f,
    thickness: Dp = 8.dp
) {

    val animateValue = remember { Animatable(0f) }

    LaunchedEffect(newScore) {
        if (newScore > 0f) {
            animateValue.snapTo(0f)
            delay(10)
            animateValue.animateTo(
                targetValue = limitAngle,
                animationSpec = tween(
                    durationMillis = 1000
                )
            )
        }
    }

    Box(modifier = modifier.fillMaxWidth()) {

        Canvas(
            modifier = Modifier
                .fillMaxWidth(0.45f)
                .padding(10.dp)
                .aspectRatio(1f)
                .align(Alignment.Center),
            onDraw = {
                // Background Arc
                drawArc(
                    color = Color.Gray,
                    startAngle = startAngle,
                    sweepAngle = limitAngle,
                    useCenter = false,
                    style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
                    size = Size(size.width, size.height)
                )

                // Foreground Arc
                drawArc(
                    color = Color.Green,
                    startAngle = startAngle,
                    sweepAngle = animateValue.value,
                    useCenter = false,
                    style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
                    size = Size(size.width, size.height)
                )
            }
        )

        Column {
            Text(
                text = level,
                modifier = Modifier
                    .fillMaxWidth(0.125f)
                    .offset(y = (-10).dp),
                color = Color.Gray,
                fontSize = 82.sp
            )

            Text(
                text = "LEVEL",
                modifier = Modifier
                    .padding(bottom = 8.dp),
                color = Color.Gray,
                fontSize = 20.sp
            )

            Text(
                text = "Score ( $newScore ) ",
                modifier = Modifier
                    .padding(bottom = 8.dp),
                color = Color.Gray,
                fontSize = 20.sp
            )
        }
    }
}

Пример использования:

@Composable
fun ScoreGenerator() {

    var newScore by remember {
        mutableStateOf(0f)
    }

    Column {
        Button(onClick = {
            newScore += 30f
        }) {
            Text("Add Score + 30")
        }

        ArcProgressbar(
            newScore = newScore,
            level = ""
        )
    }
}

Извините за то, что я не описал вопрос хорошо, я немного исправляю описание. Интересно, если сохранить +30 и более 100%, как я могу снова анимировать с 0%? Например, если 200%, я ожидаю, что зеленый индикатор сделает круг дважды. (0%-100%) * 2

Claire 22.11.2022 03:16

Я вижу, я не очень хорошо разбираюсь в анимации, и должен быть лучший способ, но я внес некоторые изменения в ваш код, используя Animatable, и отредактировал свой ответ.

z.g.y 22.11.2022 05:35

Нет проблем !, рад помочь! Я бы также посоветовал пересмотреть, я действительно ожидаю, что кто-то лучше меня в этом обеспечит более эффективный способ того, чего вы хотите достичь здесь, вы также можете проверить официальные документы для других анимаций , но в любом случае, спасибо за принятие ответа!

z.g.y 22.11.2022 11:40
Ответ принят как подходящий

Мой первый ответ не кажется справедливым, поскольку он далек от опубликованного вами gif, который показывает, что вы хотите.

Итак, вот еще один, который очень похож на него. Тем не менее, я чувствую, что эта реализация не очень эффективна с точки зрения вызова последовательностей анимаций, но с точки зрения re-composition я включил некоторую стратегию оптимизации под названием отложенное чтение, убедившись, что только составные объекты, которые наблюдают значения, будут единственными частями который будет перекомпонован. Я оставил оператор Log в родительском прогрессе, который можно компоновать, чтобы убедиться, что ArcProgressbar не обновляется без необходимости, когда прогресс анимируется.

Log.e("ArcProgressBar", "Recomposed")

Полный исходный код, который вы можете скопировать и вставить (желательно в отдельный файл) без каких-либо проблем.

val maxProgressPerLevel = 200 // you can change this to any max value that you want
val progressLimit = 300f

fun calculate(
    score: Float,
    level: Int,
) : Float {
    return (abs(score - (maxProgressPerLevel * level)) / maxProgressPerLevel) * progressLimit
}

@Composable
fun ArcProgressbar(
    modifier: Modifier = Modifier,
    score: Float
) {

    Log.e("ArcProgressBar", "Recomposed")

    var level by remember {
        mutableStateOf(score.toInt() / maxProgressPerLevel)
    }

    var targetAnimatedValue = calculate(score, level)
    val progressAnimate = remember { Animatable(targetAnimatedValue) }
    val scoreAnimate = remember { Animatable(0f) }
    val coroutineScope = rememberCoroutineScope()

    LaunchedEffect(level, score) {

        if (score > 0f) {

            // animate progress
            coroutineScope.launch {
                progressAnimate.animateTo(
                    targetValue = targetAnimatedValue,
                    animationSpec = tween(
                        durationMillis = 1000
                    )
                ) {
                    if (value >= progressLimit) {

                        coroutineScope.launch {
                            level++
                            progressAnimate.snapTo(0f)
                        }
                    }
                }
            }
            
            // animate score
            coroutineScope.launch {

                if (scoreAnimate.value > score) {
                    scoreAnimate.snapTo(0f)
                }

                scoreAnimate.animateTo(
                    targetValue = score,
                    animationSpec = tween(
                        durationMillis = 1000
                    )
                )
            }
        }
    }

    Column(
        modifier = modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Box {
            PointsProgress(
                progress = {
                    progressAnimate.value // deferred read of progress
                }
            )

            CollectorLevel(
                modifier = Modifier.align(Alignment.Center),
                level = {
                    level + 1 // deferred read of level
                }
            )
        }

        CollectorScore(
            modifier = Modifier.padding(top = 16.dp),
            score = {
                scoreAnimate.value // deferred read of score
            }
        )
    }
}

@Composable
fun CollectorScore(
    modifier : Modifier = Modifier,
    score: () -> Float
) {
    Column(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {

        Text(
            text = "Collector Score",
            color = Color.White,
            fontSize = 16.sp
        )

        Text(
            text = "${score().toInt()} PTS",
            color = Color.White,
            fontSize = 40.sp
        )
    }
}

@Composable
fun CollectorLevel(
    modifier : Modifier = Modifier,
    level: () -> Int
) {
    Column(
        modifier = modifier,
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {

        Text(
            modifier = Modifier
                .padding(top = 16.dp),
            text = level().toString(),
            color = Color.White,
            fontSize = 82.sp
        )

        Text(
            text = "LEVEL",
            color = Color.White,
            fontSize = 16.sp
        )
    }
}

@Composable
fun BoxScope.PointsProgress(
    progress: () -> Float
) {

    val start = 120f
    val end = 300f
    val thickness = 8.dp

    Canvas(
        modifier = Modifier
            .fillMaxWidth(0.45f)
            .padding(10.dp)
            .aspectRatio(1f)
            .align(Alignment.Center),
        onDraw = {
            // Background Arc
            drawArc(
                color = Color.LightGray,
                startAngle = start,
                sweepAngle = end,
                useCenter = false,
                style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
                size = Size(size.width, size.height)
            )

            // Foreground Arc
            drawArc(
                color = Color(0xFF3db39f),
                startAngle = start,
                sweepAngle = progress(),
                useCenter = false,
                style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
                size = Size(size.width, size.height)
            )
        }
    )
}

Пример использования:

@Composable
fun PrizeProgressScreen() {

    var score by remember {
        mutableStateOf(0f)
    }

    var scoreInput by remember {
        mutableStateOf("0")
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color(0xFF6b4cba)),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {

        Text(
            modifier = Modifier
                .padding(vertical = 16.dp),
            text = "Progress for every level up: $maxProgressPerLevel",
            color = Color.LightGray,
            fontSize = 16.sp
        )

        ArcProgressbar(
            score = score,
        )

        Button(onClick = {
            score += scoreInput.toFloat()
        }) {
            Text("Add Score")
        }

        TextField(
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
            value = scoreInput,
            onValueChange = {
                scoreInput = it
            }
        )
    }
}

Спасибо за такой внимательный образец!

Claire 24.11.2022 11:53

Другие вопросы по теме