Jetpack Compose에서는 앱 UI에서 다양한 애니메이션을 쉽게 구현하도록 지원하는 강력하고 확장 가능한 API를 제공합니다. 이 문서에서는 API를 사용하는 방법 및 애니메이션 시나리오에 따라 사용해야 하는 API를 설명합니다.
개요
최신 모바일 앱에서 원활하고 이해하기 쉬운 사용자 환경을 실현하기 위해서는 애니메이션이 필수적입니다. 많은 Jetpack Compose Animation API는 레이아웃 및 기타 UI 요소와 마찬가지로 구성 가능한 함수로 사용할 수 있으며 Kotlin 코루틴 정지 함수로 빌드된 하위 수준 API의 지원을 받습니다. 이 가이드에서는 여러 실제 시나리오에서 유용한 상위 수준 API부터 시작하여 더욱 세밀하게 관리하고 맞춤설정할 수 있는 하위 수준 API를 설명합니다.
아래 다이어그램은 애니메이션을 구현하는 데 사용할 API를 결정하는 데 도움이 됩니다.
- 레이아웃에서 콘텐츠 변경사항을 애니메이션 처리하는 경우:
- 나타남과 사라짐을 애니메이션으로 처리하는 경우:
AnimatedVisibility
를 사용합니다.
- 상태에 따라 콘텐츠 교체:
- 콘텐츠를 크로스페이드하는 경우:
Crossfade
를 사용합니다.
- 그렇지 않은 경우
AnimatedContent
를 사용합니다.
- 콘텐츠를 크로스페이드하는 경우:
- 그렇지 않은 경우
Modifier.animateContentSize
를 사용합니다.
- 나타남과 사라짐을 애니메이션으로 처리하는 경우:
- 애니메이션이 상태 기반인 경우:
- 컴포지션 시 애니메이션이 발생하는 경우:
- 애니메이션이 무제한인 경우:
rememberInfiniteTransition
을 사용합니다.
- 여러 값을 동시에 애니메이션 처리하는 경우:
updateTransition
을 사용합니다.
- 그렇지 않은 경우
animate*AsState
를 사용합니다.
- 애니메이션이 무제한인 경우:
- 컴포지션 시 애니메이션이 발생하는 경우:
- 애니메이션 시간을 세밀하게 관리하려는 경우:
TargetBasedAnimation
또는DecayAnimation
과 같은Animation
을 사용합니다.
- 애니메이션이 유일한 정보 소스인 경우
Animatable
을 사용합니다.
- 그 외의 경우
AnimationState
나animate
를 사용합니다.
상위 수준 Animation API
Compose는 많은 앱에서 사용되는 여러 일반적인 애니메이션 패턴을 위한 상위 수준 Animation API를 제공합니다. 이 API는 Material Design Motion의 권장사항에 맞게 조정되었습니다.
AnimatedVisibility
AnimatedVisibility
컴포저블은 콘텐츠 표시와 사라짐을 애니메이션 처리합니다.
var editable by remember { mutableStateOf(true) }
AnimatedVisibility(visible = editable) {
Text(text = "Edit")
}
기본적으로는 콘텐츠는 페이드인 및 확장 방식으로 표시되고 페이드아웃 및 축소 방식으로 사라집니다. EnterTransition
및 ExitTransition
을 지정하여 전환을 맞춤설정할 수 있습니다.
var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
AnimatedVisibility(
visible = visible,
enter = slideInVertically {
// Slide in from 40 dp from the top.
with(density) { -40.dp.roundToPx() }
} + expandVertically(
// Expand from the top.
expandFrom = Alignment.Top
) + fadeIn(
// Fade in with the initial alpha of 0.3f.
initialAlpha = 0.3f
),
exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
Text("Hello", Modifier.fillMaxWidth().height(200.dp))
}
위의 예에서 볼 수 있듯이 +
연산자를 사용하여 여러 EnterTransition
또는 ExitTransition
객체를 결합할 수 있으며 각 객체에 선택적 매개변수를 사용하여 동작을 맞춤설정할 수 있습니다. 자세한 내용은 참고 자료를 확인하세요.
EnterTransition
및 ExitTransition
예시
AnimatedVisibility
는 MutableTransitionState
를 사용하는 변형도 제공합니다. 이를 통해 AnimatedVisibility
가 컴포지션 트리에 추가되는 즉시 애니메이션을 트리거할 수 있습니다. 애니메이션 상태를 관찰하는 데도 유용합니다.
// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
val state = remember {
MutableTransitionState(false).apply {
// Start the animation immediately.
targetState = true
}
}
Column {
AnimatedVisibility(visibleState = state) {
Text(text = "Hello, world!")
}
// Use the MutableTransitionState to know the current animation state
// of the AnimatedVisibility.
Text(
text = when {
state.isIdle && state.currentState -> "Visible"
!state.isIdle && state.currentState -> "Disappearing"
state.isIdle && !state.currentState -> "Invisible"
else -> "Appearing"
}
)
}
하위 요소의 들어가기와 나가기 애니메이션
AnimatedVisibility
내 콘텐츠(직접 또는 간접 하위 요소)는 animateEnterExit
수정자를 사용하여 각각의 다른 애니메이션 동작을 지정할 수 있습니다. 이러한 각 하위 요소의 시각적 효과는 AnimatedVisibility
컴포저블에 지정된 애니메이션과 하위 요소의 자체 들어가기 및 나가기 애니메이션의 조합입니다.
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) {
// Fade in/out the background and the foreground.
Box(Modifier.fillMaxSize().background(Color.DarkGray)) {
Box(
Modifier
.align(Alignment.Center)
.animateEnterExit(
// Slide in/out the inner box.
enter = slideInVertically(),
exit = slideOutVertically()
)
.sizeIn(minWidth = 256.dp, minHeight = 64.dp)
.background(Color.Red)
) {
// Content of the notification…
}
}
}
하위 요소가 각각 animateEnterExit
로 고유한 자체 애니메이션을 보유하도록, AnimatedVisibility
가 애니메이션을 전혀 적용하지 않도록 하려는 경우가 있습니다. 이렇게 하려면 AnimatedVisibility
컴포저블에 EnterTransition.None
과 ExitTransition.None
을 지정하세요.
맞춤 애니메이션 추가
기본 제공 들어가기 및 나가기 애니메이션 외에 맞춤 애니메이션 효과를 추가하려면 AnimatedVisibility
콘텐츠 람다 내의 transition
속성을 통해 기본 Transition
인스턴스에 액세스합니다. Transition 인스턴스에 추가된 애니메이션 상태는 AnimatedVisibility
의 들어가기 및 나가기 애니메이션과 동시에 실행됩니다. AnimatedVisibility
는 콘텐츠를 삭제하기 전에 Transition
의 모든 애니메이션이 완료될 때까지 기다립니다.
Transition
과는 별개로 만들어진 나가기 애니메이션(예: animate*AsState
사용)의 경우 AnimatedVisibility
는 이를 고려할 수 없으므로 완료되기 전에 콘텐츠 컴포저블을 삭제할 수 있습니다.
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) { // this: AnimatedVisibilityScope
// Use AnimatedVisibilityScope#transition to add a custom animation
// to the AnimatedVisibility.
val background by transition.animateColor { state ->
if (state == EnterExitState.Visible) Color.Blue else Color.Gray
}
Box(modifier = Modifier.size(128.dp).background(background))
}
Transition
에 관한 자세한 내용은 updateTransition을 참고하세요.
animate*AsState
animate*AsState
함수는 Compose에서 단일 값을 애니메이션 처리하는 가장 간단한 Animation API입니다. 최종 값(또는 타겟 값)만 제공하면 API가 현재 값에서 지정된 값으로 애니메이션을 시작합니다.
다음은 이 API를 사용하여 알파를 애니메이션 처리하는 예입니다. 타겟 값을 animateFloatAsState
에 래핑하기만 하면 알파 값이 제공된 값(이 경우 1f
또는 0.5f
) 사이의 애니메이션 값이 됩니다.
val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
Box(
Modifier.fillMaxSize()
.graphicsLayer(alpha = alpha)
.background(Color.Red)
)
애니메이션 클래스의 인스턴스를 생성하거나 중단을 처리할 필요가 없습니다. 내부적으로 애니메이션 객체(즉 Animatable
인스턴스)가 생성되고 첫 번째 타겟 값을 초깃값으로 하여 호출 사이트에 저장됩니다. 이후에는 이 컴포저블에 다른 타겟 값을 제공할 때마다 이 값을 향해 애니메이션이 자동으로 시작됩니다. 이미 실행 중인 애니메이션이 있는 경우 애니메이션이 현재 값(및 속도)에서 시작하고 타겟 값을 향해 애니메이션 처리됩니다. 애니메이션 처리 중에 이 컴포저블은 재구성되고 프레임마다 업데이트된 애니메이션 값을 반환합니다.
Compose는 Float
, Color
, Dp
, Size
, Offset
, Rect
, Int
, IntOffset
, IntSize
에 사용할 수 있는 animate*AsState
함수를 즉시 제공합니다. 일반 유형을 취하는 animateValueAsState
에 TwoWayConverter
를 제공하여 다른 데이터 유형의 지원 기능을 쉽게 추가할 수 있습니다.
AnimationSpec
을 제공하여 애니메이션 사양을 맞춤설정할 수 있습니다.
자세한 내용은 AnimationSpec을 참고하세요.
AnimatedContent(실험용)
AnimatedContent
컴포저블은 타겟 상태에 따라 콘텐츠가 변경될 때 콘텐츠에 애니메이션을 적용합니다.
Row {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Add")
}
AnimatedContent(targetState = count) { targetCount ->
// Make sure to use `targetCount`, not `count`.
Text(text = "Count: $targetCount")
}
}
항상 람다 매개변수를 사용하고 이를 콘텐츠에 반영해야 합니다. API는 이 값을 키로 사용하여 현재 표시되는 콘텐츠를 식별합니다.
기본적으로 초기 콘텐츠는 페이드 아웃되고 타겟 콘텐츠가 페이드 인됩니다. 이 동작을 페이드 스루라고 합니다. transitionSpec
매개변수에 ContentTransform
객체를 지정하여 이 애니메이션 동작을 맞춤설정할 수 있습니다. with
중위 함수로 EnterTransition
을 ExitTransition
과 결합하여 ContentTransform
을 만들 수 있습니다. SizeTransform
을 using
중위 함수로 연결하여 ContentTransform
에 적용할 수 있습니다.
AnimatedContent(
targetState = count,
transitionSpec = {
// Compare the incoming number with the previous number.
if (targetState > initialState) {
// If the target number is larger, it slides up and fades in
// while the initial (smaller) number slides up and fades out.
slideInVertically { height -> height } + fadeIn() with
slideOutVertically { height -> -height } + fadeOut()
} else {
// If the target number is smaller, it slides down and fades in
// while the initial number slides down and fades out.
slideInVertically { height -> -height } + fadeIn() with
slideOutVertically { height -> height } + fadeOut()
}.using(
// Disable clipping since the faded slide-in/out should
// be displayed out of bounds.
SizeTransform(clip = false)
)
}
) { targetCount ->
Text(text = "$targetCount")
}
EnterTransition
은 타겟 콘텐츠가 표시되는 방식을 정의하고 ExitTransition
은 초기 콘텐츠가 사라지는 방식을 정의합니다. AnimatedVisibility
에서 사용할 수 있는 모든 EnterTransition
및 ExitTransition
함수 외에도 AnimatedContent
는 slideIntoContainer
와 slideOutOfContainer
를 제공합니다.
이는 초기 콘텐츠 크기와 AnimatedContent
콘텐츠의 타겟 콘텐츠에 따라 슬라이드 거리를 계산하는 slideInHorizontally/Vertically
와 slideOutHorizontally/Vertically
의 편리한 대안입니다.
SizeTransform
은 초기 콘텐츠와 타겟 콘텐츠 사이에 크기가 애니메이션되는 방식을 정의합니다. 애니메이션을 만들 때 초기 크기와 타겟 크기에 모두 액세스할 수 있습니다. SizeTransform
은 애니메이션 중에 콘텐츠를 구성요소 크기로 잘라야 하는지도 제어합니다.
var expanded by remember { mutableStateOf(false) }
Surface(
color = MaterialTheme.colors.primary,
onClick = { expanded = !expanded }
) {
AnimatedContent(
targetState = expanded,
transitionSpec = {
fadeIn(animationSpec = tween(150, 150)) with
fadeOut(animationSpec = tween(150)) using
SizeTransform { initialSize, targetSize ->
if (targetState) {
keyframes {
// Expand horizontally first.
IntSize(targetSize.width, initialSize.height) at 150
durationMillis = 300
}
} else {
keyframes {
// Shrink vertically first.
IntSize(initialSize.width, targetSize.height) at 150
durationMillis = 300
}
}
}
}
) { targetExpanded ->
if (targetExpanded) {
Expanded()
} else {
ContentIcon()
}
}
}
하위 요소의 들어가기/나가기 애니메이션
AnimatedVisibility
와 마찬가지로 animateEnterExit
수정자는 AnimatedContent
콘텐츠 람다 내에서 사용할 수 있습니다. 이를 사용하여 EnterAnimation
과 ExitAnimation
을 직접 하위 요소 또는 간접 하위 요소 각각에 별도로 적용합니다.
맞춤 애니메이션 추가
AnimatedVisibility
와 마찬가지로 transition
필드는 AnimatedContent
콘텐츠 람다 내에서 사용할 수 있습니다. AnimatedContent
전환과 동시에 실행되는 맞춤 애니메이션 효과를 만드는 데 사용합니다. 자세한 내용은 updateTransition을 참고하세요.
animateContentSize
animateContentSize
수정자는 크기를 변경하는 애니메이션을 표시합니다.
var message by remember { mutableStateOf("Hello") }
Box(
modifier = Modifier.background(Color.Blue).animateContentSize()
) {
Text(text = message)
}
Crossfade
Crossfade
는 크로스페이드 애니메이션을 사용하여 두 레이아웃 사이의 전환을 애니메이션 처리합니다. current
매개변수로 전달된 값을 전환하면 콘텐츠가 크로스페이드 애니메이션을 사용하여 전환됩니다.
var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage) { screen ->
when (screen) {
"A" -> Text("Page A")
"B" -> Text("Page B")
}
}
updateTransition
Transition
은 하나 이상의 애니메이션을 하위 요소로 관리하며 여러 상태 간에 동시에 실행합니다.
상태는 모든 데이터 유형이 될 수 있습니다. 대부분의 경우 다음 예와 같이 맞춤 enum
유형을 사용하여 유형 안전성을 보장할 수 있습니다.
enum class BoxState {
Collapsed,
Expanded
}
updateTransition
은 Transition
의 인스턴스를 만들고 저장하며 상태를 업데이트합니다.
var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState)
그런 다음 animate*
확장 함수 중 하나를 사용하여 이 전환에서 하위 애니메이션을 정의할 수 있습니다. 각 상태의 타겟 값을 지정합니다.
이 animate*
함수는 전환 상태가 updateTransition
으로 업데이트될 때 애니메이션의 모든 프레임에서 업데이트되는 애니메이션 값을 반환합니다.
val rect by transition.animateRect { state ->
when (state) {
BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
}
}
val borderWidth by transition.animateDp { state ->
when (state) {
BoxState.Collapsed -> 1.dp
BoxState.Expanded -> 0.dp
}
}
transitionSpec
매개변수를 전달하여 전환 상태 변경사항의 조합별로 다른 AnimationSpec
을 지정할 수도 있습니다. 자세한 내용은 AnimationSpec을 참고하세요.
val color by transition.animateColor(
transitionSpec = {
when {
BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
spring(stiffness = 50f)
else ->
tween(durationMillis = 500)
}
}
) { state ->
when (state) {
BoxState.Collapsed -> MaterialTheme.colors.primary
BoxState.Expanded -> MaterialTheme.colors.background
}
}
전환이 타겟 상태에 도달하면 Transition.currentState
가 Transition.targetState
와 동일하게 됩니다. 이 기능은 전환이 완료되었는지 여부를 나타내는 신호로 사용할 수 있습니다.
초기 상태를 첫 번째 타겟 상태와 다르게 설정해야 하는 경우도 있습니다. updateTransition
을 MutableTransitionState
와 함께 사용하여 이 목표를 달성할 수 있습니다. 예를 들어 이렇게 하면 코드가 컴포지션을 시작하는 즉시 애니메이션을 시작할 수 있습니다.
// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = updateTransition(currentState)
// ...
여러 구성 가능한 함수가 관련된 좀 더 복잡한 전환의 경우 createChildTransition
을 사용하여 하위 전환을 만들 수 있습니다. 이 기법은 복잡한 컴포저블에서 여러 하위 구성요소 간에 문제를 구분하는 데 유용합니다. 상위 전환은 하위 전환의 모든 애니메이션 값을 인식합니다.
enum class DialerState { DialerMinimized, NumberPad }
@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
// `isVisibleTransition` spares the need for the content to know
// about other DialerStates. Instead, the content can focus on
// animating the state change between visible and not visible.
}
@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
// `isVisibleTransition` spares the need for the content to know
// about other DialerStates. Instead, the content can focus on
// animating the state change between visible and not visible.
}
@Composable
fun Dialer(dialerState: DialerState) {
val transition = updateTransition(dialerState)
Box {
// Creates separate child transitions of Boolean type for NumberPad
// and DialerButton for any content animation between visible and
// not visible
NumberPad(
transition.createChildTransition {
it == DialerState.NumberPad
}
)
DialerButton(
transition.createChildTransition {
it == DialerState.DialerMinimized
}
)
}
}
AnimatedVisibility 및 AnimatedContent와 함께 전환 사용
AnimatedVisibility
와 AnimatedContent
는 Transition
의 확장 함수로 사용할 수 있습니다. Transition.AnimatedVisibility
및 Transition.AnimatedContent
의 targetState
는 Transition
에서 파생되며 Transition
의 targetState
가 변경될 때 필요에 따라 들어가기/나가기 전환을 트리거합니다. 이러한 확장 함수를 사용하면 AnimatedVisibility
/AnimatedContent
내부에 있는 모든 들어가기/나가기/sizeTransform 애니메이션을 Transition
으로 호이스팅할 수 있습니다.
확장 함수를 통해 AnimatedVisibility
/AnimatedContent
의 상태 변경을 외부에서 관찰할 수 있습니다. 불리언 visible
매개변수 대신 이 AnimatedVisibility
버전은 상위 전환의 타겟 상태를 불리언으로 변환하는 람다를 사용합니다.
자세한 내용은 AnimatedVisibility와 AnimatedContent를 참고하세요.
var selected by remember { mutableStateOf(false) }
// Animates changes when `selected` is changed.
val transition = updateTransition(selected)
val borderColor by transition.animateColor { isSelected ->
if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp { isSelected ->
if (isSelected) 10.dp else 2.dp
}
Surface(
onClick = { selected = !selected },
shape = RoundedCornerShape(8.dp),
border = BorderStroke(2.dp, borderColor),
elevation = elevation
) {
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
Text(text = "Hello, world!")
// AnimatedVisibility as a part of the transition.
transition.AnimatedVisibility(
visible = { targetSelected -> targetSelected },
enter = expandVertically(),
exit = shrinkVertically()
) {
Text(text = "It is fine today.")
}
// AnimatedContent as a part of the transition.
transition.AnimatedContent { targetState ->
if (targetState) {
Text(text = "Selected")
} else {
Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone")
}
}
}
}
전환을 캡슐화하고 재사용 가능하도록 설정
간단한 사용 사례에서는 UI와 동일한 컴포저블에서 전환 애니메이션을 정의하는 것이 완벽하게 유효한 방법입니다. 그러나 여러 애니메이션 값으로 구성된 복잡한 구성요소를 사용하는 경우 애니메이션 구현을 컴포저블 UI와 분리해야 할 수도 있습니다.
이렇게 하려면 모든 애니메이션 값을 포함하는 클래스와 이 클래스의 인스턴스를 반환하는 '업데이트' 함수를 만들면 됩니다. 전환 구현은 별도의 새 함수로 추출할 수 있습니다. 이 패턴은 애니메이션 로직을 중앙집중식으로 처리하거나 복잡한 애니메이션을 재사용 가능하도록 설정해야 하는 경우에 유용합니다.
enum class BoxState { Collapsed, Expanded }
@Composable
fun AnimatingBox(boxState: BoxState) {
val transitionData = updateTransitionData(boxState)
// UI tree
Box(
modifier = Modifier
.background(transitionData.color)
.size(transitionData.size)
)
}
// Holds the animation values.
private class TransitionData(
color: State<Color>,
size: State<Dp>
) {
val color by color
val size by size
}
// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
val transition = updateTransition(boxState)
val color = transition.animateColor { state ->
when (state) {
BoxState.Collapsed -> Color.Gray
BoxState.Expanded -> Color.Red
}
}
val size = transition.animateDp { state ->
when (state) {
BoxState.Collapsed -> 64.dp
BoxState.Expanded -> 128.dp
}
}
return remember(transition) { TransitionData(color, size) }
}
rememberInfiniteTransition
InfiniteTransition
에는 Transition
과 같은 하위 애니메이션이 하나 이상 포함되지만 컴포지션을 시작하는 즉시 애니메이션이 시작되며 삭제되지 않는 한 중단되지 않습니다. rememberInfiniteTransition
을 사용하여 InfiniteTransition
의 인스턴스를 만들 수 있습니다. 하위 애니메이션은 animateColor
, animatedFloat
또는 animatedValue
와 함께 추가할 수 있습니다. 또한 애니메이션 사양을 지정하려면 infiniteRepeatable을 지정해야 합니다.
val infiniteTransition = rememberInfiniteTransition()
val color by infiniteTransition.animateColor(
initialValue = Color.Red,
targetValue = Color.Green,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Box(Modifier.fillMaxSize().background(color))
하위 수준 Animation API
이전 섹션에서 언급된 모든 상위 수준 Animation API는 하위 수준 Animation API를 기반으로 빌드됩니다.
animate*AsState
함수는 가장 단순한 API로 인스턴트 값 변경사항을 애니메이션 값으로 렌더링합니다. 단일 값을 애니메이션 처리하는 코루틴 기반 API인 Animatable
의 지원을 받습니다. updateTransition
에서는 여러 애니메이션 값을 관리하고 상태 변경에 따라 실행할 수 있는 전환 객체를 만듭니다. rememberInfiniteTransition
도 비슷하지만 이 함수는 계속해서 무제한 실행되는 여러 애니메이션을 관리할 수 있는 무한 전환을 만듭니다. 이러한 API는 Animatable
을 제외하고 모두 컴포저블이며, 컴포지션 외부에서 이러한 애니메이션을 만들 수 있습니다.
이러한 API는 모두 더 기본적인 Animation
API를 기반으로 합니다. 대부분의 앱은 Animation
과 직접 상호작용하지 않지만 Animation
의 일부 맞춤설정 기능은 상위 수준 API를 통해 사용할 수 있습니다. AnimationVector
및 AnimationSpec
에 관한 자세한 내용은 애니메이션 맞춤설정을 참고하세요.
Animatable
Animatable
은 값이 animateTo
를 통해 변경될 때 값을 애니메이션으로 표시할 수 있는 값 홀더입니다. 이 API는 animate*AsState
의 구현을 지원하는 API로,
일관된 지속성과 상호 배타성을 보장하므로 값 변경이 항상 지속되고 진행 중인 모든 애니메이션이 취소됩니다.
animateTo
를 포함한 Animatable
의 많은 기능이 정지 함수로 제공됩니다. 즉 적절한 코루틴 범위에 래핑해야 합니다. 예를 들어 LaunchedEffect
컴포저블을 사용하여 지정된 키 값의 기간에만 해당하는 범위를 만들 수 있습니다.
// Start out gray and animate to green/red based on `ok`
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(Modifier.fillMaxSize().background(color.value))
위의 예에서는 Color.Gray
의 초깃값을 사용하여 Animatable
의 인스턴스를 만들고 저장합니다. 불리언 플래그 ok
의 값에 따라 색상이 Color.Green
또는 Color.Red
로 애니메이션 처리됩니다. 이후 불리언 값이 변경되면 다른 색상으로 처리되는 애니메이션이 시작됩니다. 값이 변경될 때 진행 중인 애니메이션이 있는 경우 애니메이션이 취소되며 새로운 애니메이션이 현재 스냅샷 값에서 현재 속도로 시작됩니다.
이 구현은 이전 섹션에서 언급한 animate*AsState
API를 백업하는 애니메이션 구현입니다. animate*AsState
와 비교하여 Animatable
을 사용하면 여러 가지 측면을 더 세밀하게 직접 관리할 수 있습니다. 첫째, Animatable
에는 첫 번째 타겟 값과 다른 초깃값을 설정할 수 있습니다.
예를 들어 위의 코드 예에서 처음에는 잠시 회색 상자가 표시되었다가 곧바로 녹색 또는 빨간색으로 애니메이션 처리됩니다. 둘째, Animatable
은 콘텐츠 값에 관한 추가 작업(snapTo
및 animateDecay
)을 제공합니다. snapTo
는 현재 값을 즉시 타겟 값으로 설정합니다. 이 기능은 애니메이션 자체가 유일한 정보 소스가 아니고 터치 이벤트와 같은 다른 상태와 동기화해야 하는 경우 유용합니다. animateDecay
는 지정된 속도보다 느려지는 애니메이션을 시작합니다. 이 기능은 플링 동작을 구현하는 데 유용합니다. 자세한 내용은 동작 및 애니메이션을 참고하세요.
Animatable
은 즉시 Float
및 Color
를 지원하지만 TwoWayConverter
를 제공하면 모든 데이터 유형을 사용할 수 있습니다. 자세한 내용은 AnimationVector를 참고하세요.
AnimationSpec
을 제공하여 애니메이션 사양을 맞춤설정할 수 있습니다.
자세한 내용은 AnimationSpec을 참고하세요.
애니메이션
Animation
은 사용 가능한 가장 낮은 수준의 Animation API입니다. 지금까지 본 많은 애니메이션은 Animation을 기반으로 빌드되었습니다. Animation
하위유형에는 두 가지가 있습니다. TargetBasedAnimation
과 DecayAnimation
입니다.
Animation
은 애니메이션 시간을 수동으로 제어하는 데만 사용해야 합니다.
Animation
은 스테이트리스(Stateless)이고 수명 주기 개념이 없습니다. 상위 수준 API에서 사용하는 애니메이션 계산 엔진 역할을 합니다.
TargetBasedAnimation
다른 API는 대부분의 사용 사례에 적용되지만 TargetBasedAnimation
을 사용하면 애니메이션 재생 시간을 직접 관리할 수 있습니다. 아래 예에서 TargetAnimation
의 재생 시간은 withFrameNanos
에서 제공하는 프레임 시간을 기반으로 수동으로 관리됩니다.
val anim = remember {
TargetBasedAnimation(
animationSpec = tween(200),
typeConverter = Float.VectorConverter,
initialValue = 200f,
targetValue = 1000f
)
}
var playTime by remember { mutableStateOf(0L) }
LaunchedEffect(anim) {
val startTime = withFrameNanos { it }
do {
playTime = withFrameNanos { it } - startTime
val animationValue = anim.getValueFromNanos(playTime)
} while (someCustomCondition())
}
DecayAnimation
TargetBasedAnimation
과 달리 DecayAnimation
에는 targetValue
를 제공할 필요가 없습니다. 대신 initialVelocity
와 initialValue
, 제공된 DecayAnimationSpec
에 의해 설정된 시작 조건에 기반하여 targetValue
를 계산합니다.
감쇠 애니메이션은 플링 동작 후에 흔히 사용되어 요소가 천천히 중지되도록 합니다. 애니메이션 속도는 initialVelocityVector
에서 설정한 값으로 시작하며 시간이 지남에 따라 느려집니다.
애니메이션 맞춤설정
많은 Animation API에서는 일반적으로 동작을 맞춤설정하는 매개변수를 사용할 수 있습니다.
AnimationSpec
대부분의 Animation API에서 개발자가 선택적 AnimationSpec
매개변수로 애니메이션 사양을 맞춤설정할 수 있습니다.
val alpha: Float by animateFloatAsState(
targetValue = if (enabled) 1f else 0.5f,
// Configure the animation duration and easing.
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
)
다양한 유형의 애니메이션을 만들 수 있는 여러 AnimationSpec
유형이 있습니다.
spring
spring
은 시작 값과 끝 값 사이에 물리학 기반 애니메이션을 만들며 두 매개변수 dampingRatio
및 stiffness
를 사용합니다.
dampingRatio
는 스프링의 탄성을 정의합니다. 기본값은 Spring.DampingRatioNoBouncy
입니다.
stiffness
는 스프링이 종료 값으로 이동하는 속도를 정의합니다. 기본값은 Spring.StiffnessMedium
입니다.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
)
)
spring
은 애니메이션 도중 타겟 값이 변경될 때 속도의 연속성을 보장하므로 기간 기반 AnimationSpec
유형보다 원활하게 중단을 처리할 수 있습니다. spring
은 animate*AsState
, updateTransition
등 많은 Animation API에서 기본 AnimationSpec으로 사용됩니다.
tween
tween
은 이징 곡선을 사용하여 지정된 durationMillis
동안 시작 값과 끝 값 간에 애니메이션을 처리합니다. 자세한 내용은 이징을 참고하세요. delayMillis
를 지정하여 애니메이션 시작을 연기할 수도 있습니다.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)
keyframes
keyframes
는 애니메이션 기간에 여러 타임스탬프에서 지정된 스냅샷 값을 기반으로 애니메이션을 처리합니다. 언제나 애니메이션 값은 두 키프레임 값 사이에 보간됩니다. 키프레임마다 이징을 지정하여 보간 유형 곡선을 결정할 수 있습니다.
0밀리초 및 지속 시간에 값을 지정할 수도 있습니다. 이 값을 지정하지 않으면 각각 애니메이션의 시작 값과 종료 값으로 기본 설정됩니다.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = keyframes {
durationMillis = 375
0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms
0.2f at 15 with FastOutLinearInEasing // for 15-75 ms
0.4f at 75 // ms
0.4f at 225 // ms
}
)
repeatable
repeatable
은 지정된 반복 횟수에 도달할 때까지 기간 기반 애니메이션(예: tween
또는 keyframes
)을 반복적으로 실행합니다. repeatMode
매개변수를 전달하여 처음(RepeatMode.Restart
)부터 또는 끝(RepeatMode.Reverse
)부터 시작하여 애니메이션을 반복할지 여부를 지정할 수 있습니다.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = repeatable(
iterations = 3,
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
infiniteRepeatable
infiniteRepeatable
은 repeatable
과 유사하지만 무한 반복됩니다.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
ComposeTestRule
을 사용하는 테스트에서 infiniteRepeatable
을 사용하는 애니메이션은 실행되지 않습니다. 구성요소는 각 애니메이션된 값의 초깃값을 사용하여 렌더링됩니다.
snap
snap
은 값을 즉시 종료 값으로 변환하는 특수 AnimationSpec
입니다. delayMillis
를 지정하여 애니메이션 시작을 지연시킬 수 있습니다.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = snap(delayMillis = 50)
)
이징
기간 기반 AnimationSpec
작업(예: tween
또는 keyframes
)은 Easing
을 사용하여 애니메이션의 분수 값을 조정합니다. 이렇게 하면 애니메이션 값이 일정한 속도로 움직이는 대신 더 빨라지고 느려질 수 있습니다. 비율은 애니메이션의 현재 지점을 나타내는 0(시작)과 1.0(끝) 사이의 값입니다.
이징은 실제로는 0과 1.0 사이의 분수 값을 가져와 부동 소수점 수를 반환하는 함수입니다. 반환된 값은 목표를 초과하거나 목표에 미달한 값을 나타내기 위해 경계를 벗어날 수 있습니다. 아래의 코드와 같이 맞춤 이징을 만들 수 있습니다.
val CustomEasing = Easing { fraction -> fraction * fraction }
@Composable
fun EasingUsage() {
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
easing = CustomEasing
)
)
// ...
}
Compose는 대부분의 사용 사례가 포함되는 여러 내장 Easing
함수를 제공합니다.
시나리오에 따라 이징에서 사용해야 하는 함수에 관한 자세한 내용은 속도 - Material Design을 참고하세요.
FastOutSlowInEasing
LinearOutSlowInEasing
FastOutLinearEasing
LinearEasing
CubicBezierEasing
- 더 보기
AnimationVector
대부분의 Compose Animation API에서는 기본적으로 Float
, Color
, Dp
, 기타 기본 데이터 유형이 애니메이션 값으로 지원되지만 맞춤 데이터 유형을 포함하여 다른 데이터 유형을 애니메이션 처리해야 하는 경우도 있습니다. 애니메이션 중에 모든 애니메이션 값은 AnimationVector
로 표시됩니다. 값은 핵심 애니메이션 시스템이 균일하게 처리할 수 있도록 상응하는 TwoWayConverter
에 의해 AnimationVector
로 변환되며 그 반대로도 변환됩니다. 예를 들어 Int
는 단일 부동 소수점 값을 보유한 AnimationVector1D
로 표시됩니다.
Int
의 TwoWayConverter
는 다음과 같습니다.
val IntToVector: TwoWayConverter<Int, AnimationVector1D> =
TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })
Color
는 기본적으로 빨간색, 녹색, 파란색, 알파 등 네 값의 집합이므로 Color
는 네 개의 부동 소수점 값을 보유한 AnimationVector4D
로 변환됩니다. 애니메이션에 사용되는 모든 데이터 유형은 이러한 방식으로 차원에 따라 AnimationVector1D
, AnimationVector2D
, AnimationVector3D
또는 AnimationVector4D
로 변환됩니다. 따라서 객체의 여러 구성요소를 각각 자체 속도 추적 기능을 사용하여 독립적으로 애니메이션 처리할 수 있습니다. Color.VectorConverter
, Dp.VectorConverter
등으로 기본 데이터 유형의 내장 변환기에 액세스할 수 있습니다.
새 데이터 유형 지원 기능을 애니메이션 값으로 추가하려면 자체 TwoWayConverter
를 만들어 API에 제공하면 됩니다. 예를 들어 다음과 같이 animateValueAsState
를 사용하여 맞춤 데이터 유형을 애니메이션 처리할 수 있습니다.
data class MySize(val width: Dp, val height: Dp)
@Composable
fun MyAnimation(targetSize: MySize) {
val animSize: MySize by animateValueAsState<MySize, AnimationVector2D>(
targetSize,
TwoWayConverter(
convertToVector = { size: MySize ->
// Extract a float value from each of the `Dp` fields.
AnimationVector2D(size.width.value, size.height.value)
},
convertFromVector = { vector: AnimationVector2D ->
MySize(vector.v1.dp, vector.v2.dp)
}
)
)
}
애니메이션 벡터 리소스(실험용)
AnimatedVectorDrawable
리소스를 사용하려면 animatedVectorResource
를 사용하여 드로어블 파일을 로드하고 boolean
을 전달하여 드로어블의 시작 상태와 종료 상태를 전환합니다.
@Composable
fun AnimatedVectorDrawable() {
val image = AnimatedImageVector.animatedVectorResource(R.drawable.ic_hourglass_animated)
var atEnd by remember { mutableStateOf(false) }
Image(
painter = rememberAnimatedVectorPainter(image, atEnd),
contentDescription = "Timer",
modifier = Modifier.clickable {
atEnd = !atEnd
},
contentScale = ContentScale.Crop
)
}
드로어블 파일의 형식에 관한 자세한 내용은 드로어블 그래픽 애니메이션화를 참고하세요.
목록 항목 애니메이션
지연 목록 또는 그리드 내에서 항목 재정렬에 애니메이션을 적용하려면 지연 레이아웃 항목 애니메이션 문서를 참고하세요.
동작 및 애니메이션(고급)
터치 이벤트와 애니메이션을 사용할 때는 애니메이션만 사용할 때에 비해 여러 가지 사항을 고려해야 합니다. 무엇보다도 사용자 상호작용의 우선순위가 가장 높아야 하므로 터치 이벤트가 시작될 때 진행 중인 애니메이션을 중단해야 할 수도 있습니다.
아래 예에서는 Animatable
을 사용하여 원 구성요소의 오프셋 위치를 나타냅니다. 터치 이벤트는 pointerInput
수정자로 처리됩니다. 새 탭 이벤트가 감지되면 animateTo
를 호출하여 오프셋 값을 탭 위치에 애니메이션 처리합니다. 애니메이션 도중에도 탭 이벤트가 발생할 수 있으며 이 경우 animateTo
는 진행 중인 애니메이션을 중단하고 중단된 애니메이션의 속도를 유지하면서 새 타겟 위치로 애니메이션을 시작합니다.
@Composable
fun Gesture() {
val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
coroutineScope {
while (true) {
// Detect a tap event and obtain its position.
val position = awaitPointerEventScope {
awaitFirstDown().position
}
launch {
// Animate to the tap position.
offset.animateTo(position)
}
}
}
}
) {
Circle(modifier = Modifier.offset { offset.value.toIntOffset() })
}
}
private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
또 하나의 빈번한 패턴은 애니메이션 값을 드래그와 같은 터치 이벤트에서 발생하는 값과 동기화해야 한다는 것입니다. 아래 예에서는 '스와이프하여 닫기'가 SwipeToDismiss
컴포저블을 사용하는 대신 Modifier
로 구현됩니다. 요소의 가로 오프셋은 Animatable
로 표시됩니다. 이 API에는 동작 애니메이션에 유용한 특성이 있습니다. 특성의 값은 애니메이션은 물론 터치 이벤트에서도 변경할 수 있습니다. 터치 다운 이벤트가 수신되면 Animatable
이 stop
메서드를 사용하여 정지되고 진행 중인 애니메이션이 중단됩니다.
드래그 이벤트 중에 snapTo
를 사용하여 Animatable
값을 터치 이벤트에서 계산된 값으로 업데이트합니다. 플링의 경우 Compose는 드래그 이벤트를 기록하고 속도를 계산할 수 있도록 VelocityTracker
를 제공합니다. 플링 애니메이션의 경우 animateDecay
에 직접 속도를 제공할 수 있습니다. 오프셋 값을 원래 위치로 되돌리려는 경우 animateTo
메서드를 사용하여 0f
의 타겟 오프셋 값을 지정합니다.
fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
// Used to calculate fling decay.
val decay = splineBasedDecay<Float>(this)
// Use suspend functions for touch events and the Animatable.
coroutineScope {
while (true) {
// Detect a touch down event.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
val velocityTracker = VelocityTracker()
// Stop any ongoing animation.
offsetX.stop()
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
// Update the animation value with touch events.
launch {
offsetX.snapTo(
offsetX.value + change.positionChange().x
)
}
velocityTracker.addPosition(
change.uptimeMillis,
change.position
)
}
}
// No longer receiving touch events. Prepare the animation.
val velocity = velocityTracker.calculateVelocity().x
val targetOffsetX = decay.calculateTargetValue(
offsetX.value,
velocity
)
// The animation stops when it reaches the bounds.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back.
offsetX.animateTo(
targetValue = 0f,
initialVelocity = velocity
)
} else {
// The element was swiped away.
offsetX.animateDecay(velocity, decay)
onDismissed()
}
}
}
}
}
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
테스트
Compose는 테스트 클록을 완전히 제어할 수 있는 확정적인 방식으로 애니메이션 테스트를 작성할 수 있는 ComposeTestRule
을 제공합니다. 이를 통해 중간 애니메이션 값을 확인할 수 있습니다. 또한 애니메이션의 실제 기간보다 시간보다 더 빠르게 테스트를 실행할 수 있습니다.
ComposeTestRule
은 테스트 클록을 mainClock
으로 노출합니다. 테스트 코드에서 autoAdvance
속성을 false로 설정하여 클록을 제어할 수 있습니다. 테스트할 애니메이션을 시작한 후 advanceTimeBy
를 사용하여 클록을 앞으로 이동할 수 있습니다.
참고: advanceTimeBy
는 클록을 이동할 때 지정된 기간을 정확히 따르지 않습니다. 대신 프레임 기간의 배수인 가장 가까운 기간으로 반올림합니다.
@get:Rule
val rule = createComposeRule()
@Test
fun testAnimationWithClock() {
// Pause animations
rule.mainClock.autoAdvance = false
var enabled by mutableStateOf(false)
rule.setContent {
val color by animateColorAsState(
targetValue = if (enabled) Color.Red else Color.Green,
animationSpec = tween(durationMillis = 250)
)
Box(Modifier.size(64.dp).background(color))
}
// Initiate the animation.
enabled = true
// Let the animation proceed.
rule.mainClock.advanceTimeBy(50L)
// Compare the result with the image showing the expected result.
// `assertAgainGolden` needs to be implemented in your code.
rule.onRoot().captureToImage().assertAgainstGolden()
}
도구 지원
Android 스튜디오는 애니메이션 미리보기에서 updateTransition
및 animatedVisibility
검사를 지원합니다. 다음 작업을 실행할 수 있습니다.
- 프레임별로 전환 프레임 미리보기
- 전환 시 모든 애니메이션 값 검사
- 초기 상태와 타겟 상태 간 전환 미리보기
- 한 번에 여러 애니메이션 검사 및 조정
애니메이션 미리보기를 시작하면 미리보기에 포함된 모든 전환을 실행할 수 있는 'Animations' 창이 표시됩니다. 전환 및 전환의 각 애니메이션 값에 기본 이름으로 라벨이 지정됩니다. updateTransition
및 AnimatedVisibility
함수에서 label
매개변수를 지정하여 라벨을 맞춤설정할 수 있습니다. 자세한 내용은 애니메이션 미리보기를 참고하세요.
자세히 알아보기
Jetpack Compose의 애니메이션에 관한 자세한 내용은 다음 추가 리소스를 참고하세요.