안드로이드 프레임워크·시스템 학습 노트 목차

Compose 애니메이션 — animate*AsState·Transition·제스처 연동

왜 선언형이 애니메이션과 잘 맞나

옛 명령형 애니메이션은 *"0.3초 동안 alpha를 0→1로, 그 사이 매 프레임 값을 직접 갱신해라"*처럼 과정을 일일이 제어했다 — 코드가 길고, 중간에 상태가 바뀌면 꼬였다. Compose의 선언형 모델은 발상을 뒤집는다 — *"지금 목표는 펼쳐진 상태"*라고 목표만 선언하면, Compose가 현재 값에서 목표 값까지 부드럽게 보간해 준다. 우리는 *어디로 갈지(target)*만 말하고, *어떻게 갈지(매 프레임 보간)*는 프레임워크가 맡는다. 이건 상태가 바뀌면 UI가 따라온다는 Compose의 UI = f(state) 철학([03])의 자연스러운 연장이다 — *애니메이션도 결국 "상태에서 목표 상태로의 부드러운 전환"*일 뿐이다. Compose는 간단한 값 하나부터 좌표 기반 제스처까지 계층화된 API를 준다. (출처: Android — Animations in Compose.)

animate*AsState — 값 하나를 부드럽게

가장 단순한 API — 어떤 상태에서 계산되는 값을 animate*AsState로 감싸면, 그 값이 바뀔 때 목표까지 자동 보간된다.

val expanded by remember { mutableStateOf(false) }
val size by animateDpAsState(if (expanded) 200.dp else 100.dp, label = "size")
val color by animateColorAsState(if (expanded) Color.Red else Color.Gray, label = "color")
Box(Modifier.size(size).background(color).clickable { expanded = !expanded })
// expanded를 토글하면 size·color가 '목표값으로' 부드럽게 이동(중간 프레임은 Compose가 채움)

expanded를 바꾸는 순간 우리는 새 목표(200.dp/Red)만 줬는데, size와 color가 알아서 현재 값에서 목표까지 흘러간다. animateFloatAsState·animateDpAsState·animateColorAsState 등 타입별로 있고, animationSpec(tween 시간 기반·spring 물리 기반·keyframes 구간 지정)으로 곡선·속도를 조절한다(spring은 자연스러운 튕김을 준다).

AnimatedVisibility·Crossfade·animateContentSize

값 하나가 아니라 등장/퇴장·화면 전환·크기 변화는 전용 API가 있다.

AnimatedVisibility(visible = show) { Card { Text("나타남/사라짐") } }   // enter/exit 전환
Crossfade(targetState = screen) { s -> when(s) { ... } }              // 화면 교차 페이드
Box(Modifier.animateContentSize()) { /* 내용이 늘면 크기 변화를 부드럽게 */ }
  • AnimatedVisibility — 컴포저블의 등장/퇴장을 애니메이션(fade·slide·expand를 조합). 그냥 if (show)로 넣고 빼면 툭 나타나지만, 이걸 쓰면 부드럽게 들고 난다.
  • Crossfade상태가 바뀔 때 옛 화면과 새 화면을 교차 페이드한다.
  • animateContentSize콘텐츠 크기가 바뀔 때(텍스트가 펼쳐지는 등) 크기 변화를 자동 애니메이션.

Transition — 여러 값을 한 상태로 묶어 조율

여러 속성이 한 상태 변화에 함께 움직여야 하면(선택되면 그림자도·여백도 같이) — **updateTransition**으로 하나의 전환에 여러 값을 묶는다. 따로따로 animate*AsState를 쓰는 것보다 동기화가 보장된다.

val transition = updateTransition(selected, label = "card")
val elevation by transition.animateDp(label = "elev") { if (it) 8.dp else 2.dp }
val padding   by transition.animateDp(label = "pad")  { if (it) 16.dp else 8.dp }
// selected 하나가 바뀌면 elevation·padding이 '함께' 애니메이션
다이어그램 로딩 중…

Animatable·무한 애니메이션·리스트

상황에 맞게 더 낮거나 특수한 API를 고른다.

  • Animatable저수준 수동 제어. 선언형 자동 보간으로 안 되는 *제스처 연동(드래그하는 손가락을 따라가기)*에, 코루틴으로 animateTo/snapTo로 값을 직접 움직인다.
  • rememberInfiniteTransition끝없이 반복하는 애니메이션(로딩 스피너·맥동하는 점).
  • Modifier.animateItem()LazyColumn 항목의 추가/삭제/이동을 애니메이션한다([15], key 필수 — key가 있어야 어느 항목이 움직였는지 안다).

어떤 API를 고를까 — *값 하나면 animate*AsState, 등장/퇴장이면 AnimatedVisibility, 여러 값 동기화면 updateTransition, 제스처 연동이면 Animatable, 반복이면 InfiniteTransition*이다.

정리 — 목표를 선언하면 움직임이 따라온다

Compose 애니메이션의 핵심 — 목표 상태를 선언하면 Compose가 현재→목표를 보간(UI=f(state)의 연장), animate*AsState(값 하나)·AnimatedVisibility/Crossfade(등장·전환)·updateTransition(여러 값 동기)·Animatable(제스처 저수준)·InfiniteTransition(반복)·animateItem(리스트), animationSpec(tween/spring)으로 곡선. 과정이 아니라 목적지를 선언하는 게 선언형 애니메이션의 사고법이다.

한 줄 요약 — 선언형=목표만 선언→Compose가 현재→목표 보간(UI=f(state) 연장). animate*AsState(값 하나·spec=tween/spring), AnimatedVisibility/Crossfade(등장·전환), animateContentSize(크기), updateTransition(여러 값 동기 조율), Animatable(저수준·제스처), rememberInfiniteTransition(반복), Modifier.animateItem(리스트·key 필수).

(출처: Android — Animation in Compose / Choose an animation API.)

Compose 리스트 — LazyColumn·key·성능Compose 성능·안정성 — recomposition·@Stable·skippable