How to Create a Shimmer Loading Effect in Jetpack Compose (WITHOUT Library!)

개요


Modifier 확장함수를 구현해 해당 확장함수만 Modifier 에 적용하면, 스켈레톤 이미지가 되는 로직이다.

Modifier.Extention

// 반짝임 효과 확장함수
fun Modifier.shimmerEffect(): Modifier = composed {
   // 실제 컴포즈의 크기를 추적한다.
    var size by remember {
        mutableStateOf(IntSize.Zero)
    }

    // X축으로 이동하는 무한 애니메이션 그라데이션을 정의한다.
    // 초기값은 크기의 -2배
    // 목표값은 크기의 2배
    val transition = rememberInfiniteTransition(label = "로딩중")
    val startOffsetX by transition.animateFloat(
        initialValue = -2 * size.width.toFloat(),
        targetValue = 2 * size.width.toFloat(),
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 1000)
        ),
        label = "로딩중"
    )

    // 그라데이션은 0, 0에서 시작하여 크기의 너비와 높이까지 이동한다.
    background(
        brush = Brush.linearGradient(
            colors = listOf(
                Color(0xFFB8B5B5),
                Color(0xFF8F8B8B),
                Color(0xFFB8B5B5)
            ),
            start = Offset(startOffsetX, 0f),
            end = Offset(startOffsetX + size.width.toFloat(), size.height.toFloat())
        )
    ).onGloballyPositioned { size = it.size }
}

코드 예시


Untitled

스켈레톤 화면 컴포즈 :

@Composable
fun SkeletonListItem(
    modifier: Modifier = Modifier,
    isLoading: Boolean,
    contentAfterLoading: @Composable () -> Unit
) {
    if(isLoading) {
        Row(modifier = modifier) {
            Box(
                modifier = Modifier
                    .size((100.dp))
                    .shimmerEffect()
            )
            Spacer(modifier = Modifier.width(16.dp))
            Column(
                modifier = Modifier.weight(1f)
            ) {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(20.dp)
                        .shimmerEffect()
                )
                Spacer(modifier = Modifier.height(16.dp))
                Box(
                    modifier = Modifier
                        .fillMaxWidth(0.7f)
                        .height(20.dp)
                        .shimmerEffect()
                )
            }
        }
    }
    else {
        contentAfterLoading()
    }
}

실제 표시될 컨텐츠 :

@Composable
fun Test() {
    var isLoading by remember { mutableStateOf(true) }

    LaunchedEffect(Unit) {
        delay(10000)
        isLoading = false
    }

    LazyColumn(modifier = Modifier.fillMaxSize().background(Color.White)) {
        items(20) {
            SkeletonListItem(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp),
                isLoading = isLoading
            ) {
                Row(modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp)) {
                    Icon(
                        modifier = Modifier.size(100.dp),
                        imageVector = Icons.Default.Home,
                        contentDescription = null
                    )
                    Spacer(modifier = Modifier.width(16.dp))
                    Text(
                        text = "This is a long text to show that out shimmer effect is working properly",
                    )
                }
            }
        }
    }
}