Android Compose饼图仅首次点击触发动画问题修复求助
修复Compose饼图切片点击动画仅首次生效的问题
问题分析
你的饼图动画仅首次点击生效,核心原因有两点:
- 动画变量
animatedWidth在首次放大后没有重置回初始宽度,后续点击时从放大状态再次放大,无视觉变化。 - 重复点击同一切片时,
clickedItemIndex的值未发生改变,导致依赖它的LaunchedEffect不会重新执行,动画无法触发。
修复步骤
- 重置动画初始状态:每次触发动画前,先将
animatedWidth恢复到原始的sliceWidth,再执行放大动画。 - 支持重复点击触发动画:点击同一切片时,先将
clickedItemIndex设为-1,再重新赋值为目标索引,强制触发LaunchedEffect。 - 修正累计角度计算:原代码中
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
相关产品推荐
相关产品推荐

