android Jetpack合成圆弧/圆形进度条动画(如何重新启动动画)

wlzqhblo  于 2022-11-27  发布在  Android
关注(0)|答案(2)|浏览(179)

如何创建这样的弧形进度条动画

目前我已经用Canvas画了一个弧线,并使用animateFloatAsState API在进度条上添加了动画。但是第二张图片不是我所期望的。
[

](一个字母)

// 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中的那个。有人有什么想法吗?谢谢!

lkaoscv7

lkaoscv71#

我的第一个答案感觉不像是在做任何公正的事情,因为它与你发布的显示你想要什么的gif相去甚远。
这是另一个非常类似的实现。但是,我觉得这个实现在调用动画序列方面效率不是很高,但是在re-composition方面,我加入了一些称为deferred reading的优化策略。确保只有遵守这些值的可组合体是将被重新组合的唯一部分。我在可组合的父进程中留下了一个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
            }
        )
    }
}

第一次

nc1teljy

nc1teljy2#

我在你的代码中做了一些修改来利用Animatable,所以我们总是在动画到target值之前先从snap开始。我们还消除了这里的计算,因为我们只想在每次分数更新时填充整个进程,在我们的情况下是300(limitAngle),并使用newScore状态作为LaunchedEffect中的key,以便在每次递增时触发动画。它只是一个任意值,您可以在不影响动画的情况下对其进行更改。

@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 = ""
        )
    }
}

相关问题