Compose动画
更新: 1/31/2026 字数: 0 字 时长: 0 分钟
动画分类
高级别 API | API | 说明 |
|---|---|---|
| AnimatedVisibility | UI 元素进入/退出时过渡动画 | |
| AnimatedContent | 布局内容变化时的动画 | |
| Modifier.animateContentSize | 布局大小变化时的动画 | |
| Crossfade | 两个布局切换时淡入/淡出动画 |
| 分类 | API | 说明 |
|---|---|---|
低级别 API | animate * AsState | 单个值动画 |
| Animatable | 可动画的数值容器 | |
| updateTransition | 组合动画 | |
| rememberInfiniteTransition | 组合无限执行动画 | |
| TargetBasedAnimation | 自定义执行时间的低级别动画 |
高级别动画 API
AnimatedVisibility
主要作用时控制显隐状态
@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)例子:
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类型参数的重载方法
@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)
}看一下这个类型的构造:
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 目标状态,这两个的不同驱动了动画的执行。
val state = remember {
MutableTransitionState(false).apply {
targetState = true
}
}
Column {
AnimatedVisibility(visibleState = state) {
Text("Hello,world!")
}
}currentState = false, targetState = true,所以当上屏时,状态不同,动画会立即执行。
自定义 Enter/Exit 动画
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
主要用于实现不同组件的平滑切换动画:
@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,不然看着很怪。
@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类型的参数:
infix fun EnterTransition.togetherWith(exit: ExitTransition) = ContentTransform(this, exit)可以使用 EnterTransition.togetherwith() / EnterTransition togetherwith ExitTransition连接,比如实现 Slide 从右到左:
AnimatedContent(targetState = count,
transitionSpec = {
(slideInHorizontally { fullWidth -> fullWidth } + fadeIn()).togetherWith(
slideOutHorizontally { fullWidth -> -fullWidth } + fadeOut()
)
}
) { targetCount ->
Text("Count: $targetCount")
}SizeTransform 定义大小动画
自定义过渡动画时,可以使用 using 操作符连接 SizeTransform。可以使我们预先获取到 currentContent 和 targetContent 的 Size 值,并允许我们来定制尺寸变化的过渡动画效果:
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。
var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage) { screen ->
when(screen) {
"A" -> Text("Page A")
"B" -> Text("Page B")
}
}Modifier.animateContentSize
是 Modifier 的一个修饰符方法。用途非常专一,当容器尺寸变化时,会通过动画进行过渡,开箱即用。
@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
类似于传统视图中的属性动画,可以自动完成从当前值到目标值过渡的估值计算。
@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
)
}以一个点赞按钮的点击效果为例:
@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)
}
}这里大概流程如下:
- 点击后 change 变为 true,然后 buttonSize 由 24dp 过渡到 32dp
- flag 取反,实现由红过渡到灰或者由灰过渡到红
- 当 buttonSize 变为 32dp 时,change 被重置为 false,会恢复到 24dp。
Compose 为常用数据类型都提供了 animate * AsState 方法,例如 Float,Color,Dp,Size,Bounds,Offset,Rect,Int,IntOffset 和 IntSize 等,如果其他的值可以使用通用类型 animateValueAsState。
Animatable
这是一个数值包装器,它的 animateTo 方法可以根据数值的变化设置动画效果,animate * AsState 背后就是基于 Animatable 实现的。
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 具有更多的灵活性:
- 允许设置一个不同的初始值,比如可以设置成 Green 然后再 animatTo Red
- 还提供了不少方法,比如 snapTo 可以立即到达目标值,中间没有过渡值。animateDecay 可以启动一个衰减动画
Animatable 完整代码:
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
@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 实例后,可以创建动画中所有的属性状态,当状态改变时,每个属性状态都会得到相应的更新。下面是一个例子,点击时文案消失,同时标签从底部上升:
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 向外暴露,以供使用:
@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 动画封装
// 动画的封装
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 来设置动画循环播放方式:
@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 定义动画效果:
val alpha by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
)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 弹跳动画
val value by animateFloatAsState(
targetValue = 1f,
// 还有个visibilityThreshold参数,表示动画达到这个阈值后,动画会停止
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy, // 弹簧阻尼比,表示从一次弹跳到下一次弹跳衰减速度有多快
stiffness = Spring.StiffnessMedium // 弹簧的刚度值,刚度值越大,弹簧到静止状态速度越快
)
)tween 补间动画
animationSpec = tween(
durationMillis = 300, // 动画执行时间
delayMillis = 50, // 动画延迟执行
easing = LinearOutSlowInEasing // 衰减曲线动画效果
)补间动画,动画效果基于时间计算,使用 Easing 指定不同的时间曲线动画效果。
keyframes 关键帧动画
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 循环动画
animationSpec = repeatable(
iterations = 3,
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)可以创建一个 RepeatableSpec 实例,这是一个可以循环播放的动画,指定 TweenSpec 或 KeyFramesSpec 以及循环播放的方式。上面代表循环播放三次,循环播放模式是往返执行。
infiniteRepeatable 无限循环动画
与上面类似,只不过这里是无限执行的,没有 iterations 参数。
animationSpec = infiniteRepeatable(
animation = tween(300),
repeatMode = RepeatMode.Reverse
)也可参考前面介绍的 rememberInfiniteTransition()来创建无限循环的 Transition 动画。
snap 快闪动画
animationSpec = snap(50) // 特殊动画,用没有中间过渡Easing
Easing 是基于时间参数的函数,它的输入输出都是 0f-1f 的浮点数值。
@Stable
fun interface Easing {
fun transform(fraction: Float): Float
}输入值表示当前动画在时间上的进度,返回值是当前 value 的进度,1.0 表示已经达到了 targetValue,Compose 也内置了多种 Easing 曲线:
- FastOutSlowEasing 默认 Easing,加速度起步,减速度收尾
- LinearOutSlowInEasing 匀速起步,减速度收尾
- FastOutLinearEasing 加速度起步,匀速收尾
- LinearEasing 匀速运动 \
也可用 CubicBezierEasing 三阶贝塞尔曲线自定义任意 Easing。
AnimationVector 动画矢量值
矢量动画是基于动画矢量值 AnimationVector 计算的。
Animatable 构造函数:
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
fun <T, V : AnimationVector> TwoWayConverter(
convertToVector: (T) -> V,
convertFromVector: (V) -> T
): TwoWayConverter<T, V> = TwoWayConverterImpl(convertToVector, convertFromVector)val IntToVector: TwoWayConverter<Int, AnimationVector1D> = TwoWayConverter({ AnimationVector1D(it.toFloat())}, { it.value.toInt() })不同类型的数值可以根据需求与不同的 AnimationVectorXD 进行转换,这里的 X 代表了信息的维度。例如一个 Int 可以与 AnimationVector1D 相互转换。
比如 Size 包含 width 和 height 两个维度的信息,可以与 AnimationVector2D 进行转换。常用的类型 Compose 已经提供了拓展实现。
自定义实现 TwoWayConverter
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
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))
}
}
}