본문 바로가기
안드로이드

[Android] Compose를 이용한 애니메이션

by algosketch 2023. 8. 19.

※ 완성되지 않은 코드입니다. 참고만 해주세요.

애니메이션을 넣게 된 계기

넥스터즈에서 비교적 작은 규모의 프로젝트를 진행하게 되어서 디자이너 분께 애니메이션 같은 인터렉션을 넣어도 괜찮다고 말씀드렸다. 그 결과 구현하게 된 UI는 위와 같다. 애니메이션은 대략 위와 같고 디테일은 아직 잡혀있지 않은 상태이다. 코드도 아직 정리되어 있지는 않다.

돌아가는 형태의 UI가 있고, 토글(?) 버튼을 누르면 펼쳐져서 한 번에 많은 폴더를 확인할 수 있다. 토글버튼을 눌렀을 때 확장되는 애니메이션을 제외하고는 사실 그래픽 요소이다.

 

단계별로 구현하자

아무래도 한 번에 구현할 수 있는 UI는 아니어서 단계별로 나누어 구현했다. 위 사진은 노션에 대충 휘갈겨 놓은 내용으로 나만 알아볼 수 있다. 막상 구현해 보니 어려운 건 없었다. 한 가지 어려운 게 있다면 원래 디자인은 원형큐 형태로 나왔는데, 지금은 끝이 존재하는 형태이다. 구현할 때 정해놓은 대략적인 단계는 다음과 같다.

  1. 한 번에 세 개의 아이템만 보여야 한다.
  2. 나타날 때 효과
  3. 사라질 때 효과

 

 

세 개의 아이템만 보여주기

코드는 위와 같은데, 아직 정리되지 않아 주석이 달려 있는 모습을 볼 수 있다. 원래 처음 접근은 화면에 보이지 않는 아이템은 if로 분기처리 하거나 AnimatedVisibility(visible = index < 3)과 같이 작성하는 것이었다. 말로 표현하긴 어려운데 전혀 생각지도 못 한 UI(아예 아무것도 출력이 안 되거나, zIndex가 더 높음에도 불구하고 UI가 잘린다)가 출력된다. 아마 Compose의 버그가 아닌가 추측해 본다. 내 기준에선 말이 안 되는 동작이었음 그래서 대안으로 불투명도를 조절하는 방법을 선택했다. (45~48, 68~70 라인)

index와 offset 값 추적

스크롤에 따라 카드가 나타나고 사라지는 것을 구현해야 한다. 따라서 스크롤 위치를 정밀하게 관찰하기 위해 firstVisibleItemIndex와 firstVisibleItemScrollOffset의 상태를 받아서 사용한다. firstVisibleItemIndex는 화면 상에 보이는 첫 번째 아이템의 index를 추적한다. 원래는 200dp(카드의 높이 값)마다 하나씩 증가해야 하는데 spacedBy로 -135dp를 주어서 65dp 스크롤할 때마다 증가하게 된다. 즉, 0번 아이템이 보여도 index값이 증가한다. 하지만 이게 원하는 동작이다. firstVisibleItemScrollOffset은 스크롤할 때마다 offset이 증가하는데 index값이 바뀌는 순간에는 0으로 초기화 된다. 이 offset 값은 dp가 아닌 pixel 단위이므로 변환이 필요하다.

나타나는 카드

위 코드에서 57~65 라인에 해당한다.

val itemHeight = folderHeight * pixelRatio
val marginHeight = 65 * pixelRatio
val progress = offset / marginHeight

나타나는 카드는 원래 크기의 0.5부터 시작하여 1까지 offset 값에 따라 서서히 증가하고, 원래 크기에 대한 상대 크기는 graphicsLayer의 scaleX와 scaleY 값을 조절하여 쉽게 변경할 수 있다. scale은 progress가 0일 때 0.5가 되어야 하고, progress가 1일 때 1이 되어야 한다. 아래와 같이 간단하게 구현할 수 있다.

if (index == currentIndex + 3) {
    // 도착지점에 미리 갖다 놓고, offset을 통해 스크롤이 내려가도 고정시키기
    translationY = marginHeight.toFloat() - offset

    val scale = (0.5 * progress).toFloat() + minimumScale
    scaleX = scale
    scaleY = scale

    alpha = progress.toFloat()
}

다만, scale 값은 스크롤에 따라 내려오면서 커지는 게 아닌 제자리에서 커지도록 구현해야 한다. 스크롤에 따라 offset 값이 증가하는데, 증가한 offset만큼 다시 빼 주기만 하면 스크롤 해도 아이템이 움직이지 않는다. (화면에서 y축 좌표는 수학에서와 반대 방향이다.)

사라지는 카드

if(index == currentIndex) {
    alpha = 1 - progress.toFloat()
}

스크롤에 따라 불투명도만 1에서 0까지 조절하면 된다. 다만 투명도만 조절하면 아이템이 투명한 상태로 클릭이 가능하기 때문에 완전히 사라진 이후에는 다음과 같이 화면 밖으로 이동시키는 추가 작업을 했다.

if (index < currentIndex) {
    alpha = 0f
    translationY = itemHeight.toFloat()
}

코드가 약간 마음에 들지 않는다. index < currentIndex 일 때는 아예 렌더링을 안 하면 되지 않냐고 생각할 수 있는데, 렌더링하지 않으면 다음 아이템이 없는 상태에서는 스크롤이 불가능하기 때문에 예측할 수 없는 문제가 생긴다. 그래도 위 코드가 좋아보이지는 않아서 대안을 생각 중이다.

 

등장! 애니메이션

펼쳐진 카드 보기로 전환할 때 애니메이션은 animateDpAsState를 통해 매우 간단히 구현할 수 있다. 지금 보니까 할당 받은 변수명이 좀 이상하다.

 

참고자료

https://github.com/Nexters/talkbbokki-android

 

GitHub - Nexters/talkbbokki-android: 톡뽀끼와 함께라면 쉬워지는 스몰토크

톡뽀끼와 함께라면 쉬워지는 스몰토크. Contribute to Nexters/talkbbokki-android development by creating an account on GitHub.

github.com