无论是Jetpack Compose还是原来的view体系,想做出好看的交互效果都离不开动画。Compose同样提供了相应的API来实现动画。这些API大致可以分为两类:高级别API(直接拿来用,就像使用普通组件比如Container一样)和低级别API(需要自己配置一些属性之后才能使用)。接下来就让我们详细的了解一下。
高级别API
高级别的api主要有以下几种
AnimatedVisibility
:元素进入/退出时的过渡动画AnimatedContent
:布局内容变化时的动画Modifier.animateContentSize
:布局大小变化时的动画Crossfade
:两个布局切换时的淡入/淡出动画
AnimatedVisibility
组件进入或退出时的动画
@Composable
fun AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandIn(),
exit: ExitTransition = shrinkOut() + fadeOut(),
label: String = "AnimatedVisibility",
content: @Composable() AnimatedVisibilityScope.() -> Unit
)
常用的构造函数如上,只需指定visible来控制是否可见,enter和exit为默认添加的进入和退出动画,示例如下
@Composable
fun AnimatedVisibilityDemo() {
var visibility by remember {
mutableStateOf(true)
}
Column {
AnimatedVisibility(visible = visibility,
) {
Text(
text = "Hello",
Modifier
.fillMaxWidth()
.height(200.dp)
.background(Color.LightGray)
)
}
Button(onClick = { visibility = !visibility }) {
Text(text = "ChangeVisibility")
}
}
}
AnimatedContent
用来实现不同组件间的切换动画
@ExperimentalAnimationApi
@Composable
fun <S> AnimatedContent(
targetState: S,
modifier: Modifier = Modifier,
transitionSpec: AnimatedContentScope<S>.() -> ContentTransform = {
fadeIn(animationSpec = tween(220, delayMillis = 90)) +
scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)) with
fadeOut(animationSpec = tween(90))
},
contentAlignment: Alignment = Alignment.TopStart,
content: @Composable() AnimatedVisibilityScope.(targetState: S) -> Unit
)
这里需要传入两个参数,一个是targetState
,一个是content
,content是基于targetState创建的Composable,当target变化时content的内容也会随之变化。targetState一定要在content中被使用,否则当targetState变化时,只见动画,却不见内容的变化,视觉上会很奇怪。这里的transitionSpec
其实就是定义ContentTransform
class ContentTransform(
val targetContentEnter: EnterTransition,
val initialContentExit: ExitTransition,
targetContentZIndex: Float = 0f,
sizeTransform: SizeTransform? = SizeTransform()
)
其实就是定义enterTransition
,exitTransition
以及sizeTransform
。这两个transition其实可以使用slideInxxx,slideoutxxx,fadeIn,fadeOut等组合。下面例子就是点击按钮实现从下往上淡入,从下往上淡出的轮播效果(这里使用with来创建ContentTransform)
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedContentDemo2() {
var count by remember {
mutableStateOf(0)
}
val testNames = listOf(
"我是a",
"我是aaa",
"我是bbbbbbb",
"他是ccccccccccccccccccccc",
"她是cccc+++++ddd++eeee+ffff+gggggg"
)
val result by remember {
derivedStateOf { testNames[count] }
}
Row {
Button(onClick = { if (count >= testNames.size-1) count = 0 else count++ }) {
Text(text = "Add")
}
AnimatedContent(targetState = result, transitionSpec = {
slideInVertically(initialOffsetY = { it }) + fadeIn() with slideOutVertically() + fadeOut()
}) { targetCount ->
Text(text = result)
}
}
}
Crossfade
可以说是AnimateContent的简化版,只提供淡入淡出效果
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun <T> Crossfade(
targetState: T,
modifier: Modifier = Modifier,
animationSpec: FiniteAnimationSpec<Float> = tween(),
content: @Composable (T) -> Unit
)
例子就不提供了可以直接把上面的例子中的AnimatedContent
换成Crossfade
Modifieir.animatedContentSize
当容器尺寸发生变化时,会通过动画进行过渡,比如可展开的Text
@Composable
fun AnimatedContentSizeDemo() {
var expand by remember {
mutableStateOf(false)
}
Column(Modifier.padding(16.dp)) {
Text(text = "AnimatedContentSizeDemo")
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { expand = !expand }) {
Text(text = if (expand) "Shrink" else "Expand")
}
Spacer(modifier = Modifier.height(16.dp))
Box(modifier = Modifier
.background(Color.LightGray)
.animateContentSize()){
Text(
text = "This modifier animates its own size when its child modifier (or the child composable if it is already at the tail of the chain) changes size. This allows the parent modifier to observe a smooth size change, resulting in an overall continuous visual change.\n" +
"A FiniteAnimationSpec can be optionally specified for the size change animation. By default, spring will be used.\n" +
"An optional finishedListener can be supplied to get notified when the size change animation is finished. Since the content size change can be dynamic in many cases, both initial value and target value (i.e. final size) will be passed to the finishedListener. Note: if the animation is interrupted, the initial value will be the size at the point of interruption. This is intended to help determine the direction of the size change (i.e. expand or collapse in x and y dimensions).",
fontSize = 16.sp,
textAlign = TextAlign.Justify,
modifier = Modifier.padding(16.dp),
maxLines = if (expand)Int.MAX_VALUE else 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
低级别API
低级别的API主要包括以下几种
- Animatable:是一个值的集合,属于流程定制型动画
- animateXXXAsState:对单一值进行动画,类似于传统view系统的属性动画
- updateTransition:同时对多个属性值进行动画
- animate:该函数是一个suspend函数,可见只能用于协程中,对动画进行深度定制
Animatable
Animatable
是一个值的集合。他通过animateTo
自动执行动画。而且如果animateTo在一个正在执行的动画中被触发,那么就会从这个动画的值开始执行到设定的targetValue,以保证动画的连续性。它其实是一个流程定制型动画
使用:
首先构造Animatable
,比如对Dp进行动画,构造需要两个参数,一个是targetValue
,另一个则是animatable最终返回值的类型,最终返回的其实是State<T>
,这里的T就是typeConverter
定义的类型
val anim = remember { Animatable(initialValue = size1, typeConverter = Dp.VectorConverter) }
其次在compose中使用该值
Box( Modifier .size(anim.value) .background(Color.Green) .clickable { big = !big })
最后在协程中通过animateTo或者snapTo来启动动画
LaunchedEffect(big) {
//直接变换到相应的值
anim.snapTo(if (big) 192.dp else 0.dp)
//然后开始动画
anim.animateTo(size1)
}
这里的animateTo则是对动画的流程进行定制的,它的定义如下
suspend fun animateTo(
targetValue: T,
animationSpec: AnimationSpec<T> = defaultSpringSpec,
initialVelocity: T = velocity,
block: (Animatable<T, V>.() -> Unit)? = null
)
这里的定制主要是通过animationSpec
进行的。block
则是一个回调,动画的每一帧都会回调这个方法。重点来看看animationSpec
AnimationSpec存储了动画信息:
- 需要做动画的值的类型;
- 将值转换成
AnimationVector
时用到的动画配置信息。
其实他就是一个接口
interface AnimationSpec<T> {
fun <V : AnimationVector> vectorize(
converter: TwoWayConverter<T, V>
): VectorizedAnimationSpec<V>
}
它的实现一共有以下11种
接下来我们就分析一下这些实现
1:DurationBasedAnimationSpec
它是一个具有固定持续时间的AnimationSpec
。他也是一个接口,它的实现有三种KeyframeSpec
,TweenSpec
,SnapSpec
。稍后详细说明
2:FiniteAnimationSpec
它是所有有限次数AnimationSpec的都实现的接口,其实也就是KeyframeSpec
,TweenSpec
,SnapSpec
,RepeatableSpec
,SpringSpec
。
3:FloatAnimationSpec
同样是一个接口,他只针对float值进行动画,它的实现只有两种FloatSpringSpec
和FloatTweenSpec
4:FloatSpringSpec
这是一个弹性动画,它的定义如下
class FloatSpringSpec(
val dampingRatio: Float = Spring.DampingRatioNoBouncy,
val stiffness: Float = Spring.StiffnessMedium,
private val visibilityThreshold: Float = Spring.DefaultDisplacementThreshold
)
其实就是配置弹性系数dampingRation
,阻尼系数stiffness
用于实现阻尼效果,比如如下示例
@Composable
private fun FloatSpringSpecDemo() {
var big by remember {
mutableStateOf(false)
}
val float1 = remember(big) { if (big) 100f else 50f }
val floatAnim = remember { Animatable(float1) }
LaunchedEffect(key1 = big) {
floatAnim.animateTo(
float1,
FloatSpringSpec(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
)
)
}
Box(
Modifier
.width(100.dp)
.height(floatAnim.value.dp)
.background(Color.Green)
.clickable {
big = !big
})
}
5:FloatTweenSpec
它是将一个float动画利用easing
函数从起始值过渡到最终值。定义如下
class FloatTweenSpec(
val duration: Int = DefaultDurationMillis,
val delay: Int = 0,
private val easing: Easing = FastOutSlowInEasing
)
duration
动画时长,delay
则是动画开始前的等待时长,easing
则是差值器,就是根据fraction决定当前值的函数。比如定义好的FastOutSlowInEasing
,LinearOutSlowInEasing
,FastOutLinearInEasing
,LinearEasing
。如果不满足,那么可实现Easing
接口的transform
方法自定义动画运动到某个百分比时的动画值
fun interface Easing {
fun transform(fraction: Float): Float
}
简单示例如下:
@Composable
private fun FloatTweenSpecDemo() {
var big by remember {
mutableStateOf(false)
}
val float1 = remember(big) { if (big) 100f else 50f }
val floatAnim = remember { Animatable(float1) }
LaunchedEffect(key1 = big){
floatAnim.animateTo(
float1,
FloatTweenSpec(
easing = FastOutSlowInEasing
)
)
}
Box(
Modifier
.size(floatAnim.value.dp)
.background(Color.Green)
.clickable {
big = !big
})
}
6:InfiniteRepeatableSpec
它是一个无限循环的动画,永远不会自动停止。定义如下
class InfiniteRepeatableSpec<T>(
val animation: DurationBasedAnimationSpec<T>,
val repeatMode: RepeatMode = RepeatMode.Restart,
val initialStartOffset: StartOffset = StartOffset(0)
)
这里的animation
则是需要循环的动画
,可以看到是DurationBasedAnimationSpec
。通过1中介绍可以看出其实只有三种动画可以使用它进行无限循环KeyframeSpec
,TweenSpec
,SnapSpec
,initialStartOffset
则是起始时动画偏移量
//这种是延迟1000ms后开始动画
StartOffset(1000, StartOffsetType.Delay)))
//这种是立即从动画的1000ms处开始执行
StartOffset(1000, StartOffsetType.FastForward)))
通常使用infiniteRepeatable
函数来创建
@Stable
fun <T> infiniteRepeatable(
animation: DurationBasedAnimationSpec<T>,
repeatMode: RepeatMode = RepeatMode.Restart,
initialStartOffset: StartOffset = StartOffset(0)
): InfiniteRepeatableSpec<T> =
InfiniteRepeatableSpec(animation, repeatMode, initialStartOffset)
示例如下,至于这里的tween
其实就是TweenSpec
,稍后介绍,使用示例如下
@Composable
private fun InfiniteRepeatableSpecDemo() {
var big by remember {
mutableStateOf(false)
}
val size1 = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size1,Dp.VectorConverter) }
LaunchedEffect(key1 = big){
anim.animateTo(size1, infiniteRepeatable(tween(), RepeatMode.Reverse,
StartOffset(1000, StartOffsetType.FastForward)))
}
Box(
Modifier
.size(anim.value)
.background(Color.Green)
.clickable {
big = !big
})
}
7:KeyframsSpec
它是一个关键帧动画,也就是说需要定义出一系列的关键帧,比如动画执行到100ms时值是多少,它的定义如下
class KeyframesSpec(val config: KeyframesSpecConfig) : DurationBasedAnimationSpec
可以看到是通过KeyframesSpecConfig
来定义的关键帧,这个config的定义如下
class KeyframesSpecConfig<T> {
//动画时长
var durationMillis: Int = DefaultDurationMillis
//动画延时时长,单位毫秒
var delayMillis: Int = 0
internal val keyframes = mutableMapOf<Int, KeyframeEntity<T>>()
//添加一个关键帧,使得动画的值在这一时刻是这个值,比如 0.8f at 100(在动画执行到100ms时值为0.8f)
infix fun T.at(/*@IntRange(from = 0)*/ timeStamp: Int): KeyframeEntity<T> {
return KeyframeEntity(this).also {
keyframes[timeStamp] = it
}
}
//添加一个动画差值器,比如 0f at 50 with LinearEasing(从这之前到50ms这个时间段内采用LinearEasing差值器)
infix fun KeyframeEntity<T>.with(easing: Easing) {
this.easing = easing
}
...
}
每个方法的作用都做了注释,通常我们使用keyframs
函数来创建关键帧动画
@Stable
fun <T> keyframes(
init: KeyframesSpec.KeyframesSpecConfig<T>.() -> Unit
)
这里示例如下
@Composable
private fun KeyframesSpecDemo() {
var big by remember {
mutableStateOf(false)
}
val size1 = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size1,Dp.VectorConverter) }
LaunchedEffect(key1 = big){
anim.animateTo(size1, keyframes {
durationMillis = 450
delayMillis = 500
48.dp at 0 with FastOutLinearInEasing
144.dp at 150 with FastOutSlowInEasing
20.dp at 300
})
}
Box(
Modifier
.size(anim.value)
.background(Color.Green)
.clickable {
big = !big
})
}
8:RepeatableSpec
他其实跟InfiniteRepeatableSpec
一样,只不过这里指定了循环的次数
class RepeatableSpec<T>(
val iterations: Int,
val animation: DurationBasedAnimationSpec<T>,
val repeatMode: RepeatMode = RepeatMode.Restart,
val initialStartOffset: StartOffset = StartOffset(0)
)
跟InfiniteRepeatableSpec
相比,只多了一个iterations
(次数)参数,通常我们通过repeatable
函数来进行构造
@Stable
fun <T> repeatable(
iterations: Int,
animation: DurationBasedAnimationSpec<T>,
repeatMode: RepeatMode = RepeatMode.Restart,
initialStartOffset: StartOffset = StartOffset(0)
): RepeatableSpec<T> =
RepeatableSpec(iterations, animation, repeatMode, initialStartOffset)
所以我们将InfiniteRepeatable示例中的循环改成3次如下
@Composable
private fun RepeatableSpecDemo() {
var big by remember {
mutableStateOf(false)
}
val size1 = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size1,Dp.VectorConverter) }
LaunchedEffect(key1 = big){
anim.animateTo(size1, repeatable(3,tween(), RepeatMode.Reverse,
StartOffset(0, StartOffsetType.FastForward))
)
}
Box(
Modifier
.size(anim.value)
.background(Color.Green)
.clickable {
big = !big
})
}
9:SnapSpec
A jump-cut type of animation,其实就是直接将动画执行到最终值。类似于view体系的moveTo。通常使用snap函数进行构造。示例如下
@Composable
private fun SnapSpecDemo() {
var big by remember {
mutableStateOf(false)
}
val size1 = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size1,Dp.VectorConverter) }
LaunchedEffect(key1 = big){
anim.animateTo(size1, snap())
}
Box(
Modifier
.size(anim.value)
.background(Color.Green)
.clickable {
big = !big
})
}
10:SpringSpec
弹性动画,跟FloatSpringSpec
类似,只不过它不仅仅支持float类型的值。通常通过spring
函数创建
@Stable
fun <T> spring(
dampingRatio: Float = Spring.DampingRatioNoBouncy,
stiffness: Float = Spring.StiffnessMedium,
visibilityThreshold: T? = null
): SpringSpec<T> =
SpringSpec(dampingRatio, stiffness, visibilityThreshold)
该方法的示例如下
@Composable
private fun SpringSpecDemo() {
var big by remember {
mutableStateOf(false)
}
val size1 = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size1,Dp.VectorConverter) }
LaunchedEffect(key1 = big){
anim.animateTo(size1, spring(0.1f, Spring.StiffnessMedium), 2000.dp)
}
Box(
Modifier
.size(anim.value)
.background(Color.Green)
.clickable {
big = !big
})
}
11:TweenSpec
补间动画,它可以指定动画的时长,延迟以及差值器,定义如下
@Immutable
class TweenSpec<T>(
val durationMillis: Int = DefaultDurationMillis,
val delay: Int = 0,
val easing: Easing = FastOutSlowInEasing
) : DurationBasedAnimationSpec<T>
该动画通常使用tween()
函数创建
@Stable
fun <T> tween(
durationMillis: Int = DefaultDurationMillis,
delayMillis: Int = 0,
easing: Easing = FastOutSlowInEasing
): TweenSpec<T> = TweenSpec(durationMillis, delayMillis, easing)
该方法示例如下
@Composable
private fun TweenSpecDemo() {
var big by remember {
mutableStateOf(false)
}
val size1 = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size1,Dp.VectorConverter) }
LaunchedEffect(key1 = big){
anim.animateTo(size1, tween())
}
Box(
Modifier
.size(anim.value)
.background(Color.Green)
.clickable {
big = !big
})
}
animateXXXAsState
该方法是对单一值进行动画的,它类似于传统视图中的属性动画,可以自动完成从当前值到目标值过渡的估值计算。主要有以下几种
返回的是一个State<T>
,比如animateDpAsState
,它返回的就是一个Sate<Dp>
。他其实是简化版的Animatable。最终调用animateValueAsState.
@Composable
fun animateDpAsState(
targetValue: Dp,
animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
finishedListener: ((Dp) -> Unit)? = null
): State<Dp> {
return animateValueAsState(
targetValue,
Dp.VectorConverter,
animationSpec,
finishedListener = finishedListener
)
}
这里直接调用的animateValueAsState
,并且指定拉倒typeConverter
为Dp.VectorConverter
,其实跟我们创建Animatable
时候一样,继续看animateValueAsState方法
@Composable
fun <T, V : AnimationVector> animateValueAsState(
targetValue: T,
typeConverter: TwoWayConverter<T, V>,
animationSpec: AnimationSpec<T> = remember {
spring(visibilityThreshold = visibilityThreshold)
},
visibilityThreshold: T? = null,
finishedListener: ((T) -> Unit)? = null
): State<T> {
val animatable = remember { Animatable(targetValue, typeConverter) }
...
LaunchedEffect(channel) {
for (target in channel) {
val newTarget = channel.tryReceive().getOrNull() ?: target
launch {
if (newTarget != animatable.targetValue) {
animatable.animateTo(newTarget, animSpec)
listener?.invoke(animatable.value)
}
}
}
}
return animatable.asState()
}
可以看到剩下的步骤与使用Animatable一样,只不过是已经写好的。创建Animatable,在协程中使用animateTo。 所以该方法使用时
首先需要调动animateXXXAsState
指定初始值,比如这里
val size by animateDpAsState(if (big1) 96.dp else 48.dp) // State
然后直接使用该值
@Composable
private fun AnimateXXXAsDemo() {
//animateXXXAsState是直接确定了动画的起点和终点
var big1 by remember {
mutableStateOf(false)
}
val size by animateDpAsState(if (big1) 96.dp else 48.dp) // State
Box(
Modifier
.size(size)
.background(Color.Green)
.clickable {
big1 = !big1
})
}
updateTransition
同时对多个值进行动画,其实就是整合多个Animatable
,使其一起动画,所能支持的值与animateXXXAsState
是一致的
所以用法基本上同上述的大同小异
- 创建updateTransition:直接传入targetState,一般来说是一个枚举值
@Composable
fun <T> updateTransition(
targetState: T,
label: String? = null
)
- 利用创建好的transition调用
transition.animateXXX
来进行具体值的动画,比如这里是对color进行动画。而animateXXX
中配置的transitionSpec
其实是FiniteAnimationSpec
,这在上面已经介绍过了
val color by transition.animateColor(label = "Color", transitionSpec = {
when{
BoxState.Small isTransitioningTo BoxState.Large->
keyframes {
durationMillis=2500
Color.Green at 0
Color.Cyan at 500 with LinearOutSlowInEasing
Color.DarkGray at 1000 with FastOutLinearInEasing
Color.Magenta at 1500 with LinearEasing
Color.Red at 2000 with FastOutSlowInEasing
}
else-> tween(durationMillis = 3000)
}
}) { state ->
when (state) {
BoxState.Small -> Color.Blue
BoxState.Large -> Color.Yellow
}
}
- 将定义好的动画值绑定到需要做动画的
compsable
组件上 - 更改“状态”运行动画
下面的示例为对box同时进行大小和颜色动画
private enum class BoxState{
Small,
Large
}
@Composable
private fun UpdateTransitionDemo(){
var boxState by remember {
mutableStateOf(BoxState.Small)
}
//第一步创建updateTransition
val transition= updateTransition(targetState = boxState, label = "Box Transition")
//第二步利用创建好的transition调用transition.animateXXX来进行具体值的动画
val color by transition.animateColor(label = "Color", transitionSpec = {
when{
BoxState.Small isTransitioningTo BoxState.Large->
keyframes {
durationMillis=2500
Color.Green at 0
Color.Cyan at 500 with LinearOutSlowInEasing
Color.DarkGray at 1000 with FastOutLinearInEasing
Color.Magenta at 1500 with LinearEasing
Color.Red at 2000 with FastOutSlowInEasing
}
else-> tween(durationMillis = 3000)
}
}) { state ->
when (state) {
BoxState.Small -> Color.Blue
BoxState.Large -> Color.Yellow
}
}
//第二步利用transition.animateXXX来进行具体值的动画
val size by transition.animateDp(label = "Size", transitionSpec = {
when{
BoxState.Small isTransitioningTo BoxState.Large-> keyframes {
durationMillis=2000
500.dp at 0
50.dp at 1000 with LinearOutSlowInEasing
400.dp at 20000 with FastOutLinearInEasing
100.dp at 3000 with LinearEasing
300.dp at 4000 with FastOutSlowInEasing
}
else-> spring(dampingRatio = Spring.DampingRatioHighBouncy)
}
}) { state ->
when (state) {
BoxState.Small -> 100.dp
BoxState.Large -> 300.dp
}
}
Column {
//第四步开启动画
Button(onClick = {
when(boxState){
BoxState.Small->boxState=BoxState.Large
else ->boxState=BoxState.Small
}
}){
Text(text = "Toggle")
}
//第三步将动画值绑定到需要动画的组件上
Box(
Modifier
.size(size)
.background(color = color)) {
}
}
}
animate
该函数是一个suspend
函数,可见只能用于协程中。这个函数可对动画进行深度定制,也就是控制动画的执行顺序可以更容易,其实主要是通过协程作用域实现的
suspend fun animate(
initialValue: Float,
targetValue: Float,
initialVelocity: Float = 0f,
animationSpec: AnimationSpec<Float> = spring(),
block: (value: Float, velocity: Float) -> Unit
)
这里指定了动画的开始值结束值以及AnimationSpec。比如利用这个实现点赞红心的alpha和scale动画效果的demo如下
@Composable
fun AnimateDemo() {
var alpha by remember {
mutableStateOf(0f)
}
var scale by remember {
mutableStateOf(0f)
}
val scope = rememberCoroutineScope()
Box(
Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
scope.launch {
//point1
// 开始出现时的动画
coroutineScope {
//point3
//执行透明度的变换
launch {
animate(0f, 1f, animationSpec = keyframes {
durationMillis = 500
0f at 0
0.5f at 100
1f at 225
}) { value, _ ->
alpha = value
}
}
//point4与point3是并行的。也就达到了updateTransition的效果。
//执行scale变换
launch {
animate(
0f,
2f,
animationSpec = spring(dampingRatio = Spring.DampingRatioHighBouncy)
) { value, _ ->
scale = value
}
}
}
//point2与point1是顺序执行
//开始出现动画执行完毕后直接执行消失动画
coroutineScope {
launch {
animate(1f, 0f, animationSpec = snap()) { value, _ ->
alpha = value
}
}
launch {
animate(2f, 4f, animationSpec = snap()) { value, _ ->
scale = value
}
}
}
}
}
)
}
) {
Icon(
Icons.Filled.Favorite,
"点赞",
Modifier
.align(Alignment.Center)
.graphicsLayer(
alpha = alpha,
scaleX = scale,
scaleY = scale
),
tint = Color.Red
)
}
}
可以看到point1与point2在一个协程作用域中,这两块是顺序执行的。而point3和point4分别通过launch函数又重新创建了一个子协程,所以执行互不干扰也就是并行执行。如果想对动画进行取消则cancel协程就可以了。以上就是compose中动画API的介绍。