목표 값을 선언하면 런타임이 그리로 부드럽게 이동시킨다.
val color by animateColorAsState(
targetValue = if (isSelected) Blue else Gray,
animationSpec = tween(300),
label = "selection"
)
Box(Modifier.background(color))
명령형이라면
- ValueAnimator 를 만들고 · 리스너를 붙이고 · 중간에 값이 바뀌면 취소하고 다시 시작
선언형이라면
- "선택됐으면 파랑" 만 적는다
- 중간에 값이 바뀌면 현재 위치에서 새 목표로 자연스럽게 이어진다 (인터럽트 처리가 공짜)
종류
animateFloatAsState · animateDpAsState · animateColorAsState
animateIntOffsetAsState · animateSizeAsState
// 나타남/사라짐
AnimatedVisibility(visible = expanded, enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()) { Details() }
// 내용 교체
Crossfade(targetState = screen) { s -> when (s) { ... } }
// 크기 변화
Column(Modifier.animateContentSize()) { if (expanded) LongText() }
여러 값을 한 상태로 묶는다
val transition = updateTransition(targetState = isExpanded, label = "card")
val height by transition.animateDp(label = "h") { if (it) 200.dp else 80.dp }
val alpha by transition.animateFloat(label = "a") { if (it) 1f else 0.6f }
val corner by transition.animateDp(label = "c") { if (it) 0.dp else 16.dp }
개별 animateAsState 를 세 개 쓰면 각각 따로 돈다 (미세하게 어긋난다) Transition 은 하나의 상태 전환으로 묶어 조율한다
- Android Studio 애니메이션 프리뷰에서도 함께 보인다 (label 이 그래서 필요하다)
spec 고르기
tween(300, easing = FastOutSlowInEasing) 시간 기반. 예측 가능하다
- spring(dampingRatio = 0.6f) — 물리 기반. 제스처 뒤 이어질 때 자연스럽다
- keyframes { } — 중간 지점을 지정한다 repeatable / infiniteRepeatable 반복
제스처로 시작한 움직임은 spring 이 낫다 — 속도를 이어받는다
무한 애니메이션
val infinite = rememberInfiniteTransition(label = "shimmer")
val x by infinite.animateFloat(
0f, 1000f,
infiniteRepeatable(tween(1200), RepeatMode.Restart), label = "x"
)
접근성
// 사용자가 '애니메이션 제거' 를 켰다면 존중한다
val reduce = LocalAccessibilityManager.current?.let { ... }
면접 함정
- ❌ "애니메이션은 리컴포지션을 유발하지 않는다" → 값을 컴포지션에서 읽으면 프레임마다 돈다.
graphicsLayer/offset {}람다로 읽기를 늦춘다. - ❌ "AnimatedVisibility는 그냥 alpha 애니메이션" → 사라진 뒤 컴포지션에서도 제거된다. 상태가 함께 사라진다.