You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Compose饼图仅首次点击触发动画问题修复求助

修复Compose饼图切片点击动画仅首次生效的问题

问题分析

你的饼图动画仅首次点击生效,核心原因有两点:

  • 动画变量animatedWidth在首次放大后没有重置回初始宽度,后续点击时从放大状态再次放大,无视觉变化。
  • 重复点击同一切片时,clickedItemIndex的值未发生改变,导致依赖它的LaunchedEffect不会重新执行,动画无法触发。

修复步骤

  1. 重置动画初始状态:每次触发动画前,先将animatedWidth恢复到原始的sliceWidth,再执行放大动画。
  2. 支持重复点击触发动画:点击同一切片时,先将clickedItemIndex设为-1,再重新赋值为目标索引,强制触发LaunchedEffect。
  3. 修正累计角度计算:原代码中progressSize使用可变列表在LaunchedEffect中累加会导致数据重复,改用remember计算正确的累计角度列表。

修改后的完整代码

@Composable
internal fun PieChart(
    modifier: Modifier = Modifier,
    colors: List<Color>,
    inputValues: List<Float>,
    textColor: Color = MaterialTheme.colorScheme.primary,
) {
    val chartDegrees = 360f
    var startAngle = 270f
    val proportions = remember(inputValues) {
        inputValues.map {
            it * 100 / inputValues.sum()
        }
    }
    val angleProgress = remember(proportions) {
        proportions.map { prop ->
            chartDegrees * prop / 100
        }
    }

    val clickedItemIndex = remember {
        mutableStateOf(-1)
    }

    // 修正累计角度计算,避免重复累加
    val progressSize = remember(angleProgress) {
        buildList {
            var total = 0f
            angleProgress.forEach { angle ->
                total += angle
                add(total)
            }
        }
    }

    val density = LocalDensity.current
    val textFontSize = with(density) { 30.dp.toPx() }
    val textPaint = remember {
        Paint().apply {
            color = textColor.toArgb()
            textSize = textFontSize
            textAlign = Paint.Align.CENTER
        }
    }

    Box(Modifier.padding(24.dp)) {
        BoxWithConstraints(
            modifier = modifier.fillMaxWidth(),
            contentAlignment = Alignment.Center
        ) {

            val canvasSize = min(constraints.maxWidth, constraints.maxHeight)
            val size = Size(canvasSize.toFloat(), canvasSize.toFloat())
            val canvasSizeDp = with(density) { canvasSize.toDp() }
            val sliceWidth = with(LocalDensity.current) { 20.dp.toPx() }
            val animatedWidth = remember { Animatable(initialValue = sliceWidth) }

            LaunchedEffect(clickedItemIndex.value) {
                if (clickedItemIndex.value != -1) {
                    // 先重置回原始宽度,再执行放大动画
                    animatedWidth.snapTo(sliceWidth)
                    animatedWidth.animateTo(
                        targetValue = sliceWidth * 1.5f,
                        animationSpec = tween(durationMillis = 600),
                    )
                } else {
                    // 未选中时重置宽度
                    animatedWidth.animateTo(sliceWidth)
                }
            }

            Canvas(
                modifier = Modifier
                    .size(canvasSizeDp)
                    .pointerInput(inputValues) {
                        detectTapGestures { offset ->
                            val clickedAngle = touchPointToAngle(
                                width = canvasSize.toFloat(),
                                height = canvasSize.toFloat(),
                                touchX = offset.x,
                                touchY = offset.y,
                                chartDegrees = chartDegrees
                            )
                            progressSize.forEachIndexed { index, item ->
                                if (clickedAngle <= item) {
                                    // 处理重复点击:先设为-1再赋值,强制触发LaunchedEffect
                                    clickedItemIndex.value = if (clickedItemIndex.value == index) {
                                        -1
                                    } else {
                                        index
                                    }
                                    return@detectTapGestures
                                }
                            }
                        }
                    }
            ) {

                angleProgress.forEachIndexed { index, angle ->
                    val strokeWidth =
                        if (index == clickedItemIndex.value)
                            animatedWidth.value
                        else sliceWidth
                    drawArc(
                        color = colors[index],
                        startAngle = startAngle,
                        sweepAngle = angle,
                        useCenter = false,
                        size = size,
                        style = Stroke(width = strokeWidth)
                    )
                    startAngle += angle
                }

                if (clickedItemIndex.value != -1) {
                    drawIntoCanvas { canvas ->
                        canvas.nativeCanvas.drawText(
                            "${proportions[clickedItemIndex.value].roundToInt()}%",
                            ((canvasSize / 2).toFloat()),
                            ((canvasSize / 2).toFloat()),
                            textPaint
                        )
                    }
                }
            }
        }
    }
}

关键修改说明

  • 动画重置:在LaunchedEffect中,先调用animatedWidth.snapTo(sliceWidth)将宽度重置,再执行放大动画,确保每次点击都能看到从原始到放大的过渡。
  • 重复点击处理:在点击逻辑中,判断如果当前点击的是已选中的切片,先将clickedItemIndex设为-1,再重新赋值,强制触发LaunchedEffect执行动画。
  • 累计角度优化:用buildList在remember中一次性计算累计角度,避免原代码中可变列表重复累加导致的角度错误。

内容的提问来源于stack exchange,提问作者Niyas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 00:07:21