Jetpack Compose-Canvas动画

前言

简单动画推荐使用 animate*AsStaterememberInfiniteTransition,复杂交互动画则适合 AnimatableupdateTransition

个人比较喜欢使用Animatable,灵活方便。

单属性动画

animate*AsState

使用 animate*AsState 系列 API

这是最基础的动画方式,通过监听状态变化自动生成动画值,适用于简单的属性动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp


@Composable
fun AnimDemo() {
var isStart by remember { mutableStateOf(false) }

val progress by animateFloatAsState(
targetValue = if (isStart) 1f else 0f,
animationSpec = tween(durationMillis = 2000),
finishedListener = {
println("动画已完成")
}

)

LaunchedEffect(Unit) {
isStart = true
}

Canvas(
modifier = Modifier
.padding(40.dp)
.size(200.dp)
) {
drawArc(
color = Color.Blue,
startAngle = 0f,
sweepAngle = 360f * progress,
useCenter = false,
style = Stroke(width = 8.dp.toPx())
)
}
}

@Preview
@Composable
fun AnimDemoPreview() {
AnimDemo()
}

rememberInfiniteTransition

使用 rememberInfiniteTransition 实现无限动画
适用于需要无限循环的动画,如加载指示器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp


@Composable
fun AnimDemo() {
val infiniteTransition = rememberInfiniteTransition()
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(2000, easing = LinearEasing)
)
)

Canvas(modifier = Modifier
.padding(40.dp)
.size(200.dp)) {
rotate(rotation) {
drawRect(
color = Color.Red,
topLeft = Offset(75.dp.toPx(), 75.dp.toPx()),
size = Size(50.dp.toPx(), 50.dp.toPx())
)
}
}
}

@Preview
@Composable
fun AnimDemoPreview() {
AnimDemo()
}

Animatable

循环播放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp


@Composable
fun AnimDemo() {
val animProgress = remember { Animatable(50f) }
LaunchedEffect(Unit) {
animProgress.animateTo(
targetValue = 200f,
animationSpec = infiniteRepeatable(
animation = tween(1200),
repeatMode = RepeatMode.Reverse
)
)
}

Canvas(
modifier = Modifier
.padding(40.dp)
.size(200.dp)
) {
drawRect(
color = Color.Red,
size = Size(animProgress.value.dp.toPx(), animProgress.value.dp.toPx())
)
}
}

@Preview
@Composable
fun AnimDemoPreview() {
AnimDemo()
}

单次播放

1
2
3
4
5
6
7
val animProgress = remember { Animatable(0.5f) }
LaunchedEffect(Unit) {
animProgress.animateTo(
targetValue = 1f,
animationSpec = tween(1200)
)
}

updateTransition

使用 updateTransition 管理多状态过渡

适合在多个状态之间平滑过渡,如组件的展开 / 折叠 / 隐藏状态切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

sealed class State {
object Collapsed : State()
object Expanded : State()
}

@Composable
fun AnimDemo() {
var state: State by remember { mutableStateOf(State.Collapsed) }
val transition = updateTransition(state, label = "state transition")

val size by transition.animateDp(
transitionSpec = { tween(1000) },
label = "size"
) {
when (it) {
is State.Collapsed -> 50.dp
is State.Expanded -> 200.dp
}
}

LaunchedEffect(Unit) {
state = State.Expanded
}

Canvas(
modifier = Modifier
.padding(40.dp)
.size(200.dp)
) {
drawRect(
color = Color.Red,
size = Size(size.toPx(), size.toPx())
)
}
}

@Preview
@Composable
fun AnimDemoPreview() {
AnimDemo()
}

组合动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch


@Composable
fun AnimDemo() {
// 创建两个可动画的值
val scale = remember { Animatable(1f) }
val rotation = remember { Animatable(0f) }

LaunchedEffect(Unit) {
// 同时启动两个动画(通过协程并发)
launch {
scale.animateTo(
targetValue = 2f,
animationSpec = tween(1000)
)
}
launch {
rotation.animateTo(
targetValue = 360f,
animationSpec = tween(1000)
)
}
}

Canvas(modifier = Modifier.size(200.dp)) {
withTransform(
transformBlock = {
scale(scale.value, scale.value) // 应用缩放动画
rotate(rotation.value) // 应用旋转动画
}
) {
drawRect(
color = Color.Green,
size = Size(50.dp.toPx(), 50.dp.toPx()),
topLeft = Offset(center.x - 25.dp.toPx(), center.y - 25.dp.toPx())
)
}
}
}

@Preview
@Composable
fun AnimDemoPreview() {
AnimDemo()
}

动画类型

缓动动画

tween

缓动曲线(Easing),用于控制动画的速度变化节奏。

它们基于贝塞尔曲线(Cubic Bezier)实现,能让动画效果更符合物理规律或视觉预期,避免机械感的匀速运动。

1
tween(durationMillis = 1200, easing = LinearEasing)

easing值

默认是FastOutSlowInEasing

1
2
3
4
5
6
7
8
9
10
11
// 先快速加速,然后缓慢减速到停止
public val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)

// 开始匀速,最后阶段缓慢减速
public val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)

// 快速加速后保持匀速运动
public val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)

// 完全匀速运动
public val LinearEasing: Easing = Easing { fraction -> fraction }

单次动画时长2秒,线性匀速

1
animation = tween(2000, easing = LinearEasing)

弹簧动画

弹簧动画(不能用在infiniteRepeatable中)

1
2
3
4
5
6
7
8
9
val animProgress = remember { Animatable(50f) }
LaunchedEffect(Unit) {
animProgress.animateTo(
targetValue = 200f,
animationSpec = spring(
visibilityThreshold = 0.1f,
)
)
}

关键帧动画

1
2
3
4
5
6
7
animation = keyframes {
durationMillis = 2000 // 总时长2秒
0f at 0 // 0ms时在0位置
180f at 100 // 100ms时到50
100f at 1000 // 1000ms时回退到100
200f at 2000 // 2000ms时到达200
}

跳变

1.2秒后直接跳变

1
animation = snap(1200)

循环播放

1
2
3
4
5
6
7
animProgress.animateTo(
targetValue = 200f,
animationSpec = infiniteRepeatable(
animation = tween(1200),
repeatMode = RepeatMode.Reverse
)
)

在 Jetpack Compose 中,infiniteRepeatable() 是用于创建无限循环动画的关键函数,通常配合 rememberInfiniteTransition 使用。它的参数用于控制循环方式、动画曲线、重复次数等,以下是详细说明:

函数签名

1
2
3
4
5
fun <T> infiniteRepeatable(
animation: AnimationSpec<T>,
repeatMode: RepeatMode = RepeatMode.Restart,
initialStartOffset: StartOffset = StartOffset(0)
): InfiniteRepeatableSpec<T>

核心参数说明

动画类型

animation: AnimationSpec<T>(必填)

作用:定义单次动画的行为(如时长、缓动曲线等)

常用值

  • tween():指定固定时长的动画(最常用)
  • spring():弹簧物理效果的动画
  • keyframes():关键帧动画
  • snap():无动画,直接跳变

循环方式

repeatMode: RepeatMode(可选,默认 Restart

作用:定义每次循环的方式

取值

  • RepeatMode.Restart:每次循环从初始值重新开始
  • RepeatMode.Reverse:交替反向播放(如从0→1→0→1…)

示例:先正向播放,再反向播放,循环往复

1
repeatMode = RepeatMode.Reverse

延迟

initialStartOffset: StartOffset(可选,默认 StartOffset(0)

作用:定义动画首次启动的延迟或偏移

使用场景

  • 延迟首次动画开始时间
  • 让多个循环动画错开启动(实现错开的波浪效果)
1
2
// 首次启动延迟500ms
initialStartOffset = StartOffset(500)

注意:offsetMillis 是相对于动画总时长的偏移(如2000ms动画,偏移1000ms即从中间开始)

完整示例

以下是一个结合 infiniteRepeatable 参数的旋转+缩放动画:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Composable
fun InfiniteAnimationDemo() {
val infiniteTransition = rememberInfiniteTransition()

// 旋转动画(3秒一圈,匀速,循环重启)
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(3000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)

// 缩放动画(1秒一次,先放大后缩小,反向循环)
val scale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 1.5f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse,
initialStartOffset = StartOffset(500) // 延迟500ms启动
)
)

Canvas(modifier = Modifier.size(200.dp)) {
withTransform({
rotate(rotation)
scale(scale, scale)
}) {
drawCircle(
color = Color.Magenta,
radius = 50.dp.toPx(),
center = center
)
}
}
}

关键注意点

  1. infiniteRepeatable 必须配合 rememberInfiniteTransition 使用,不能直接用于 animate*AsState
  2. repeatMode = Reverse 时,动画会在 initialValuetargetValue 之间来回切换
  3. 多个动画可以共享同一个 infiniteTransition,通过 initialStartOffset 实现错开效果
  4. 若需要停止无限动画,可通过控制 rememberInfiniteTransition 的生命周期(如用 LaunchedEffectkey 参数)

通过组合这些参数,可以实现各种复杂的循环动画效果,如加载指示器、呼吸效果、旋转动画等。