Jetpack Compose动画

前面讲到布局基础图像绘制,本篇来讲下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 - 示例源码

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容