Compose实现选中时长时白色高亮框的平滑移动动画问题
Jetpack Compose 高亮框动画修复方案
问题分析
你当前遇到两个核心问题:
- 白色高亮框绘制在文本上方,导致被遮挡的文本(比如初始的"5 min")无法显示
- 高亮框的偏移动画未正确关联选中项的位置,动画无法正常触发
修复方案
1. 调整层级解决遮挡问题
Compose 中组件的绘制顺序是代码编写的顺序,后写的组件会覆盖先写的。把高亮框放在文本 Row 的前面,就能让文本显示在高亮框上方。
2. 动态获取选中项的位置和宽度
使用 onGloballyPositioned 监听每个文本项的布局信息,记录它们的位置(x坐标)和宽度,这样高亮框就能精准匹配选中项的位置和尺寸。
3. 绑定动画到选中项的位置
用 animateDpAsState 根据选中项的位置动态计算目标偏移量,实现平滑移动效果。
完整修复代码
import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.layout.boundsInParent @Composable fun DurationSelector() { var selectedIndex by remember { mutableIntStateOf(0) } val items = listOf("5 min", "15 min", "30 min", "1 hour") // 保存每个item的布局边界(位置和宽度) val itemBounds = remember { mutableStateListOf<Rect>() } // 初始化边界列表,避免索引越界 LaunchedEffect(items.size) { itemBounds.clear() repeat(items.size) { itemBounds.add(Rect.Zero) } } // 根据选中项计算目标偏移量和宽度 val targetOffset: Dp by animateDpAsState( targetValue = if (itemBounds.isNotEmpty()) itemBounds[selectedIndex].left.dp else 0.dp, animationSpec = tween(durationMillis = 500), label = "Highlight offset animation" ) val targetWidth: Dp by animateDpAsState( targetValue = if (itemBounds.isNotEmpty()) itemBounds[selectedIndex].width.dp else 0.dp, animationSpec = tween(durationMillis = 500), label = "Highlight width animation" ) Box( modifier = Modifier .fillMaxWidth() .height(48.dp) .background( color = Color(0xff8138FF).copy(0.08f), shape = RoundedCornerShape(12.dp), ) .padding(horizontal = 4.dp, vertical = 4.dp), ) { // 高亮框放在前面,避免遮挡文本 Box( modifier = Modifier .width(targetWidth) .height(40.dp) // 减去上下padding的4dp,和父容器内边距匹配 .background(color = Color.White, shape = RoundedCornerShape(12.dp)) .offset(x = targetOffset, y = 0.dp) ) Row( modifier = Modifier .fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { items.forEachIndexed { index, item -> Text( fontWeight = FontWeight.W600, fontSize = 16.sp, text = item, color = if (selectedIndex == index) Color.Black else Color.DarkGray, modifier = Modifier .clickable { selectedIndex = index } .padding(horizontal = 16.dp) .onGloballyPositioned { layoutCoordinates -> // 记录当前item在父容器中的边界 itemBounds[index] = layoutCoordinates.boundsInParent() } ) } } } }
关键细节说明
- 层级调整:高亮框Box写在Row前面,确保文本绘制在高亮框之上
- 布局信息获取:
onGloballyPositioned获取每个Text在父容器中的相对位置,存入itemBounds列表 - 动画绑定:
animateDpAsState监听选中index的变化,自动计算并动画过渡到目标偏移和宽度 - 尺寸匹配:高亮框的高度设置为40dp(父容器高度48dp减去上下各4dp的padding),宽度随选中文本动态调整,更贴合设计
内容的提问来源于stack exchange,提问作者ant2009
相关产品推荐
相关产品推荐

