前面讲到布局基础图像绘制,本篇来讲下Jetpack Compose动画。
介绍动画主要从下图中几点进行讲解
动画学习目录

一、内容动画

与布局内容变化相关的几种动画,官方称之为高级别动画API。

  • AnimatedVisibility,实验性功能,可组合项可为内容的出现和消失添加动画效果;
  • AnimatedContent,实验性功能,可组合项在内容根据目标状态发生变化时,添加内容的动画效果;
  • AnimateContentSize,可组合项内容大小发生变化动画;
  • Crossfade,可组合项的淡入淡出;

AnimatedVisibility
使用如下:

var editable by remember { mutableStateOf(true) }
AnimatedVisibility(visible = editable) {
    Text(text = "Edit")
}

AnimatedContent
示例:

AnimatedContent(targetState = member,
            transitionSpec ={
                // Compare the incoming number with the previous number.
                if (targetState > initialState) {
                    // If the target number is larger, it slides up and fades in
                    // while the initial (smaller) number slides up and fades out.
                    slideInVertically({ height -> height }) + fadeIn() with
                            slideOutVertically({ height -> -height }) + fadeOut()
                } else {
                    // If the target number is smaller, it slides down and fades in
                    // while the initial number slides down and fades out.
                    slideInVertically({ height -> -height })+ fadeIn() with
                            slideOutVertically({ height -> height }) + fadeOut()
                }.using(
                    // Disable clipping since the faded slide-in/out should
                    // be displayed out of bounds.
                    SizeTransform(clip = false)
                )
            }
        ) {targetCount->
            Text(text = "$targetCount")
        }

AnimatedContentSize
示例:

Text(text = if (!isExpanded) "点我展开" else "点我收起\n收起",modifier = Modifier
            .fillMaxWidth()
            .animateContentSize()
            .clickable { isExpanded = !isExpanded })

Crossfade
示例:

//UI切换 带淡入淡出动画
          Crossfade(targetState = crossFadeState) {
                Box(modifier = Modifier.background(color = if (it) Color.Green else Color.Gray)) {
                    if (it) Text(text = "Page A",)
                    else Text(text = "Page B")
                }
            }

上述示例展示动画:
内容动画.gif

二、值动画

通知单个或多个值发生变化来设置动画,分为:多值动画单值动画重复动画
多值动画
定义:当状态发生改变时,多个值要一起发生改变。
修饰符:updateTransition
下面以颜色、大小、边框为例设置多值动画

@Composable
private fun MultiValueAnimation(){
    val targetState = remember {
        mutableStateOf(BoxState.Collapsed)
    }
    val transition = updateTransition(
        targetState = targetState,
        label = "hahah"
    )

    val rect by transition.animateRect { state ->
        when (state.value) {
            BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
            BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
        }
    }
    val borderWidth by transition.animateDp { state ->
        when (state.value) {
            BoxState.Collapsed -> 1.dp
            BoxState.Expanded -> 0.dp
        }
    }
    val color by transition.animateColor {state ->
        when(state.value){
            BoxState.Expanded->Color.LightGray
            BoxState.Collapsed->Color.Gray
        }
    }
    Surface(shape = RectangleShape,
        border = BorderStroke(width = borderWidth,color = Color.Blue),
        modifier = Modifier
            .size(rect.width.dp, rect.height.dp)
            .clickable {
                if (targetState.value == BoxState.Expanded) targetState.value =
                    BoxState.Collapsed else targetState.value = BoxState.Expanded
            },
        color = color
    ) {
        Text(text = "多值动画")
    }
}

运行效果:多值动画.gif

单值动画
定义:为单个值添加动画效果。Compose 为 Float、Color、Dp、Size、Offset、Rect、Int、IntOffset 和 IntSize 提供开箱即用的 animate*AsState 函数。通过为接受通用类型的 animateValueAsState 提供 TwoWayConverter,您可以轻松添加对其他数据类型的支持
修饰符:animateXXAsState
animateFloatAsStateanimateOffsetAsState为示例:

@Composable
private fun SingleValueAnimation(){
    var enabled by remember {
        mutableStateOf(false)
    }
    //使用animateFloatAsState变化透明度
    val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
    val offset by animateOffsetAsState(targetValue = if (enabled) Offset.Zero else Offset(10f,10f))
    Image(
        painter = painterResource(id = R.mipmap.ic_girl),
        contentDescription = "avatar",
        modifier = Modifier
            .offset(offset.x.dp, offset.y.dp)
            .alpha(alpha)
            .clickable { enabled = !enabled }
    )
}

运行结果:单值动画.gif

针对单值动画,除了上述使用方式外,也可采用Animatable来实现。
如下:

var enabled by remember {
        mutableStateOf(false)
    }
    val color = remember { Animatable(Color.Gray) }
    LaunchedEffect(enabled) {
        color.animateTo(if (enabled) Color.Green else Color.Red)
    }
    Box(
        Modifier
            .size(60.dp)
            .background(color.value)
            .clickable { enabled = !enabled }
    )

运行效果:Animatable.gif

重复动画
定义:InfiniteTransition 可以像 Transition 一样保存一个或多个子动画,但是,这些动画一进入组合阶段就开始运行,除非被移除,否则不会停止。使用 rememberInfiniteTransition 创建 InfiniteTransition 实例,并使用 animateColor、animatedFloat 或 animatedValue 添加子动画。

@Composable
private fun InfiniteTransitionAnimation(){
    val infiniteTransition = rememberInfiniteTransition()
    val state by infiniteTransition.animateColor(
        initialValue = Color.Red,
        targetValue = Color.Cyan,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 2000,easing = FastOutSlowInEasing),
            repeatMode = RepeatMode.Reverse
        ))
    Box(
        Modifier
            .size(60.dp)
            .background(state)
    )
}

运行结果:重复动画.gif

三、自定义动画

有时候系统提供的默认动画无法满足我们的需求,这样我们就需要进行自定义了。那么怎么自定义呢,先看下代码:

val alpha: Float by animateFloatAsState(
    targetValue = if (enabled) 1f else 0.5f,
    // Configure the animation duration and easing.
    animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
)

看到有个参数为:animationSpec,其类型为AnimationSpec,该参数为可选有默认值,是定义动画的类型。因此想自定义动画,必须给该参数传值了。
先看下有哪些类型的动画:image.png
spring(弹性动画)
定义:可在起始值和结束值之间创建基于物理特性的动画。它接受 2 个参数:dampingRatio 和 stiffness

  • dampingRatio,定义弹簧的弹性。默认值为 Spring.DampingRatioNoBouncy。
  • stiffness,定义弹簧应向结束值移动的速度。默认值为 Spring.StiffnessMedium。
    示例:
/**
 * 弹性动画
 */
@Composable
private fun springAnimation(){
    var enable by remember{ mutableStateOf(true)}
    val value: Int by animateIntAsState(
        targetValue = if (enable) 200 else 50,
        // Configure the animation duration and easing.
        animationSpec = spring(
            //定义弹簧的弹性
            dampingRatio = Spring.DampingRatioHighBouncy,
            //定义弹簧应向结束值移动的速度
            stiffness = Spring.StiffnessHigh
        )
    )
    Box(
        Modifier
            .offset(50.dp)
            .width(value.dp)
            .height(50.dp)
            .background(Color.Blue)
            .clickable { enable = !enable }
    ){
        Text(text = "spring")
    }
}

运行结果spring.gif

tween
在指定的 durationMillis 内使用缓和曲线在起始值和结束值之间添加动画效果。还可以指定 delayMillis 来推迟动画播放的开始时间。
示例:

@Composable
private fun tweenAnimation(){
    var enable by remember{ mutableStateOf(false)}
    val value by animateIntAsState(
        targetValue = if (enable) 150 else 50,
        animationSpec = tween(
            durationMillis = 1000,
            delayMillis = 500,
            easing = LinearOutSlowInEasing
        )
    )
    Box(
        Modifier.width(value.dp)
            .height(50.dp)
            .background(Color.Gray)
            .clickable { enable = !enable }
    ) {
        Text(text = "tween")
    }
}

运行结果:tween.gif
keyframes
定义:会根据在动画时长内的不同时间戳中指定的快照值添加动画效果。在任何给定时间,动画值都将插值到两个关键帧值之间。对于其中每个关键帧,您都可以指定 Easing 来确定插值曲线。
需要设置的配置为:

  • durationMillis 动画执行时长,单位毫秒;
  • delayMillis 延迟时间,单位毫秒;
  • keyframes 关键帧,internal类型不能直接配置,需通过KeyframesSpecConfig的扩展函数atwith结合来设置关键帧信息
    示例如下:
@Composable
private fun keyframesAnimation(){
    var enable by remember{ mutableStateOf(false)}

    val value by animateIntAsState(
        targetValue = if (enable) 200 else 50,
        animationSpec = keyframes {
            durationMillis = 2000   //动画执行时长
            delayMillis = 500       //动画延迟多久后执行
            50 at 0 with LinearOutSlowInEasing   //0 - 200ms执行的帧
            100 at 200 with FastOutLinearInEasing // 200 - 1200ms执行的帧
            150 at 1200 with LinearEasing
        }
    )
    Box(
        Modifier.height(50.dp)
            .width(value.dp)
            .background(Color.Yellow)
            .clickable { enable = !enable }
    ) {
       Text(text = "keyframes")
    }
}

运行结果:keyframes.gif

repeatable(按次数重复动画)
定义:反复运行基于时长的动画,直至达到指定的迭代计数。可以传递 repeatMode 参数来指定动画是从头开始 (RepeatMode.Restart) 还是从结尾开始 (RepeatMode.Reverse) 重复播放。
可设置参数为:

  • iterations 重复次数
  • animation 有时长的动画
  • repeatMode 重复模式,有:RepeatMode.RestartRepeatMode.Reverse
    示例如下:
/**
 * 重复动画
 */
@Composable
private fun repeatableAnimation(){
    var enable by remember{ mutableStateOf(false)}

    val value by animateIntAsState(
        targetValue = if (enable) 200 else 50,
        animationSpec = repeatable(
            iterations = 2,   //重复执行次数
            animation = tween(durationMillis = 1000),
            repeatMode = RepeatMode.Reverse  //重复执行模式,从最后开始
        )
    )
    Box(
        Modifier.height(50.dp)
            .width(value.dp)
            .background(Color.Red)
            .clickable { enable = !enable }
    ) {
        Text(text = "repeatable")
    }
}

运行效果:repeatable.gif
infiniteRepeatable(无限重复动画)
该动画类似repeatable,都是重复的迭代。但infiniteRepeatable为无限次的重复执行。
示例如下:

/**
 * 无限次重复动画
 */
@Composable
private fun InfiniteRepeatableAnimation(){
    var enable by remember{ mutableStateOf(false)}

    val value by animateIntAsState(
        targetValue = if (enable) 200 else 50,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 1000),
            repeatMode = RepeatMode.Reverse  //重复执行模式,从最后开始
        )
    )
    Box(
        Modifier.height(50.dp)
            .width(value.dp)
            .background(Color.Green)
            .clickable { enable = !enable }
    ) {
        Text(text = "infiniteRepeatable")
    }
}

运行结果就不展示了。
snap
定义:是一种特殊的 AnimationSpec类型,它会立即将值切换到结束值。您可以指定 delayMillis 来延迟动画播放的开始时间。
以延迟1000ms为例,如下:

/**
 * 无限次重复动画
 */
@Composable
private fun SnapAnimation(){
    var enable by remember{ mutableStateOf(false)}
    val value by animateIntAsState(
        targetValue = if (enable) 200 else 50,
        animationSpec = snap(
            delayMillis = 1000   //延迟1000ms执行
        )
    )
    Box(
        Modifier
            .height(50.dp)
            .width(value.dp)
            .background(Color.Green)
            .clickable { enable = !enable }
    ) {
        Text(text = "snap")
    }
}

运行结果:snap.gif

四、手势动画

我们使用 Animatable 表示图片组件的偏移位置为例,触摸以修饰符pointerInput。当检测到新的点按事件时,我们将调用 animateTo 以将偏移值通过动画过渡到点按位置。

/**
 * 通过手势点击,设置偏移量动画
 */
@Composable
private fun Gesture(){
    val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
            .pointerInput(Unit) {
                coroutineScope {
                    while (true) {
                        // Detect a tap event and obtain its position.
                        val position = awaitPointerEventScope {
                            awaitFirstDown().position
                        }
                        launch {
                            // Animate to the tap position.
                            offset.animateTo(position)
                        }
                    }
                }
            }
    ) {
        Image(
            painter = painterResource(id = R.mipmap.ic_girl),
            contentDescription = "avatar",
            modifier = Modifier.offset { offset.value.toIntOffset() }
        )
    }
}

运行结果:手势动画.gif

总结

动画知识点结构图

欢迎留言,一起学习,共同进步!

github - 示例源码
gitee - 示例源码

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐