Skip to content

Compose动画

更新: 1/31/2026 字数: 0 字 时长: 0 分钟

动画分类



高级别 API
API说明
AnimatedVisibilityUI 元素进入/退出时过渡动画
AnimatedContent布局内容变化时的动画
Modifier.animateContentSize布局大小变化时的动画
Crossfade两个布局切换时淡入/淡出动画
分类API说明


低级别 API
animate * AsState单个值动画
Animatable可动画的数值容器
updateTransition组合动画
rememberInfiniteTransition组合无限执行动画
TargetBasedAnimation自定义执行时间的低级别动画

高级别动画 API

AnimatedVisibility

主要作用时控制显隐状态

kotlin
@Composable
fun <T> Transition<T>.AnimatedVisibility(
    visible: (T) -> Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = shrinkOut() + fadeOut(),
    content: @Composable() AnimatedVisibilityScope.() -> Unit
) = AnimatedVisibilityImpl(this, visible, modifier, enter, exit, content = content)

例子:

kotlin
var editable by remember { mutableStateOf(true) }
val density = LocalDensity.current
// 添加这个按钮来测试动画
Button(onClick = { editable = !editable }) {
    Text(if (editable) "Hide" else "Show")
}
AnimatedVisibility(
    visible = editable,
    enter = slideInVertically {
        // 从顶部40dp位置开始滑入
        with(density) { -40.dp.roundToPx() }
    } + expandVertically(
        // 从顶部开始展开
        expandFrom = Alignment.Top
    ) + fadeIn(
        // 从初始透明度0.3f开始淡入
        initialAlpha = 0.3f
    ),
    exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
    Text(text = "Hello", Modifier.fillMaxWidth().height(200.dp))
}

有 fadeIn, scaleIn, slideInVertically, expendVertically 四种 EnterTransition,换成 Out 就是对应的 ExitTransition。

MutableTransitionState 监听动画状态

AnimatedVisibility 有另一个接受 MutableTransitionState类型参数的重载方法

kotlin
@Composable
fun ColumnScope.AnimatedVisibility(
    visibleState: MutableTransitionState<Boolean>,
    modifier: Modifier = Modifier,
    enter: EnterTransition = expandVertically() + fadeIn(),
    exit: ExitTransition = shrinkVertically() + fadeOut(),
    label: String = "AnimatedVisibility",
    content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
    val transition = rememberTransition(visibleState, label)
    AnimatedVisibilityImpl(transition, { it }, modifier, enter, exit, content = content)
}

看一下这个类型的构造:

kotlin
class MutableTransitionState<S>(initialState: S) : TransitionState<S>() {
    /**
     * Current state of the transition. [currentState] is initialized to the initialState that the
     * [MutableTransitionState] is constructed with.
     *
     * It will be updated by the Transition that is created with this [MutableTransitionState]
     * when the transition arrives at a new state.
     */
    override var currentState: S by mutableStateOf(initialState)
        internal set

    /**
     * Target state of the transition. [targetState] is initialized to the initialState that the
     * [MutableTransitionState] is constructed with.
     *
     * It can be updated to a new state at any time. When that happens, the [Transition] that is
     * created with this [MutableTransitionState] will update its
     * [Transition.targetState] to the same and subsequently starts a transition animation to
     * animate from the current values to the new target.
     */
    override var targetState: S by mutableStateOf(initialState)
        public set

    /**
     * [isIdle] returns whether the transition has finished running. This will return false once
     * the [targetState] has been set to a different value than [currentState].
     *
     * @sample androidx.compose.animation.core.samples.TransitionStateIsIdleSample
     */
    val isIdle: Boolean
        get() = (currentState == targetState) && !isRunning

    override fun transitionConfigured(transition: Transition<S>) {
    }

    override fun transitionRemoved() {
    }
}

主要是当前状态 currentState 和 targetState 目标状态,这两个的不同驱动了动画的执行。

kotlin
val state = remember {
    MutableTransitionState(false).apply {
        targetState = true
    }
}
Column {
    AnimatedVisibility(visibleState = state) {
        Text("Hello,world!")
    }
}

currentState = false, targetState = true,所以当上屏时,状态不同,动画会立即执行。

自定义 Enter/Exit 动画

kotlin
AnimatedVisibility(
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
) {
    val background by transition.animateColor { state ->
        if (state == EnterExitState.Visible) Color.Blue else Color.Green
    }
    Box(modifier = Modifier.size(128.dp).background(background))
}

AnimatedContent

主要用于实现不同组件的平滑切换动画:

kotlin
@Composable
fun <S> AnimatedContent(
    targetState: S,
    modifier: Modifier = Modifier,
    transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = {
        (fadeIn(animationSpec = tween(220, delayMillis = 90)) +
            scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)))
            .togetherWith(fadeOut(animationSpec = tween(90)))
    },
    contentAlignment: Alignment = Alignment.TopStart,
    label: String = "AnimatedContent",
    contentKey: (targetState: S) -> Any? = { it },
    content: @Composable() AnimatedContentScope.(targetState: S) -> Unit
) {
    val transition = updateTransition(targetState = targetState, label = label)
    transition.AnimatedContent(
        modifier,
        transitionSpec,
        contentAlignment,
        contentKey,
        content = content
    )
}

接收一个 targetState 和 content,当 targetState 变化时,content 也会随之变化,注意 content 里一定要使用 targetState,不然看着很怪。

kotlin
@Composable
fun AnimatedContentStudy() {
    Row {
        var count by remember { mutableStateOf(0) }
        Button(
            onClick = { count++ }
        ) {
            Text("Add")
        }
        AnimatedContent(targetState = count) { targetCount ->
            Text("Count: $targetCount")
        }
    }
}

ContentTransform 自定义动画

用 transitionSpec 参数进行修改,返回一个 ContentTransform类型的参数:

kotlin
infix fun EnterTransition.togetherWith(exit: ExitTransition) = ContentTransform(this, exit)

可以使用 EnterTransition.togetherwith() / EnterTransition togetherwith ExitTransition连接,比如实现 Slide 从右到左:

kotlin
AnimatedContent(targetState = count,
            transitionSpec = {
                (slideInHorizontally { fullWidth -> fullWidth } + fadeIn()).togetherWith(
                    slideOutHorizontally { fullWidth -> -fullWidth } + fadeOut()
                )
            }
        ) { targetCount ->
            Text("Count: $targetCount")
        }

SizeTransform 定义大小动画

自定义过渡动画时,可以使用 using 操作符连接 SizeTransform。可以使我们预先获取到 currentContent 和 targetContent 的 Size 值,并允许我们来定制尺寸变化的过渡动画效果:

kotlin
var expanded by remember { mutableStateOf(false) }
Surface(
    color = MaterialTheme.colorScheme.primary,
    onClick = {
        expanded = !expanded
    }
) {
    AnimatedContent(
        targetState = expanded,
        transitionSpec = {
            fadeIn(animationSpec = tween(150,150)) togetherWith
            fadeOut(animationSpec = tween(150)) using
            SizeTransform { initialSize, targetSize ->
                if (targetState) {
                    // 展开时,先水平方向展开
                    keyframes {
                        IntSize(targetSize.width, initialSize.height) at 150 // 在150时横向展开,后面再竖向展开
                        durationMillis = 300
                    }
                } else {
                    // 收起时,先垂直方向收起
                    keyframes {
                        IntSize(initialSize.width, targetSize.height) at 150
                        durationMillis = 300
                    }
                }
            }
        }
    ) { targetExpanded ->
        if (targetExpanded) {
            Text(text = "11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
        } else {
            Icon(imageVector = Icons.Filled.Search, contentDescription = null)
        }
    }
}

Crossfade

这是 AnimatedContent 的一种功能特性,它使用起来更简单,如果只需要淡入淡出效果,可以使用 Crossfade 替代 AnimatedContent。

kotlin
var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage) { screen ->
    when(screen) {
        "A" -> Text("Page A")
        "B" -> Text("Page B")
    }
}

Modifier.animateContentSize

是 Modifier 的一个修饰符方法。用途非常专一,当容器尺寸变化时,会通过动画进行过渡,开箱即用。

kotlin
@Composable
fun AnimateContentSizeDemo() {
    var expend by remember { mutableStateOf(false) }
    Column(Modifier.padding(16.dp)) {
        Text("AnimatedContentSizeDemo")
        Spacer(Modifier.height(16.dp))
        Button(
            onClick = { expend = ! expend }
        ) {
            Text(if (expend) "Shrink" else "Expand")
        }
        Spacer(Modifier.height(16.dp))
        Box(
            Modifier
                .background(Color.LightGray)
                .animateContentSize()
        ) {
            Text(
                text = "animateContentSize() animates its own size when its child modifier (or the child composable if it is already at the tail of the chain) changes size...............................................",
                fontSize = 16.sp,
                textAlign = TextAlign.Justify,
                modifier = Modifier.padding(16.dp),
                maxLines = if (expend) Int.MAX_VALUE else 2
            )
        }
    }
}

低级别 API

animate * AsState

类似于传统视图中的属性动画,可以自动完成从当前值到目标值过渡的估值计算。

kotlin
@Composable
fun animateColorAsState(
    targetValue: Color,
    animationSpec: AnimationSpec<Color> = colorDefaultSpring,
    label: String = "ColorAnimation",
    finishedListener: ((Color) -> Unit)? = null
): State<Color> {
    val converter = remember(targetValue.colorSpace) {
        (Color.VectorConverter)(targetValue.colorSpace)
    }
    return animateValueAsState(
        targetValue, converter, animationSpec, label = label, finishedListener = finishedListener
    )
}

以一个点赞按钮的点击效果为例:

kotlin
@Composable
fun LowAPIStudy() {
    var change by remember { mutableStateOf(false) }
    var flag by remember { mutableStateOf(false) }
    val buttonSize by animateDpAsState(
        targetValue = if (change) 32.dp else 24.dp
    )
    val buttonColor by animateColorAsState(
        targetValue = if (flag) Color.Red else Color.Gray,
        animationSpec = tween(500)
    )
    if (buttonSize == 32.dp) {
        change = false
    }
    IconButton(
        onClick = {
            change = true
            flag = !flag
        }
    ) {
        Icon(Icons.Rounded.Favorite,
            contentDescription = null,
            modifier = Modifier.size(buttonSize),
            tint = buttonColor)
    }
}

这里大概流程如下:

  1. 点击后 change 变为 true,然后 buttonSize 由 24dp 过渡到 32dp
  2. flag 取反,实现由红过渡到灰或者由灰过渡到红
  3. 当 buttonSize 变为 32dp 时,change 被重置为 false,会恢复到 24dp。

Compose 为常用数据类型都提供了 animate * AsState 方法,例如 Float,Color,Dp,Size,Bounds,Offset,Rect,Int,IntOffset 和 IntSize 等,如果其他的值可以使用通用类型 animateValueAsState。

Animatable

这是一个数值包装器,它的 animateTo 方法可以根据数值的变化设置动画效果,animate * AsState 背后就是基于 Animatable 实现的。

kotlin
    var change by remember { mutableStateOf(false) }
    var flag by remember { mutableStateOf(false) }
    val buttonSize by animateDpAsState(
        targetValue = if (change) 32.dp else 24.dp
    )
    // Animatable
    val buttonColor  = remember { Animatable(Color.Gray) }
    LaunchedEffect(flag) {
        buttonColor.animateTo(if (flag) Color.Gray else Color.Red)
    }
    if (buttonSize == 32.dp) {
        change = false
    }
    IconButton(
        onClick = {
            change = true
            flag = !flag
        }
    ) {
        Icon(Icons.Rounded.Favorite,
            contentDescription = null,
            modifier = Modifier.size(buttonSize),
            tint = buttonColor.value)
    }

Animatable 中包括 animateTo 在内许多 API 都是挂起函数,需要在 CoroutineScope 中执行,可以使用 LaunchedEffect 为其提供所需的环境。Animatable 具有更多的灵活性:

  1. 允许设置一个不同的初始值,比如可以设置成 Green 然后再 animatTo Red
  2. 还提供了不少方法,比如 snapTo 可以立即到达目标值,中间没有过渡值。animateDecay 可以启动一个衰减动画

Animatable 完整代码:

kotlin
    var change by remember { mutableStateOf(false) }
    var flag by remember { mutableStateOf(false) }
    val buttonSize = remember { Animatable(24.dp, Dp.VectorConverter) }
    // Animatable
    val buttonColor  = remember { Animatable(Color.Gray) }
    LaunchedEffect(flag) {
        buttonSize.animateTo(if (change) 32.dp else 24.dp)
        buttonColor.animateTo(if (flag) Color.Gray else Color.Red)
    }
    if (buttonSize.value == 32.dp) {
        change = false
    }
    IconButton(
        onClick = {
            change = true
            flag = !flag
        }
    ) {
        Icon(Icons.Rounded.Favorite,
            contentDescription = null,
            modifier = Modifier.size(buttonSize.value),
            tint = buttonColor.value)
    }

Animatable 传入 Dp.VectorConverter 参数时,这是一个针对 Dp 类型的 TwoWayConverter。当传入 Float 或 Color 类型的值时可以直接传入。传入其他值时需要提供对应的 TwoWayConverter,Compose 为常用数据

类型都提供了对应的实现直接传入即可。

LaunchedEffect 会在 onActive 时执行,所以要确保初始值与 LaunchedEffect 初始 animateTo 的值是否相同,否则会触发动画,比如这里初始是 24dp,跳转也是跳转到 24dp,故没有变化。

Transition 过渡动画

updateTransition

kotlin
@Composable
fun <T> updateTransition(
    targetState: T,
    label: String? = null
): Transition<T> {
    val transition = remember { Transition(targetState, label = label) }
    transition.animateTo(targetState)
    DisposableEffect(transition) {
        onDispose {
            // Clean up on the way out, to ensure the observers are not stuck in an in-between
            // state.
            transition.onDisposed()
        }
    }
    return transition
}

重要的是 targetState 状态,是动画执行所需的依赖,获取到 Transition 实例后,可以创建动画中所有的属性状态,当状态改变时,每个属性状态都会得到相应的更新。下面是一个例子,点击时文案消失,同时标签从底部上升:

kotlin
sealed class SwitchState {
    object OPEN: SwitchState()
    object CLOSE: SwitchState()
}

@Composable
fun SwitchBlock() {
    var selectedState: SwitchState by remember { mutableStateOf(SwitchState.CLOSE) }
    val transition = updateTransition(selectedState, label = "switch_transition")
    val selectBarPadding by transition.animateDp(transitionSpec = { tween(1000) }, label = "" ) {
        when(it) {
            SwitchState.CLOSE -> 40.dp
            SwitchState.OPEN -> 0.dp
        }
    }
    val textAlpha by transition.animateFloat(transitionSpec = { tween(1000) }, label = "") {
        when(it) {
            SwitchState.CLOSE -> 1f
            SwitchState.OPEN -> 0f
        }
    }
    Box(
        modifier = Modifier
            .size(150.dp)
            .padding(8.dp)
            .clip(RoundedCornerShape(10.dp))
            .clickable {
                selectedState = if (selectedState == SwitchState.OPEN) SwitchState.CLOSE else SwitchState.OPEN
            }
    ) {
        Image(
            painter = painterResource(R.drawable.avatar),
            contentDescription = null,
            contentScale = ContentScale.FillBounds
        )
        Text(
            text = "点我",
            fontSize = 30.sp,
            fontWeight = FontWeight.W900,
            color = Color.White,
            modifier = Modifier
                .align(Alignment.Center)
                .alpha(textAlpha)
        )
        Box(modifier = Modifier
            .align(Alignment.BottomCenter)
            .fillMaxWidth()
            .height(40.dp)
            .padding(top = selectBarPadding)
            .background(Color(0xFF5FB878))
        ) {
            Row(modifier = Modifier
                .align(Alignment.Center)
                .alpha(1 - textAlpha)
            ) {
                Icon(painter = painterResource(R.drawable.ic_collect), contentDescription = "star", tint = Color.White)
                Spacer(modifier = Modifier.width(2.dp))
                Text(
                    text = "已选择",
                    fontSize = 20.sp,
                    fontWeight = FontWeight.W900,
                    color = Color.White
                )
            }
        }
    }
}

AnimatedVisibility 和 AnimatedContent 配合使用

这两个扩展函数可以将 Transition 的 State 转换成所需的 TargetState,将这两个动画状态通过 Transition 向外暴露,以供使用:

kotlin
@Composable
fun AnimatedWithTransition() {
    var selected by remember { mutableStateOf(false) }
    val transition = updateTransition(selected)
    val borderColor by transition.animateColor { isSelected ->
        if (isSelected) Color.Magenta else Color.White
    }
    val elevation by transition.animateDp { isSelected ->
        if (isSelected) 10.dp else 2.dp
    }
    Surface(
        onClick = { selected = ! selected },
        shape = RoundedCornerShape(8.dp),
        border = BorderStroke(2.dp, borderColor),
        tonalElevation = elevation
    ) {
        Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
            Text(text = "Hello,world!")
            // 作为过渡动画的一部分
            transition.AnimatedVisibility(
                visible = { targetSelected -> targetSelected },
                enter = expandVertically(),
                exit = shrinkVertically()
            ) {
               Text(text = "It is fine today")
            }
            transition.AnimatedContent { targetState ->
                if (targetState) {
                    Text("Selected")
                } else {
                    Icon(imageVector = Icons.Default.Phone, contentDescription = null)
                }
            }
        }
    }
}

Transition 动画封装

kotlin
// 动画的封装
enum class BoxState {
    Collapsed,
    Expanded
}

@Composable
fun AnimatingBox(boxState: BoxState) {
    val transitionData = updateTransitionData(boxState)
    Box(
        modifier = Modifier
            .background(transitionData.color)
            .size(transitionData.size)
    )
}

private class TransitionData(
    color: State<Color>,
    size: State<Dp>
) {
    val color by color
    val size by size
}

// 创建一个Transition并返回动画值
@Composable
private fun updateTransitionData(boxState: BoxState) : TransitionData {
    val transition = updateTransition(boxState)
    val color = transition.animateColor { state ->
        when (state) {
            BoxState.Collapsed -> Color.Gray
            BoxState.Expanded -> Color.Red
        }
    }
    val size = transition.animateDp { state ->
        when (state) {
            BoxState.Collapsed -> 64.dp
            BoxState.Expanded -> 128.dp
        }
    }
    return remember(transition) { TransitionData(color, size) }
}

rememberInfiniteTransition

这是一个无限循环的 Transition,会无限执行下去,子动画可以用 animateColor,animateFloat 等进行添加,另外还需指定 infiniteRepeatableSpec 来设置动画循环播放方式:

kotlin
@Composable
fun InfiniteTransitionStudy() {
    val infiniteTransition = rememberInfiniteTransition()
    val color by infiniteTransition.animateColor(
        initialValue = Color.Red,
        targetValue = Color.Green,
        animationSpec = infiniteRepeatable(
            animation = tween(1000, easing = LinearEasing), // 一个动画值转换持续1s,缓和方式为LinearEasing
            repeatMode = RepeatMode.Reverse // Reverse是1->2,2->1,Repeat是1->2,1->2
        )
    )
    Box(Modifier.fillMaxSize().background(color))
}

上面使用 infiniteRepeatable 创建了 infiniteRepeatableSpec,使用 tween 创建一个单词动画的 animationSpec。通过 repeateMode 指定循环播放方式。

AnimationSpec 动画规格

大部分动画 API 都支持设置 AnimationSpec 定义动画效果:

kotlin
val alpha by animateFloatAsState(
        targetValue = 1f,
        animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
    )
kotlin
interface AnimationSpec<T> {
    /**
     * Creates a [VectorizedAnimationSpec] with the given [TwoWayConverter].
     *
     * The underlying animation system operates on [AnimationVector]s. [T] will be converted to
     * [AnimationVector] to animate. [VectorizedAnimationSpec] describes how the
     * converted [AnimationVector] should be animated. E.g. The animation could simply
     * interpolate between the start and end values (i.e.[TweenSpec]), or apply spring physics
     * to produce the motion (i.e. [SpringSpec]), etc)
     *
     * @param converter converts the type [T] from and to [AnimationVector] type
     */
    fun <V : AnimationVector> vectorize(
        converter: TwoWayConverter<T, V>
    ): VectorizedAnimationSpec<V>
}

T 是当前动画数值类型,vectorize 用来创建矢量动画的配置,这是通过函数运算生成的,而 AnimationVector 就是用来参与计算的动画矢量。TwoWayConverter 将 T 转为参与动画计算的矢量数据。

spring 弹跳动画

kotlin
val value by animateFloatAsState(
    targetValue = 1f,
    // 还有个visibilityThreshold参数,表示动画达到这个阈值后,动画会停止
    animationSpec = spring(
        dampingRatio = Spring.DampingRatioHighBouncy, // 弹簧阻尼比,表示从一次弹跳到下一次弹跳衰减速度有多快
        stiffness = Spring.StiffnessMedium // 弹簧的刚度值,刚度值越大,弹簧到静止状态速度越快
    )
)

tween 补间动画

kotlin
animationSpec = tween(
            durationMillis = 300, // 动画执行时间
            delayMillis = 50, // 动画延迟执行
            easing = LinearOutSlowInEasing // 衰减曲线动画效果
        )

补间动画,动画效果基于时间计算,使用 Easing 指定不同的时间曲线动画效果。

keyframes 关键帧动画

kotlin
animationSpec = keyframes { 
    durationMillis = 375
    0.0f at 0 using LinearOutSlowInEasing // 0-15
    0.2f at 15 using FastOutLinearInEasing // 15-75
    0.4f at 75 // ms
    0.4f at 225 // ms
}

这里比如 0.2f at 15 using FastOutLinearInEasing表示在 15ms 时刻 value 应达到 0.2f,且使用相应的动画效果,使用中缀运算符进行配置。

repeatable 循环动画

kotlin
animationSpec = repeatable(
    iterations = 3,
    animation = tween(durationMillis = 300),
    repeatMode = RepeatMode.Reverse
)

可以创建一个 RepeatableSpec 实例,这是一个可以循环播放的动画,指定 TweenSpec 或 KeyFramesSpec 以及循环播放的方式。上面代表循环播放三次,循环播放模式是往返执行。

infiniteRepeatable 无限循环动画

与上面类似,只不过这里是无限执行的,没有 iterations 参数。

kotlin
animationSpec = infiniteRepeatable(
    animation = tween(300),
    repeatMode = RepeatMode.Reverse
)

也可参考前面介绍的 rememberInfiniteTransition()来创建无限循环的 Transition 动画。

snap 快闪动画

kotlin
animationSpec = snap(50) // 特殊动画,用没有中间过渡

Easing

Easing 是基于时间参数的函数,它的输入输出都是 0f-1f 的浮点数值。

kotlin
@Stable
fun interface Easing {
    fun transform(fraction: Float): Float
}

输入值表示当前动画在时间上的进度,返回值是当前 value 的进度,1.0 表示已经达到了 targetValue,Compose 也内置了多种 Easing 曲线:

  1. FastOutSlowEasing 默认 Easing,加速度起步,减速度收尾
  2. LinearOutSlowInEasing 匀速起步,减速度收尾
  3. FastOutLinearEasing 加速度起步,匀速收尾
  4. LinearEasing 匀速运动 \

也可用 CubicBezierEasing 三阶贝塞尔曲线自定义任意 Easing。

AnimationVector 动画矢量值

矢量动画是基于动画矢量值 AnimationVector 计算的。

Animatable 构造函数:

kotlin
class Animatable<T, V : AnimationVector>(
    initialValue: T, // T类型动画初始值
    val typeConverter: TwoWayConverter<T, V>, // 将T类型数值与V类型AnimationVector进行转换
    private val visibilityThreshold: T? = null, // 动画消失阈值
    val label: String = "Animatable"
)
TwoWayConverter
kotlin
fun <T, V : AnimationVector> TwoWayConverter(
    convertToVector: (T) -> V,
    convertFromVector: (V) -> T
): TwoWayConverter<T, V> = TwoWayConverterImpl(convertToVector, convertFromVector)
kotlin
val IntToVector: TwoWayConverter<Int, AnimationVector1D> = TwoWayConverter({ AnimationVector1D(it.toFloat())}, { it.value.toInt() })

不同类型的数值可以根据需求与不同的 AnimationVectorXD 进行转换,这里的 X 代表了信息的维度。例如一个 Int 可以与 AnimationVector1D 相互转换。

比如 Size 包含 width 和 height 两个维度的信息,可以与 AnimationVector2D 进行转换。常用的类型 Compose 已经提供了拓展实现。

自定义实现 TwoWayConverter
kotlin
data class MySize(val width: Dp, val height: Dp) 

@Composable
fun MyAnimation(targetSize: MySize) {
    val animSize: MySize by animateValueAsState<MySize, AnimationVector2D>(
        targetSize,
        TwoWayConverter(
            convertToVector = { size: MySize ->
                AnimationVector2D(size.width.value, size.height.value)
            },
            convertFromVector = { vector: AnimationVector2D ->
                MySize(vector.v1.dp, vector.v2.dp)
            }
        )
    )
}

骨架屏 Demo

kotlin
val barHeight = 10.dp
val spacerPadding = 3.dp
val roundedCornerShape = RoundedCornerShape(3.dp)

@Composable
fun ShimmerDemo() {
    val shimmerColors = listOf(
        Color.LightGray.copy(alpha = 0.6f),
        Color.LightGray.copy(alpha = 0.2f),
        Color.LightGray.copy(alpha = 0.6f)
    )
    val transition = rememberInfiniteTransition()
    val translateAnim = transition.animateFloat(
        initialValue = 0f,
        targetValue = 1000f,
        animationSpec = infiniteRepeatable(
            animation = tween(
                durationMillis = 1000,
                easing = FastOutSlowInEasing
            ),
            repeatMode = RepeatMode.Restart
        )
    )
    val brush = Brush.linearGradient(
        colors = shimmerColors,
        start = Offset.Zero,
        end = Offset(x = translateAnim.value, y = translateAnim.value)
    )
    ShimmerItem(brush)
}

@Composable
fun ShimmerItem(brush: Brush) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(all = 10.dp)
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Column(verticalArrangement = Arrangement.Center) {
                repeat(5) {
                    Spacer(modifier = Modifier.padding(spacerPadding))
                    Spacer(
                        modifier = Modifier
                            .height(barHeight)
                            .clip(roundedCornerShape)
                            .fillMaxWidth(0.7f)
                            .background(brush)
                    )
                    Spacer(modifier = Modifier.padding(spacerPadding))
                }
            }
            Spacer(modifier = Modifier.width(10.dp))
            Spacer(
                modifier = Modifier
                    .size(100.dp)
                    .clip(roundedCornerShape)
                    .background(brush)
            )
        }
        repeat(3) {
            Spacer(modifier = Modifier.padding(spacerPadding))
            Spacer(
                modifier = Modifier
                    .height(barHeight)
                    .clip(roundedCornerShape)
                    .fillMaxWidth()
                    .background(brush)
            )
            Spacer(modifier = Modifier.padding(spacerPadding))
        }
    }
}