본문 바로가기

Android/Utility

[Android] Lottie Animation을 적용해보자.

728x90

실무를 진행하다 보면, 로딩 화면이나 배너 화면에서 다양한 애니메이션을 경험할 수 있다.

이 애니메이션들은 gif 파일을 사용하여 보여주는 경우도 있겠지만, Lottie를 사용하여 애니메이션을 보여주는 경우도 있다.

 

이번 글에서는 간단하게 gif와 Lottie에 대한 차이와, 사용하는 방법에 대해서 작성해보고자 한다.


우선,

Gif와 Lottie의 차이부터 확인해 보고 넘어가자.

 

gif는 가능한 작은 용량의 파일로 애니메이션을 표현하기 위한 파일 형식으로, 소리 없이 재생되는 영상 파일이라고 생각하면 편하다.

쉽게 인터넷에서 찾을 수 있는 움직이는 짤과 같은 것들은 gif 확장자로 저장된 파일이라고 볼 수 있다.

영상 파일이라고 생각하면 편하다. 라고 언급한 이유는 다양한 것들이 있는데, 가장 간단하고 쉽게 이해할 수 있는 것은 특정 홈페이지에서는 gif 파일의 용량에 따라서 이미지가 아닌 영상 (mp4)로 분류해서 파일을 저장하고 관리한다고 하기 때문이다.

 

lottie는 gif와 마찬가지로 애니메이션을 표현하기 위한 파일 형식인데, lottie는 JSON을 기반으로 구현된 애니메이션 형태로 애플리케이션 단에서 애니메이션을 사용할 때 다양한 장점을 갖고 사용할 수 있다고 볼 수 있다.

 

즉, gif와 lottie 모두 애니메이션을 표현할 때 사용할 수 있으며, 파일의 형식이 다르기 때문에 각자 가지고 있는 장단점이 다르게 된다.

 

그렇다면, gif와 lottie중 어느 것을 사용하면 되는가?

상황에 따라서 다르겠지만, 모바일 단에서 애니메이션을 사용해야 한다.라고 하면은 lottie를 사용하는 것이 장점이 훨씬 더 많아 보인다.

 

gif와 비교해서 lottie의 장점은 해당 LottieFiles 페이지를 보면 자세히 나와있는데, 필자는 여기서 가장 중요한 장점은

첫 번째로 파일의 크기라고 생각한다.

lottie는 JSON 기반으로 구현 된 애니메이션 파일이기 때문에 gif 파일보다 월등히 낮은 용량을 사용하게 된다.

낮은 용량을 사용한다는 것은, 로딩 속도가 그만큼 빨라진다는 것이며 그것으로 인해 얻을 수 있는 사용자 경험의 만족도는 상당히 차이가 나게 될 것이다.

또한, JSON 파일을 로컬에 저장하여 사용해야 하는 경우도 많은데, 앱 내에 고용량 gif를 저장하고 사용하는 것보다 저 용랑 JSON 파일을 저장해서 사용하는 것이 앱의 크기 측면에서도 이득이라고 볼 수 있다.

 

두 번째로는 품질이다.

gif파일은 해상도가 정해져있는 파일이기 때문에, gif파일을 보여줄 때의 크기에 따라 품질이 크게 깨지는 것을 확인할 수 있다.

비율로 저장하여 보여주고 있던 gif 파일의 경우, 작은 화면에서 볼 때는 불편함 없이 보이겠지만 큰 화면으로 보게 된다면 품질이 깨져 생각하는 것보다 자연스럽지 않은 애니메이션을 볼 수 있다.

하지만, lottie는 JSON으로 되어있기 때문에 해상도가 정해져있지 않다. 파일 자체가 코드화되어있기 때문에 다른 해상도로 확인한다고 하더라도 품질이 변화하지 않는다.

 

여기까지 알아보고 필자는 다음과 같은 의문이 들었다.

Gif파일의 단점은 알겠는데, Lottie 말고 SVG로 사용하면 되지 않을까?

SVG도 벡터 파일이기 때문에 파일 자체의 용량도 작고, 품질적으로도 떨어지지 않기 때문이다.

 

그래서 svg와 lottie의 차이점을 찾아보았는데, 결과부터 말하자면 보여주고자 하는 애니메이션에 따라서 다른 파일을 선택해서 사용하면 된다. 인 것으로 보인다.

lottie는 그래픽 디자인 소프트웨어에서 만든 복잡하고 고품질의 애니메이션을 표현하기에 적합하고, svg는 단순한 벡터 그래픽에 적합한 파일이라고 한다.

이것들 외에도 svg와 lottie의 차이에 따라서 취사선택하여 애니메이션을 보여주면 될 것으로 보인다.


일단 애니메이션을 보여주기 위한 파일들에 대해 알아보았으니, 간단하게 적용해보도록 하자.

 

우선 gif 파일부터 적용해보자.

필자는 Coil 라이브러리를 사용하여 gif 파일을 통한 애니메이션을 보여주도록 하였다.

 

우선 Coil 라이브러리를 추가해 주고, 

implementation "io.coil-kt:coil-compose:2.5.0"
implementation "io.coil-kt:coil-gif:2.6.0"

 

이미지를 보여주기 위한 ImageRequest 객체를 만들어주도록 하자.

val imageRequest = remember(replayKey) {
    ImageRequest.Builder(context)
        .data(R.drawable.gif_lottie_sample)
        .crossfade(true)
        .build()
}

 

그리고 ImageRequest 객체를 AsyncImagePainter 객체에 넣어준 후

val painter = rememberAsyncImagePainter(
    model = imageRequest
)

 

Coil Image Component에 넣어주면 된다.

Image(
    painter = painter,
    contentDescription = null,
    modifier = Modifier
        .size(100.dp)
)

 

물론, 이렇게만 하면 일반적인 이미지는 보일 수 있겠지만 gif 파일은 로드되지 않는다.

ImageRequest 객체를 만들 때, Gif파일에 대한 Decoder를 추가해줘야 하기 때문이다.

ImageRequest.Builder(context)
    .data(R.drawable.gif_lottie_sample)
    .repeatCount(0) // GIF를 한 번만 재생하도록 설정
    .crossfade(true)
    .decoderFactory(
        if (SDK_INT >= 28) {
            ImageDecoderDecoder.Factory()
        } else {
            GifDecoder.Factory()
        }
    )
    .onAnimationStart {
        Log.d("CoilTransform", "onAnimationStart")
    }
    .onAnimationEnd {
        Log.d("CoilTransform", "onAnimationEnd")
    }
    .build()

 

정상적으로 gif 파일 로드가 가능한 형태의 imageRequest 객체는 다음과 같다.

기본적으로 반복 횟수는 무한히 반복하도록 되어있지만, 필자는 한 번만 반복하도록 하기 위해 repeatCount를 0으로 설정하여 첫 재생 이후 반복하지 않도록 구현하였다.

 

onAnimation~ 관련된 api는 gif파일이 재생되고, 멈췄을 때의 callBack을 받을 수 있는 함수가 있길래 추가해 둔 부분이기 때문에 해당 부분은 없어도 상관없다.

 

이처럼 ImageRequest 객체만 만들어두면 쉽게 gif 파일을 재생할 수 있다.


다음으로는 Lottie 파일을 재생해 보도록 하자.

Lottie 파일을 재생하기 위해서는 airbnb에서 만든 라이브러리를 사용하는 것이 가장 간편하고 대표적이다.

 

따라서 Lottie 라이브러리를 추가해 주고,

implementation "com.airbnb.android:lottie-compose:6.0.0"

 

lottieComposition 객체를 만들어 준다.

val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.lottie_sample))

 

gif 파일을 재생할 때 해당 파일은 drawable 패키지 아래에 넣었는데, Lottie 라이브러리에서 불러올 파일을 지정하는 함수가 RawRes여서 raw 패키지 아래에 파일을 저장해 두고 사용하였다.

 

lottie 객체를 만들어 주었으면,

LottieAnimation(
    modifier = Modifier.size(100.dp),
    composition = composition,
    iterations = Int.MAX_VALUE
)

 

이처럼 LottieAnimation을 재생할 수 있는 Component를 사용하여 해당 Lottie 파일을 재생하도록 한다.

반복 횟수는 default로 1로 저장되어 있기 때문에, 무한하지는 않지만 최대치로 반복시키기 위해 Int.MAX_VALUE로 설정해 두었다.

 

이렇게 간단하게 Lottie를 재생시킬 수 있는데, 이것만 기록하기 위해 글을 작성한 것은 당연히 아니다.

필자가 업무를 진행하면서, Lottie는 1회 재생시키되 해당 객체를 클릭하는 이벤트가 발생하면 다시 Lottie를 재생시켜야 했다.

 

그렇다면 위에 선언한 객체들을 커스텀하여 1회만 재생시키고, 클릭 시 이벤트를 받아서 다시 재생시키도록 해보자.

우선 힌트를 얻기 위해 LottieAnimation Component를 확인해 보자.

@Composable
fun LottieAnimation(
    composition: LottieComposition?,
    progress: () -> Float,
    modifier: Modifier = Modifier,
    outlineMasksAndMattes: Boolean = false,
    applyOpacityToLayers: Boolean = false,
    enableMergePaths: Boolean = false,
    renderMode: RenderMode = RenderMode.AUTOMATIC,
    maintainOriginalImageBounds: Boolean = false,
    dynamicProperties: LottieDynamicProperties? = null,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    clipToCompositionBounds: Boolean = true,
    fontMap: Map<String, Typeface>? = null,
)

 

많은 변수들을 넣어서 커스텀할 수 있는 것으로 보이는데, 여기서 눈에 띄는 것은 progress이다.

해당 변수에 대한 설명을 보면 다음과 같다.

A provider for the progress (between 0 and 1) that should be rendered. If you want to render a specific frame, you can use LottieComposition. getFrameForProgress. In most cases, you will want to use one of the overloaded LottieAnimation composables that drives the animation for you. The overloads that have isPlaying as a parameter instead of progress will drive the animation automatically. You may want to use this version if you want to drive the animation from your own Animatable or via events such as download progress or a gesture.

 

Lottie로 사용 중인 애니메이션의 진행률을 0과 1 사이의 값으로 표현해 준다고 하며, download progress 같은 기능을 구현할 때도 해당 값을 사용하면 된다고 한다.

 

그렇다면 progress는 어떻게 사용해야 하는가? 

LottieAnimation.kt 파일을 확인하다 보면 다음과 같은 코드를 확인할 수 있다.

val progress by animateLottieCompositionAsState(
    composition,
    isPlaying,
    restartOnPlay,
    reverseOnRepeat,
    clipSpec,
    speed,
    iterations,
)

 

LottieAnimation의 progress를 사용할 때 우리가 사용해야 하는 방법에 대해 알 수 있다.

 

그렇다면 해당 progress 변수를 사용하여 반복시킬 수 있도록 구현해 보자.

val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.lottie_sample))
var isPlaying by remember { mutableStateOf(true) }
val progress by animateLottieCompositionAsState(
    composition = composition,
    isPlaying = isPlaying,
    iterations = 1,
    speed = 1f,
    restartOnPlay = true
)

 

animateLottieCompositionAsState를 사용할 때 composition을 제외한 나머지는 모두 default 값이 설정되어 있지만, 커스텀을 많이 할 것이라고 생각하는 것들을 포함해서 선언해 두었다.

여기서 중요한 부분은 isPlaying으로, isPlaying이 false에서 true로 변할 때 restartOnPlay 값이 true라면 Lottie Animation이 재실행되는 것이다.

 

LottieAnimation(
    modifier = Modifier
        .size(100.dp),
    composition = composition,
    progress = {
        progress
    },
)

 

기본적으로 progress는 다음과 같이 선언해 두면 되는데, 이렇게만 선언하고 사용하면 기존에 progress를 사용하지 않을 때와 다른 것이 없다.

isPlaying이라는 변수는 LottieAnimation 밖에 선언되어 있기 때문에, 직접 progress에 따라서 컨트롤을 해주어야 한다.

LottieAnimation(
    modifier = Modifier
        .size(100.dp)
        .clickable {
            isPlaying = true
        },
    composition = composition,
    progress = {
        if (progress == 1.0f) {
            isPlaying = false
        }
        progress
    },
)

 

그래서 필자는 다음과 같이 선언을 해두었다.

progress가 1.0f가 됐다는 것은 100% 모두 진행이 완료됐다는 것이므로, isPlaying을 false로 변경하고, 해당 Lottie를 클릭했을 때 다시 isPlaying을 true로 변경하여 다시 실행되도록 하였다.

 

하지만, 생각한 것처럼 동작하지 않았다.

로그를 찍어서 확인해 보니, isPlaying은 false로 변경되기는 하지만 progress가 1.0f에서 0.0f으로 바로 초기화되지 않았다.

1.0f인 상태에서 클릭하면 isPlaying = true로 인해 다시 재생 상태로 변하고, progress = { ... } 부분에서  0.0f로 초기화되는 것을 볼 수 있었다.

즉, 두 번 클릭해야 다시 애니메이션이 재생되는 것이다.

 

해당 이슈에 대한 다른 변수나 조건 같은 것들은 찾을 수 없으니, progress와 clickable 두 범위 내에서 한 번 클릭 시 두 번 클릭되는 것과 동일한 효과를 줘야 한다.

위의 로직에서 로그를 찍어보면 순서는 다음과 같다.

  1. isPlaying이 true로 변함
  2. progress가 0.0부터 1.0까지 증가함
  3. progress가 1.0이 되면 애니메이션이 멈춤.
  4. 클릭을 하면 isPlaying이 true로 변하지만 progress는 1.0f에서 초기화가 되지 않아 progress == 1.0f를 만족하여 isPlaying을 false로 바꿈.
  5. false로 바뀌면서 progress가 0.0f로 초기화됨.
  6. 다시 클릭하면 1번으로 돌아가 반복됨.

그렇다면, 클릭을 했을 때, progress가 1.0f이 0.0f으로 초기화가 되니 이때 한번 더 isPlaying을 true로 만들어주면 된다. 

따라서 필자는 다음과 같이 구현하였다.

LottieAnimation(
    modifier = Modifier
        .size(100.dp)
        .clickable {
            if (progress == 1.0f) {
                isReplay = true
            }
            isPlaying = true
        },
    composition = composition,
    progress = {
        if (progress == 1.0f) {
            isPlaying = false
        }

        // lottie가 끝났을 때, 한번 더 클릭하면 progress가 1.0에서 0.0으로 초기화 되는 동작을 수행한다.
        if (isReplay) {
            isPlaying = true
            isReplay = false
        }
        progress
    },
)

 

어차피 isPlaying이 true가 되었을 때 progress가 1.0f여서 그렇지 다시 한번 재생을 시도하는 것은 맞다.

그렇기 때문에, isReplay라는 변수를 하나 추가해 주어서 이것을 사용하도록 하였다.

1.0f 일 때 isReplay가 true 값을 가지게 된다면, 애니메이션이 재생되었다가 끝났으며 다시 재생을 위해 클릭한 것이라는 플래그로 사용할 수 있기 때문이다.

 

따라서, progress에서 isReplay가 true라면 다시 isPlaying을 true로 변경시켜 주고 isReplay를 false로 변경시켜 반복 재생을 방지한다.

 

이렇게 구현하고 확인을 해보면, 최초 한번 재생 후 클릭할 때마다 다시 재생이 되는 것을 확인할 수 있을 것이다.


이것으로 간단하게 gif와 lottie로 애니메이션을 재생하는 방법에 대하여 알아보았다.

gif로도 클릭할 때마다 재생하는 방식을 구현해보려고 하였으나, Coil을 사용해서는 다시 재생하는 것이 무슨짓을 해도 되지 않아서 2개의 painter 객체를 사용하여 재생하도록 구현은 해두었다.

하지만 클릭할 때 마다 깜빡이는 등 이슈가 있어서 본 글에는 추가하지 않았지만, github에는 올려두었으니 필요하면 참고하길 바란다.

 

기존에 Lottie를 적용할 때는 Loading에 보여주는 용도로만 사용하고 있어서 무한히 반복하도록만 사용했었는데, 이번에는 Loading 할 때가 아닌 다른 케이스에서 Lottie를 사용하니 생각보다 커스텀할 수 있는 것들이 많다는 것을 알게 되었다.

 

겸사겸사 gif와 비교해서 사용을 해보았는데, 디자이너가 뽑아주는 gif 파일과 json 파일의 크기 차이부터가 많이 나기도 했고, 로딩 속도 측면에서도 상당한 차이를 보였기 때문에 역시 Lottie를 사용하는 것이 모바일에서 애니메이션을 구현할 때는 최적이 아닐까 생각한다.

 

해당 게시글에 사용한 예제는 Github에 올려두었다.

https://github.com/HeeGyeong/ComposeSample

 

GitHub - HeeGyeong/ComposeSample: This project provides various examples needed to actually use Jetpack Compose.

This project provides various examples needed to actually use Jetpack Compose. - HeeGyeong/ComposeSample

github.com

728x90