
shorts 탭에서 데이터가 로딩되기 전에 저렇게 shimmering effect가 있다. 확인해보니 안드로이드 버전에는 없고 아이폰, 아이패드 버전에만 존재하는 듯 하다.
너무 순식간에 지나가서 중간과정을 캡쳐해보았다.
다음과 같이 그라데이션이 있는 사각형이 왼쪽에서 오른쪽으로 움직이면서 shimmering 효과를 내고 있었다.
shimmering effect를 보여주는 placeholder를 먼저 만들었다.
shimmering effect의 사각형의 크기를 설정해준다.
var size by remember {
mutableStateOf(IntSize.Zero)
}
val shimmeringWidth = size.width / 4f
val placeholderColor = if (backgroundColor.luminance() > 0.5f) {
Color(0xFFD3D3D3).copy(alpha = 0.6f)
} else {
Color(0xFF4F4F4F).copy(alpha = 0.6f)
}
Box(
modifier = modifier
.onGloballyPositioned {
size = it.size
}
.clip(
shape = RoundedCornerShape((size.height / 2).toDp())
)
.background(placeholderColor)
.shimmerEffect(
size = size,
shimmeringWidthPx = shimmeringWidth,
placeholderColor = placeholderColor
)
)
placeholder는 background color를 매개변수로 받는데 이는 placeholder의 상위 composable의 background color이다.
실제 애니메이션에서 배경색이 어두울 때는 어두운 회색을 띄고 있지만 데이터가 로드되어 배경색이 흰색이 되면

다음과 같이 투명한 흰색이 된다.
따라서, 배경색에 맞춰서 색상을 변경해주도록 하였다.
val transition = rememberInfiniteTransition(label = "")
val startOffsetX by transition.animateFloat(
initialValue = -shimmeringWidthPx,
targetValue = size.width.toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1500,
easing = FastOutSlowInEasing
)
),
label = ""
)
graphicsLayer {
translationX = startOffsetX
}
계속 반복적으로 shimmering이 일어나야 하므로 infiniteRepeatable을 사용했고 마지막에 살짝 속도가 느려지기에 FastOutSlowInEasing 효과를 사용했다.
그리고 translationX 에 적용해서 사각형이 오른쪽으로 계속 이동하도록 하였다.
val brush = Brush.horizontalGradient(
colors = listOf(
shimmerColor.copy(alpha = 0.1f),
shimmerColor.copy(alpha = 0.4f),
shimmerColor.copy(alpha = 0.1f)
),
startX = 0f,
endX = shimmeringWidthPx
)
.drawWithContent {
drawRect(
brush = brush,
topLeft = Offset(x = 0f, y = 0f),
size = Size(shimmeringWidthPx, size.height.toFloat())
)
}
가운데를 기점으로 양쪽으로 그라데이션이 퍼져나가는 구조라 다음과 같이 alpha를 이용해서 구현하였다.
start와 end의 위치를 설정안해주면 전체 placeholder 크기에 대해서 그라데이션을 적용하고 그 중 drawRect한 부분만 보여주는 식으로 구현되어서 크기를 정해주었다.
shimmering effect효과는 Modifier로 따로 분리하였다. placeholder에서 제공해준 크기와 배경색을 토대로 만들었다.
fun Modifier.shimmerEffect(
size: IntSize,
shimmeringWidthPx: Float,
placeholderColor: Color,
): Modifier =
composed {
//애니메이션, brush 코드
}
}
size - shimmering이 어디까지 이동해야 하는 지 알기 위함
shimmeringWidthPx - shimmering effect의 사각형의 크기
placeholderColor - placeholder의 크기를 토대로 shimmering의 색상 결정
composed 함수는 Modifier에 추가적인 속성을 합쳐주는 함수이다.
@Composable
fun ShimmeringPlaceholder(
modifier: Modifier = Modifier,
backgroundColor: Color,
) {
var size by remember {
mutableStateOf(IntSize.Zero)
}
val shimmeringWidth = size.width / 4f
val placeholderColor = if (backgroundColor.luminance() > 0.5f) {
Color(0xFFD3D3D3).copy(alpha = 0.6f)
} else {
Color(0xFF4F4F4F).copy(alpha = 0.6f)
}
Box(
modifier = modifier
.onGloballyPositioned {
size = it.size
}
.clip(
shape = RoundedCornerShape((size.height / 2).toDp())
)
.background(placeholderColor)
.shimmerEffect(
size = size,
shimmeringWidthPx = shimmeringWidth,
placeholderColor = placeholderColor
)
)
}
fun Modifier.shimmerEffect(
size: IntSize,
shimmeringWidthPx: Float,
placeholderColor: Color,
): Modifier =
composed {
val transition = rememberInfiniteTransition(label = "")
val startOffsetX by transition.animateFloat(
initialValue = -shimmeringWidthPx,
targetValue = size.width.toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1500,
easing = FastOutSlowInEasing
),
),
label = ""
)
val shimmerColor = if (placeholderColor.luminance() > 0.5f) {
Color.White.copy(alpha = 0.4f)
} else {
Color.Gray.copy(alpha = 0.4f)
}
val brush = Brush.horizontalGradient(
colors = listOf(
shimmerColor.copy(alpha = 0.1f),
shimmerColor.copy(alpha = 0.4f),
shimmerColor.copy(alpha = 0.1f)
),
startX = 0f,
endX = shimmeringWidthPx
)
graphicsLayer {
translationX = startOffsetX
}.drawWithContent {
drawRect(
brush = brush,
topLeft = Offset(x = 0f, y = 0f),
size = Size(shimmeringWidthPx, size.height.toFloat())
)
}
}