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

Jetpack Compose技术问询:MotionLayout内部能否使用AnimatedVisibility?

问题解答

核心结论

在MotionLayout内部使用AnimatedVisibility完全符合Jetpack Compose的规范,两者同属Compose动画体系,可配合实现复杂的复合动画效果。

现有实现分析

你的代码已完成两个核心需求:

  • 启动页元素的入场动画(通过AnimatedVisibility的enter参数定义)
  • 数据加载完成后启动页收缩为AppBar的布局过渡动画(通过MotionScene约束集实现)

但当前实现存在可优化点:

  1. 冗余状态管理:同时用animateFloatAsState控制MotionLayout进度,又单独维护motionHeight、fontSize的动画状态,这些属性完全可以在MotionScene中统一定义,减少状态维护成本。
  2. 动画协同性:入场动画与收缩动画的触发时机未做协同,可能出现视觉冲突。

优化建议

1. 将高度、字体动画整合到MotionScene

修改splash_app_bar_motion_scene,把高度、字体大小的过渡逻辑加入约束集:

{
  ConstraintSets: {
    start: {
      parent: {
        height: "parent"
      },
      logo:{
        width: 200,
        height: 200,
        start: ['parent', 'start'],
        end: ['parent', 'end'],
        top: ['parent', 'top'],
        bottom: ['parent', 'bottom']
      },
      title: {
        start: ['logo', 'start'],
        end: ['logo', 'end'],
        top: ['logo', 'bottom'],
        fontSize: 96
      }
    },
    end: {
      parent: {
        height: 56
      },
      logo:{
        width: 24,
        height: 24,
        start: ['parent', 'start', 16],
        top: ['parent', 'top', 12],
        bottom: ['parent', 'bottom', 12],
      },
      title: {
        start: ['logo', 'end', 16],
        top: ['logo', 'top'],
        bottom: ['logo', 'bottom'],
        fontSize: 20
      }
    }
  },
  Transitions: {
    default: {
      from: 'start',
      to: 'end',
      pathMotionArc: 'startVertical',
      duration: 2000,
      KeyFrames: {
        KeyAttributes: [
          {
            target: ['logo'],
            frames: [0, 100],
            rotationZ: [0,  360]
          },
          {
            target: ['title'],
            frames: [0, 20, 50, 80, 100],
            translationY: [0,-100, -45, -10, 0],
            translationX: [0, 0, -80, -85, 0],
          }
        ]
      }
    }
  }
}

简化SplashAppBar的状态逻辑,移除冗余的动画状态:

fun SplashAppBar(
    collapsed: Boolean,
    hideEnterAnimation: Boolean = true,
) {
    val context = LocalContext.current
    val motionScene = remember {
        context.resources.openRawResource(R.raw.splash_app_bar_motion_scene).readBytes()
            .decodeToString()
    }

    val progress by animateFloatAsState(
        targetValue = if (collapsed) 1f else 0f,
        tween(2000)
    )

    var visible by remember { mutableStateOf(hideEnterAnimation) }
    val density = LocalDensity.current
    LaunchedEffect(key1 = true) {
        visible = true
    }

    MotionLayout(
        motionScene = MotionScene(content = motionScene),
        progress = progress,
        modifier = Modifier
            .fillMaxWidth()
            .background(
                Brush.verticalGradient(
                    listOf(
                        Color.Black,
                        BlueGray500
                    )
                ),
            )
    ) {
        AnimatedVisibility(
            visible = visible,
            enter = slideInVertically(
                animationSpec = tween(
                    durationMillis = 1000,
                    easing = EaseOutCirc
                )
            ) {
                with(density) { 200.dp.roundToPx() }
            } + fadeIn(initialAlpha = 0.3f),
            modifier = Modifier.layoutId("title"),
        ) {
            Text(
                text = "Foodie",
                fontSize = motionStyle.fontSize,
                color = Color.White,
                style = if (progress > 0.5f) MaterialTheme.typography.h6 else MaterialTheme.typography.h1,
            )
        }
        AnimatedVisibility(
            visible = visible,
            enter = slideInVertically(
                animationSpec = tween(
                    durationMillis = 2000,
                    easing = EaseOutBounce
                )
            ) {
                with(density) { -500.dp.roundToPx() }
            } + fadeIn(initialAlpha = 0.3f),
            modifier = Modifier.layoutId("logo"),
        ) {
            Image(
                painter = painterResource(R.drawable.foodie_logo),
                contentDescription = "Foodie logo",
            )
        }
    }
}

2. 优化动画触发时机

若希望入场动画完成后再启动收缩动画,可调整LaunchedEffect逻辑:

// 在SplashAppBar中添加
val enterAnimationCompleted = remember { mutableStateOf(false) }
LaunchedEffect(key1 = true) {
    visible = true
    // 等待最长入场动画完成(2000ms)
    delay(2000)
    enterAnimationCompleted.value = true
}

// 仅当入场动画完成且collapsed为true时,触发收缩动画
val progress by animateFloatAsState(
    targetValue = if (collapsed && enterAnimationCompleted.value) 1f else 0f,
    tween(2000)
)

3. 避免重复加载MotionScene

将MotionScene的加载逻辑提取为全局缓存,避免每次重组重复读取资源:

// 全局缓存或使用rememberSaveable
val splashMotionScene by rememberSaveable {
    mutableStateOf(
        MotionScene(
            content = LocalContext.current.resources.openRawResource(R.raw.splash_app_bar_motion_scene)
                .readBytes()
                .decodeToString()
        )
    )
}

总结

MotionLayout擅长处理复杂布局过渡,AnimatedVisibility擅长元素显隐动画,二者组合是合法且高效的实现方式。通过将分散的动画状态统一交给MotionLayout管理,可提升代码简洁度与动画协同性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 00:54:54