Skip to content

骨架屏与微光 (Shimmer)

在数据加载时,显示一个类似内容布局的灰色占位符,并带有微光扫过的动画效果,这种体验远优于单纯的旋转圆圈。

1. 基础实现原理

微光效果本质上是一个线性渐变 (LinearGradient) 的画笔,在水平方向上不断移动。

kotlin
fun Modifier.shimmer(): Modifier = composed {
    val transition = rememberInfiniteTransition()
    val translateAnim by transition.animateFloat(
        initialValue = 0f,
        targetValue = 1000f,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 1000, easing = FastOutSlowInEasing),
            repeatMode = RepeatMode.Restart
        )
    )

    val brush = Brush.linearGradient(
        colors = listOf(
            Color.LightGray.copy(alpha = 0.6f),
            Color.LightGray.copy(alpha = 0.2f),
            Color.LightGray.copy(alpha = 0.6f),
        ),
        start = Offset.Zero,
        end = Offset(x = translateAnim, y = translateAnim)
    )

    this.background(brush)
}

2. 第三方库 (推荐)

虽然手写不难,但处理复杂的圆角和组合布局时,使用成熟的库更方便。Google 官方并没有提供 Shimmer,社区最流行的是 com.valentinilk.shimmer:compose-shimmer

依赖

kotlin
implementation("com.valentinilk.shimmer:compose-shimmer:1.2.0")

使用

只需在父容器加上 Modifier.shimmer(),子元素就会自动应用效果。

kotlin
import com.valentinilk.shimmer.shimmer

@Composable
fun SkeletonItem() {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
            .shimmer() // 魔法发生在这里
    ) {
        // 圆形头像占位
        Box(
            modifier = Modifier
                .size(48.dp)
                .background(Color.Gray, CircleShape)
        )
        
        Spacer(modifier = Modifier.width(8.dp))
        
        Column {
            // 标题占位
            Box(
                modifier = Modifier
                    .width(100.dp)
                    .height(20.dp)
                    .background(Color.Gray, RoundedCornerShape(4.dp))
            )
            Spacer(modifier = Modifier.height(8.dp))
            // 内容占位
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(14.dp)
                    .background(Color.Gray, RoundedCornerShape(4.dp))
            )
        }
    }
}