실제로 작업을 진행했던 것은 꽤 전의 일이지만, 기록 & 공유 용으로 글을 남긴다. 실제로 회사에서 업무로 진행했던 일이라 코드는 서비스적 내용을 제외하고 기능의 핵심 부분만 나와있다.
전달받아 진행해야할 작업은 다음과 같았다.
"앱 온보딩 과정에서 로티를 이용하여 서비스를 소개해야 한다"
디자인 시안을 받아보니 단순히 로티(Lottie)를 띄우기만 하는 것이 아닌 로티와 함께 텍스트, 버튼을 보여줘야 했고, 해당 작업의 핵심은 로티가 실행중일 때 그 정확한 시점(진행이 된 정도, 이하 Frame, F)를 아는 것이었다.
우선적으로 compose에서 lottie를 사용하기 위한 방법을 찾아보았고, lottie-compose 라는 라이브러리를 발견하여 사용하였다. 공식문서는 여기에서 확인할 수 있다.
dependencies {
...
implementation "com.airbnb.android:lottie-compose:$lottieVersion"
...
}
gradle 파일에 위와 같이 dependency를 추가해주고 sync를 한번 해주고 나면 작업을 위한 준비는 다 되었다.
공식문서를 보면 lottie를 띄우는 방식으로 아래 두 가지를 소개한다.
별다른 조건 없이 lottie를 재생시키기만 할 것이라면 첫번째 방식을 사용해도 되나, lottie를 의도한 타이밍에 실행시키거나, 원하는 지점으로 이동시키기 위해서는 두번째 방식을 이용해야 한다는 것을 알 수 있다.
본 작업에서는 lottie를 skip 하는 기능 또한 구현해야 했기에 두 번째 방식을 사용하였다.
아래 코드가 별다른 추가 요구조건 없이 lottieAnimatable을 사용하여 단순히 lottie를 띄운 코드이다.
val composition by rememberLottieComposition(
LottieCompositionSpec.RawRes([lottie json 파일])
)
val lottieAnimatable = rememberLottieAnimatable()
LaunchedEffect(composition) {
lottieAnimatable.animate(
composition = composition,
clipSpec = LottieClipSpec.Frame(0, 1200),
initialProgress = 0f
)
}
Box(
modifier = Modifier.fillMaxSize()
) {
LottieAnimation(
composition = composition,
progress = lottieAnimatable.progress,
contentScale = ContentScale.FillHeight
)
}
LaunchedEffect를 이용하여 lottie를 시작한다. 다만 LaunchedEffect의 key가 composition인 이유는 composition이 제대로 설정된 후에 LaunchedEffect가 한번 불려야 정상적으로 lottie animation이 재생되기 때문이다!
정상적으로 로티가 재생되는 것을 확인하고 나서 해야할 일은 타이밍을 맞추는 문제였다. 디자인 시안 자체에 Text 및 Button이 등장하고 사라지는 순간이 Frame 단위로 세세하게 주어져 있었기에 실행중인 로티의 현재 프레임을 알아낼 방법을 찾아야 했다.
찾아본 결과 lottie가 Frame 자체를 직접적으로 제공해주지는 않는다. 하지만 다행스럽게도 LottieAnimatable.value를 통해서 progress, 즉 0f~1f 사이의 현재 진행정도를 나타내주는 float 값을 얻을 수 있고,
LottieComposition.getFrameForProgress([progress])
로 계산해서 Frame 값을 구할 수 있다. 이 계산된 Frame을 이용하여 Text가 나타나게 하는 코드는 다음과 같다.
val composition by rememberLottieComposition(
LottieCompositionSpec.RawRes([lottie json 파일])
)
val lottieAnimatable = rememberLottieAnimatable()
val currentFrame = composition?.getFrameForProgress(lottieAnimatable.value)
val textAlpha =
if (currentFrame != null) {
when (currentFrame) {
in 0F..60F -> 0F
in 60F..117F -> currentFrame.normalize(60F, 117F)
in 117F..357F -> 1F
in 357F..390F -> 1 - currentFrame.normalize(357F, 390F)
else -> 0F
}
} else {
0F
}
Box(....) {
Text(
text = "~~~",
modifier = Modifier
.fillMaxWidth()
.alpha(textAlpha),
textAlign = TextAlign.Center,
style = ...
)
}
위와 같이 현재의 Frame을 통해 텍스트가 나타나기 시작하는 60F 부터 완전히 나타난 117F까지는 선형적으로 서서히 나타나게, 사라지기 시작하는 357F 부터 완전히 사라지는 390F 까지는 선형적으로 서서히 사라지도록 textAlpha값을 0~1F 사이의 값을 주게 하고 Text composable의 alpha값으로 적용하여 자연스럽게 나타나고 사라지는 Text를 구현할 수 있다.
마지막으로 추가했던 기능은 lottie를 스킵하는 기능이었다. 이 부분이 앞서 언급했던 lottieAnimatable을 사용했던 이유라고도 할 수 있다. 방법은 간단하다.
lottieAnimatable.snapTo(composition, 1f)
위와 같이 LottieAnimatable은 snapTo라는 함수를 제공하는데, 이를 통해서 lottie를 0f ~ 1f 사이의 원하는 지점으로 이동시킬 수 있다. 따라서 스킵 버튼의 onClick callback으로 위 코드를 넣어주면 된다. 다만 여기서 snapTo를 사용하기 위해선 실질적으로는 아래처럼 감싸서 사용해야 한다는 점을 잊지 말자.
val scope = rememberCoroutineScope()
...
onClick = {
scope.launch {
lottieAnimatable.snapTo(composition, 1f)
}
}
끝으로는 해당 작업을 통한 결과물 영상을 하나 남긴다. 글의 작성을 완료하는 지금 시점에서는 앱 온보딩 과정이 다시 바뀌어 이제는 찾아볼 수 없지만, 로티를 잘 활용했던 예시 정도로 참고하면 좋을 것 같다.