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

Jetpack Compose实现Box先扩宽再增高显内容的自定义动画求助

Jetpack Compose完全可以实现你描述的顺序展开动画效果。你提到的AnimationVector是Compose动画框架的底层数据类型,用于封装不同维度的动画数值,普通业务开发不需要直接调用,使用官方封装的上层动画API即可满足需求。

需要学习的核心API

  • animate*AsState 系列:你已经在使用的单属性动画API,适合对宽度、高度、透明度等单个属性做目标值切换的过渡动画,使用成本低,适合简单动画场景。
  • Animatable:比animate*AsState更灵活的单属性动画API,支持在协程中控制动画的启动、停止,还可以等待动画执行完成,适合多阶段顺序执行的动画场景,无需硬编码动画延迟时间。
  • Transition API:针对多状态联动的动画场景设计,你可以定义多个业务状态(如隐藏、宽度展开完成、高度展开完成),所有动画的执行时机、时长都和状态绑定,不需要单独维护多个动画状态的同步逻辑。
  • 透明度动画:通过animateFloatAsState控制Text的alpha属性即可实现纯淡入效果,避免AnimatedVisibility和父容器裁剪共同作用带来的滑动错觉。

现有代码优化方案

你当前的实现存在三个可优化点:

  1. 冗余的隐藏文本组件:不需要提前添加不可见的重复文本测量尺寸,给外层容器开启裁剪,文本正常放在容器内,初始宽高设为极小值,动画过程中容器会自动裁剪超出范围的内容即可。
  2. 硬编码的动画延迟:当前你通过写死延迟时间保证宽度动画结束后再执行高度动画,后续修改动画时长需要同步修改多处配置,容易出错。可以在协程中顺序执行动画,等待宽度动画结束后再启动高度动画。
  3. 文本淡入异常:你看到文本从底部出现的效果,是因为父容器高度增加过程中,被裁剪的文本下半部分逐渐露出带来的错觉,直接控制Text的alpha属性即可实现纯淡入效果。

优化后的参考代码

@ExperimentalAnimationApi
@Composable
fun MessageBoxAnimated() {
    val text = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."
    val scope = rememberCoroutineScope()
    val density = LocalDensity.current

    // 动画状态
    val widthAnimatable = remember { Animatable(10.dp, Dp.VectorConverter) }
    val heightAnimatable = remember { Animatable(10.dp, Dp.VectorConverter) }
    val textAlphaAnimatable = remember { Animatable(0f) }

    // 文本最终尺寸,测量一次即可
    var textTargetWidth by remember { mutableStateOf<Dp?>(null) }
    var textTargetHeight by remember { mutableStateOf<Dp?>(null) }

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
        modifier = Modifier.padding(20.dp)
    ) {
        Column(
            Modifier
                .width(widthAnimatable.value)
                .height(heightAnimatable.value)
                .background(Color.Gray)
                .clipToBounds(), // 开启裁剪,超出容器的内容不可见
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(
                text = text,
                modifier = Modifier
                    .padding(20.dp)
                    .alpha(textAlphaAnimatable.value)
                    .onSizeChanged {
                        // 首次测量拿到文本最终需要的宽高
                        if (textTargetWidth == null) {
                            with(density) {
                                textTargetWidth = it.width.toDp() + 40.dp // 加上左右padding
                                textTargetHeight = it.height.toDp() + 40.dp // 加上上下padding
                            }
                        }
                    },
                color = MaterialTheme.colors.onBackground,
                fontFamily = FontFamily.SansSerif
            )
        }

        Button(
            onClick = {
                val targetW = textTargetWidth ?: return@Button
                val targetH = textTargetHeight ?: return@Button
                scope.launch {
                    // 第一步:执行宽度展开动画,时长400ms
                    widthAnimatable.animateTo(
                        targetValue = targetW,
                        animationSpec = tween(
                            durationMillis = 400,
                            easing = LinearOutSlowInEasing
                        )
                    )
                    // 第二步:宽度动画结束后执行高度展开动画,时长400ms
                    launch {
                        heightAnimatable.animateTo(
                            targetValue = targetH,
                            animationSpec = tween(
                                durationMillis = 400,
                                easing = LinearOutSlowInEasing
                            )
                        )
                    }
                    // 延迟200ms(高度动画走到一半)开始淡入文本
                    delay(200)
                    textAlphaAnimatable.animateTo(
                        targetValue = 1f,
                        animationSpec = tween(durationMillis = 200)
                    )
                }
            },
            colors = ButtonDefaults.buttonColors(backgroundColor = Color.Blue)
        ) {
            Text(text = "Press here", color = Color.White)
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.08 20:18:02