[Compose UI] 텍스트/이미지 반짝임 로딩화면 만들기

반짝임 애니메이션

앱을 사용하다보면, 화면이 로딩되는 과정에서 이미지나, 텍스트가 보여지는 공간에 반짝임 애니메이션이 보이며 로딩중이라는 느낌을 주는 경우가 많다. 과연 이런건 어떻게 구현할까?

 

오늘의 결과물

반짝임 애니메이션 만들기

확장 함수 만들기

Modifier클래스의 확장 함수를 만들어 반짝임 이미지를 생성하도록 하겠습니다.

Modifier의 확장 함수를 만들기 위해, 다음과 같이 작성해줍니다.

fun Modifier.shimmerEffect(): Modifier = composed {
}

함수 내부의 값은 가장 먼저 상태 값을 저장할 변수를 정의해줍니다.

var size by remember {
            mutableStateOf(IntSize.Zero)
        }
        val transition = rememberInfiniteTransition(
            label = ""
        )
        val startOffsetX by transition.animateFloat(
            initialValue = -2 * size.width.toFloat(),
            targetValue = 2 * size.width.toFloat(),
            animationSpec = infiniteRepeatable(
                animation = tween(2000)
            ),
            label = ""
        )

가장 먼저 size의 경우에는 컴포저블의 크기를 저장하는 변수입니다. IntSize.Zero의 경우에는 애니메이션의 widthheight값을 지정하기 위해 사용하였습니다.

특히 로딩의 경우에는 특정 시점까지는 무한히 반복되는 애니메이션이여야 하기 때문에 transition의 경우에는, 무한 전환을 위해 생성해줍니다.

다음은 startOffsetX로 해당 값은 반짝임 효과의 시작 위치를 저장하는 상태 변수입니다.

initialValuetargetValue의 경우에는 애니메이션의 시작과 끝을 정의합니다.
infinitiRepeatabletween(2000)을 이용하여 2초에 한번씩 반복되는 애니메이션을 생성해줍니다.

여기까지만, 작성을 하고 적용해본다면 아무런 효과가 없을 것입니다. 그 이유는 배경을 생성해주지 않았기 때문입니다.

배경 생성

다음 코드를 작성하여 배경 및 그라데이션을 설정해줍니다.

this
            .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())
                ),
                shape = RoundedCornerShape(6.dp)
            )
            .onGloballyPositioned {
                size = it.size
            }

Modifier의 확장 함수 내부의 작성하여 background를 설정해줍니다.
background의 경우에는 Brush.linearGradient를 이용하여 선형 그라데이션 배경을 설정합니다.
colors를 이용하여 색상을 설정하고, start를 이용하여 시작 위치, end를 이용하여 끝 위치를 설정합니다.

마지막으로 onGloballyPositioned를 통해 컴포저블 크기가 결정되었을 때 size의 상태를 업데이트 합니다.

적용해보기

적용하기 앞서 반짝임 로딩은 결국 키고 끌 수 있어야 하는게 필수이므로, 토글을 생성해주겠습니다.
또한 반짝임의 상태를 저장하는 변수를 생성하여 토글을 누를때마다 바뀌도록 설정하겠습니다.

Button(onClick = {
                        isShimmering = !isShimmering
                    }) {
                        Text(text = if(isShimmering) "turn off" else "turn on")
                    }

그리고, 토글을 누를 때 반짝임 이펙트가 사라지도록 해야하기 때문에 기존에 작성한 shimmerEffect함수의 값을 약간 수정하겠습니다.

아래와 같이 상태 변수를 받아서 상태에 따라 반짝임을 적용하도록 하겠습니다.

private fun Modifier.shimmerEffect(isShimmering : Boolean): Modifier = composed {
        if(!isShimmering) return@composed this

        // ...
}       

이제 어느정도의 틀은 완성했으므로 테스트를 해보겠습니다.

Column {
                    var isShimmering by remember { mutableStateOf(true) }
                    Button(onClick = {
                        isShimmering = !isShimmering
                    }) {
                        Text(text = if(isShimmering) "turn off" else "turn on")
                    }
                    LazyColumn{ items(10){ idx->
                        Row(Modifier.padding(10.dp)){
                            Spacer(modifier = Modifier
                                .width(50.dp)
                                .height(50.dp)
                                .background(color = Color.Black, shape = RoundedCornerShape(6.dp))
                                .shimmerEffect(isShimmering)
                            )
                            Spacer(modifier = Modifier.width(10.dp))
                            Text(
                                text = if(!isShimmering) "안녕하세용" else "",
                                modifier = Modifier
                                    .fillMaxWidth(1f)
                                    .shimmerEffect(isShimmering)
                            )
                        }
                    }
                    }
                }

결과

오늘의 결과물

 

전체 코드

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Weather_animationTheme {
                Column {
                    var isShimmering by remember { mutableStateOf(true) }
                    Button(onClick = {
                        isShimmering = !isShimmering
                    }) {
                        Text(text = if(isShimmering) "turn off" else "turn on")
                    }
                    LazyColumn{ items(10){ idx->
                        Row(Modifier.padding(10.dp)){
                            Spacer(modifier = Modifier
                                .width(50.dp)
                                .height(50.dp)
                                .background(color = Color.Black, shape = RoundedCornerShape(6.dp))
                                .shimmerEffect(isShimmering)
                            )
                            Spacer(modifier = Modifier.width(10.dp))
                            Text(
                                text = if(!isShimmering) "안녕하세용" else "",
                                modifier = Modifier
                                    .fillMaxWidth(1f)
                                    .shimmerEffect(isShimmering)
                            )
                        }
                    }
                    }
                }
            }
        }
    }
    private fun Modifier.shimmerEffect(isShimmering : Boolean): Modifier = composed {
        if(!isShimmering) return@composed this

        var size by remember {
            mutableStateOf(IntSize.Zero)
        }
        val transition = rememberInfiniteTransition(
            label = ""
        )
        val startOffsetX by transition.animateFloat(
            initialValue = -2 * size.width.toFloat(),
            targetValue = 2 * size.width.toFloat(),
            animationSpec = infiniteRepeatable(
                animation = tween(2000)
            ),
            label = ""
        )
        this
            .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())
                ),
                shape = RoundedCornerShape(6.dp)
            )
            .onGloballyPositioned {
                size = it.size
            }


    }



}