Jetpack Compose 【四】动画

一、传统动画与 Compose 动画的区别

在传统的 Android View 系统中,动画通常需要通过 ViewPropertyAnimatorObjectAnimatorValueAnimator 等 API 来实现。这些动画 API 为开发者提供了属性动画、帧动画等功能,可以对视图的属性(如位置、透明度、大小等)进行动画化。然而,这些动画 API 的使用往往较为复杂,需要手动控制动画的生命周期、插值器等细节,且需要配合 View 的布局和状态管理。

与此不同,Jetpack Compose 提供了一种更加声明式和简洁的方式来处理动画。在 Compose 中,动画通过状态驱动,开发者只需关注数据的变化,而 Compose 会自动根据数据变化更新 UI 并应用动画效果。Compose 的动画 API 设计上更加简洁,通常只需要几个方法就能实现复杂的动画效果。

传统动画(View 系统):

  1. 需要显式创建和管理动画对象。
  2. 通过 View 的属性动画对单个视图进行动画化。
  3. 通常需要额外的状态管理来控制动画执行与生命周期。
  4. 动画效果需要手动计算与插值,过程较为繁琐。

Compose 动画(Jetpack Compose):

  1. 使用声明式编程,动画基于状态变化自动触发。
  2. 通过 animate*AsState 系列 API、TransitionAnimatedVisibility 等直接对 UI 元素进行动画。
  3. 动画生命周期由 Compose 框架管理,自动执行与停止。
  4. 开发者无需手动计算动画过程,只需设置目标状态和动画属性。

因此,Compose 动画不仅简化了动画实现的流程,还增强了动画和状态的紧密结合,极大地提升了开发效率。

二、Compose 动画的实现方式

2.1 AnimateXxxAsState 系列

animate*AsState 系列动画是 Compose 中最常见的动画方式,它允许我们动画化元素的某些属性,如尺寸、颜色和位置等。

示例:尺寸、颜色动画

@Composable
fun AnimatedXXAsStateExample() {
    var expanded by remember { mutableStateOf(false) }
    //尺寸变化
    val size by animateDpAsState(targetValue = if (expanded) 200.dp else 100.dp, label = "")
    //颜色变化
    val color by animateColorAsState(
        targetValue = if (expanded) Color.Red else Color.Blue,
        label = ""
    )

    Box(
        modifier = Modifier
            .size(size)
            .background(color)
            .clickable { expanded = !expanded }
    )
}

在这个示例中,方块的尺寸在 isExpanded 状态变化时平滑过渡,展示了如何使用 animateDpAsState 来实现尺寸动画。

示例:位移动画 (animateDpAsState)

@Composable
fun AnimatedOffsetExample() {
    var isMoved by remember { mutableStateOf(false) }

    // 使用 animateDpAsState 动画化偏移量
    val offsetX by animateDpAsState(
        targetValue = if (isMoved) 200.dp else 0.dp,
        label = "BoxOffset"
    )

    Column(modifier = Modifier.fillMaxSize()) {
        Box(
            modifier = Modifier
                .size(100.dp)
                .offset(x = offsetX) // 位置设置 x 轴偏移
                .background(Color.Blue)
        )

        Spacer(modifier = Modifier.height(20.dp))

        Button(onClick = { isMoved = !isMoved }) {
            Text("Move position")
        }
    }
}

此示例展示了如何通过 animateDpAsState 实现方块的平滑位移动画。

2.2 AnimatedVisibility

AnimatedVisibility 是一个用于控制视图可见性的动画组件。它通过 enterexit 动画来控制视图的显示和隐藏,可以配置多种不同的入场和出场动画效果,包含如下内容:

  • 淡入 : fadeIn / fadeout
  • 缩放 : scaleIn / scaleOut
  • 滑动 : slideIn / slideOut
  • 展开 : expandIn / shrinkOut

示例:淡入淡出 (fadeIn / fadeOut)

@Composable
fun FadeInOutExample() {
    var isVisible by remember { mutableStateOf(true) }

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        AnimatedVisibility(
            visible = isVisible,
            enter = fadeIn(),
            exit = fadeOut()
        ) {
            Box(
                modifier = Modifier
                    .size(100.dp)
                    .background(Color.Blue)
            )
        }

        Spacer(modifier = Modifier.height(20.dp))

        Button(onClick = { isVisible = !isVisible }) {
            Text("切换显示")
        }
    }
}

此示例演示了如何使用 fadeInfadeOut 来实现方块的淡入淡出效果。

示例:缩放 (scaleIn / scaleOut)

@Composable
fun ScaleInScaleOutExample() {
    var visible by remember { mutableStateOf(true) }

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
        modifier = Modifier.fillMaxSize()
    ) {
        AnimatedVisibility(
            visible = visible,
            enter = scaleIn(tween(durationMillis = 500)),
            exit = scaleOut(tween(durationMillis = 500))
        ) {
            Box(
                modifier = Modifier
                    .size(100.dp)
                    .background(Color.Red)
            )
        }

        Spacer(modifier = Modifier.height(20.dp))

        Button(onClick = { visible = !visible }) {
            Text("切换可见性")
        }
    }
}

此例中,方块的显示与隐藏通过缩放动画实现。

示例:滑动 (slideIn / slideOut)

@Composable
fun SlideInOutExample() {
    var isVisible by remember { mutableStateOf(true) }

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        AnimatedVisibility(
            visible = isVisible,
            enter = slideInHorizontally(
                initialOffsetX = { -300 } // 从左侧滑入
            ),
            exit = slideOutHorizontally(
                targetOffsetX = { 300 } // 向右滑出
            )
        ) {
            Box(
                modifier = Modifier
                    .size(120.dp)
                    .background(Color.Green)
            )
        }

        Spacer(modifier = Modifier.height(20.dp))

        Button(onClick = { isVisible = !isVisible }) {
            Text("滑动切换")
        }
    }
}
  • slideInHorizontally

    • initialOffsetX 控制初始位置,负值表示从左侧滑入。
  • slideOutHorizontally

    • targetOffsetX 控制退出时的位置,正值表示向右滑出。

示例:enter/exit 都可以组合这4个动画

@Composable
fun CombinedAnimationExample() {
    var isVisible by remember { mutableStateOf(true) }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        AnimatedVisibility(
            visible = isVisible,
            enter = fadeIn() + scaleIn(initialScale = 0.5f), 
            exit = fadeOut() + scaleOut(targetScale = 1.5f)
        ) {
            Box(
                modifier = Modifier
                    .size(120.dp)
                    .background(Color.Magenta),
                contentAlignment = Alignment.Center
            ) {
                Text("Hello", color = Color.White)
            }
        }

        Spacer(modifier = Modifier.height(20.dp))

        Button(onClick = { isVisible = !isVisible }) {
            Text(if (isVisible) "隐藏" else "显示")
        }
    }
}

该示例展示了淡入 + 缩放组合动画。

2.3 Transition 动画

在 Compose 中,Transition 是一种强大的动画工具,允许开发者在多个状态之间平滑过渡。Transition 使得开发者能够根据不同的状态变化定义一系列的动画过渡效果,从而实现复杂的 UI 动画。它特别适用于那些需要同时动画多个属性(如位置、尺寸、透明度等)的场景。

Transition 通过对多个目标值进行动画处理,可以实现更为丰富和复杂的交互效果,例如在视图状态变化时同时对多个属性进行过渡。

示例1:使用 Transition 实现位置 + 颜色 + 大小 组合动画

@Composable
fun TransitionExample() {
    var isExpanded by remember { mutableStateOf(false) }

    // 使用 updateTransition 来处理多个属性的动画
    val transition = updateTransition(targetState = isExpanded, label = "BoxTransition")

    // 定义动画效果
    val size by transition.animateDp(label = "Size") { state ->
        if (state) 150.dp else 100.dp
    }
    val color by transition.animateColor(label = "Color") { state ->
        if (state) Color.Red else Color.Green
    }
    val offset by transition.animateDp(label = "Offset") { state ->
        if (state) 200.dp else 0.dp
    }

    Column {
        Box(
            modifier = Modifier
                .size(size)
                .offset(x = offset)
                .background(color)
        )

        Spacer(modifier = Modifier.height(20.dp))

        Button(onClick = { isExpanded = !isExpanded }) {
            Text("切换状态")
        }
    }
}

在这个例子中,updateTransition 被用来处理 isExpanded 状态的变化。当状态从 false 变为 true 时,方块的尺寸、颜色和位置都会同步动画过渡。这里通过 animateDpanimateColor 方法分别对尺寸、颜色和位置进行动画化。

示例 2:高级用法:多状态切换动画实现 3 个状态的切换

使用枚举定义状态,实现多状态之间的复杂动画。

enum class BoxState {
    Small, Medium, Large
}

@Composable
fun MultiStateTransition() {
    var boxState by remember { mutableStateOf(BoxState.Small) }

    // 创建多状态 Transition
    val transition = updateTransition(targetState = boxState, label = "MultiStateTransition")

    // 动画:大小变化
    val boxSize by transition.animateDp(label = "BoxSize") { state ->
        when (state) {
            BoxState.Small -> 80.dp
            BoxState.Medium -> 150.dp
            BoxState.Large -> 250.dp
        }
    }

    // 动画:颜色变化
    val boxColor by transition.animateColor(label = "BoxColor") { state ->
        when (state) {
            BoxState.Small -> Color.Red
            BoxState.Medium -> Color.Green
            BoxState.Large -> Color.Blue
        }
    }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Box(
            modifier = Modifier
                .size(boxSize)
                .background(boxColor)
        )

        Spacer(modifier = Modifier.height(20.dp))

        Button(onClick = {
            boxState = when (boxState) {
                BoxState.Small -> BoxState.Medium
                BoxState.Medium -> BoxState.Large
                BoxState.Large -> BoxState.Small
            }
        }) {
            Text("切换状态")
        }
    }
}

Transition 的优势

  • 多属性同步动画Transition 使得多个属性的动画可以同步进行,避免了手动管理多个动画对象。
  • 简洁声明式:动画过程的声明式编程方式使得代码更加简洁、可读。开发者只需关注状态的变化,Compose 会自动处理动画的细节。
  • 动态控制:通过 updateTransition,开发者可以在状态变化过程中灵活调整多个属性的动画效果,从而打造更加丰富的交互体验。

2.4 AnimationSpec动画

AnimationSpec 定义了动画的行为,类似于传统View体系中的差值器Interpolator,包括动画的速度、持续时间、缓动曲线等。它适用于所有 animate* 系列函数(如 animateDpAsStateupdateTransitionAnimatable 等),用于控制动画的执行方式。

常用的 AnimationSpec 类型

类型 描述 适用场景
tween() 补间动画,按时间线性或非线性变化 适用于简单、平滑的动画过渡
spring() 弹性动画,模拟物理世界的弹性和阻尼效果 适用于有弹性的动画,如按钮回弹
keyframes() 关键帧动画,定义多个时间点的动画值 适用于复杂、多阶段的动画
snap() 瞬间完成动画,直接跳到目标值 适用于无过渡效果的快速切换
repeatable() 可重复动画,指定重复次数和方向 适用于循环动画
infiniteRepeatable() 无限循环动画 适用于持续播放的动画(如旋转)

示例1. tween()——补间动画

控制动画的 时长延迟缓动曲线

@Composable
fun TweenAnimation() {
    var isMoved by remember { mutableStateOf(false) }

    val offsetX by animateDpAsState(
        targetValue = if (isMoved) 200.dp else 0.dp,
        animationSpec = tween(
            durationMillis = 1000,      // 动画时长
            delayMillis = 300,          // 动画延迟
            easing = FastOutSlowInEasing // 缓动曲线
        ), label = "OffsetAnimation"
    )

    Column {
        Box(
            Modifier
                .size(100.dp)
                .offset(x = offsetX)
                .background(Color.Blue)
        )
        Button(onClick = { isMoved = !isMoved }) {
            Text("切换动画")
        }
    }
}

tween() 参数:

  • durationMillis:动画持续时间(毫秒)。
  • delayMillis:动画开始前的延迟时间。
  • easing:缓动效果(详见下方缓动函数)。

示例2. spring()——弹性动画

模拟物理世界的 弹性效果,包括弹力和阻尼。

@Composable
fun SpringAnimation() {
    var isExpanded by remember { mutableStateOf(false) }

    val size by animateDpAsState(
        targetValue = if (isExpanded) 200.dp else 100.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy, // 阻尼比
            stiffness = Spring.StiffnessLow                 // 刚度
        ), label = "SpringAnimation"
    )

    Column {
        Box(
            Modifier
                .size(size)
                .background(Color.Green)
        )
        Button(onClick = { isExpanded = !isExpanded }) {
            Text("弹性动画")
        }
    }
}

spring() 参数:

  • dampingRatio:阻尼比,控制动画的回弹程度:

    • DampingRatioNoBouncy:无回弹
    • DampingRatioLowBouncy:轻微回弹
    • DampingRatioMediumBouncy:中等回弹(推荐)
    • DampingRatioHighBouncy:强烈回弹
  • stiffness:刚度,控制动画速度:

    • StiffnessVeryLow:非常慢
    • StiffnessLow:较慢
    • StiffnessMedium:中速(默认)
    • StiffnessHigh:快速

示例3. keyframes()——关键帧动画

自定义动画的各个关键时间点,精确控制动画过程。


@Composable
fun KeyframesAnimation() {
    var isMoved by remember { mutableStateOf(false) }

    val offsetX by animateDpAsState(
        targetValue = if (isMoved) 300.dp else 0.dp,
        animationSpec = keyframes {
            durationMillis = 3000 // 总时长
            50.dp at 500          // 0.5 秒后到 50.dp
            150.dp at 1000        // 1 秒后到 150.dp
            200.dp at 2000        // 2 秒后到 200.dp
        }, label = "KeyframeAnimation"
    )

    Column {
        Box(
            Modifier
                .size(100.dp)
                .offset(x = offsetX)
                .background(Color.Red)
        )
        Button(onClick = { isMoved = !isMoved }) {
            Text("关键帧动画")
        }
    }
}

keyframes() 参数:

  • durationMillis:总动画时长(必须)。
  • at:指定关键帧时间点,格式为 value at time

示例4. repeatable() & infiniteRepeatable()——循环动画

@Composable
fun RepeatAnimation() {
    val infiniteOffset by rememberInfiniteTransition(label = "infinite").animateFloat(
        initialValue = 0f,
        targetValue = 200f,
        animationSpec = infiniteRepeatable(
            animation = tween(1000),  // 每次动画的时长
            repeatMode = RepeatMode.Reverse // 循环方式
        ), label = "RepeatAnimation"
    )

    Box(
        Modifier
            .size(100.dp)
            .offset(x = infiniteOffset.dp)
            .background(Color.Magenta)
    )
}

参数:

  • animation:内部使用 tween()spring()keyframes()

  • repeatMode

    • RepeatMode.Restart:每次重头开始。
    • RepeatMode.Reverse:往返播放(推荐)。

示例5. snap()——瞬间动画

瞬间完成动画,立即切换到目标值。

@Composable
fun SnapAnimation() {
    var isMoved by remember { mutableStateOf(false) }

    val offsetX by animateDpAsState(
        targetValue = if (isMoved) 200.dp else 0.dp,
        animationSpec = snap(delayMillis = 500), label = "SnapAnimation"
    )

    Column {
        Box(
            Modifier
                .size(100.dp)
                .offset(x = offsetX)
                .background(Color.Cyan)
        )
        Button(onClick = { isMoved = !isMoved }) {
            Text("瞬间切换")
        }
    }
}

常用 Easing 缓动函数

缓动函数 描述
LinearEasing 线性匀速
FastOutSlowInEasing 快出慢入,Material Design 标准曲线
EaseIn 慢入,适用于淡入效果
EaseOut 快出,适用于淡出效果
EaseInOut 先慢后快,再慢,适用于对称动画
CubicsBezierEasing 自定义贝塞尔曲线
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容