animate*AsState
를 사용하여 단일 값에 애니메이션 적용
animate*AsState
함수는 Compose에서 단일 값을 애니메이션 처리하는 가장 간단한 애니메이션 API입니다. 타겟 값 (또는 최종 값)만 제공하면 API가 현재 값에서 지정된 값으로 애니메이션을 시작합니다.
다음은 이 API를 사용하여 알파를 애니메이션 처리하는 예입니다. 타겟 값을 animateFloatAsState
에 래핑하기만 하면 알파 값이 제공된 값 (이 경우 1f
또는 0.5f
) 사이의 애니메이션 값이 됩니다.
var enabled by remember { mutableStateOf(true) } val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha") 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을 참고하세요.
전환으로 여러 속성을 동시에 애니메이션 처리
Transition
은 하나 이상의 애니메이션을 하위 요소로 관리하며 여러 상태 간에 동시에 실행합니다.
상태는 모든 데이터 유형이 될 수 있습니다. 대부분의 경우 다음 예와 같이 맞춤 enum
유형을 사용하여 유형 안전성을 보장할 수 있습니다.
enum class BoxState { Collapsed, Expanded }
updateTransition
는 Transition
의 인스턴스를 만들고 저장하며 상태를 업데이트합니다.
var currentState by remember { mutableStateOf(BoxState.Collapsed) } val transition = updateTransition(currentState, label = "box state")
그런 다음 animate*
확장 함수 중 하나를 사용하여 이 전환에서 하위 애니메이션을 정의할 수 있습니다. 각 상태의 타겟 값을 지정합니다.
이 animate*
함수는 전환 상태가 updateTransition
으로 업데이트될 때 애니메이션의 모든 프레임에서 업데이트되는 애니메이션 값을 반환합니다.
val rect by transition.animateRect(label = "rectangle") { state -> when (state) { BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f) BoxState.Expanded -> Rect(100f, 100f, 300f, 300f) } } val borderWidth by transition.animateDp(label = "border width") { 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) } }, label = "color" ) { state -> when (state) { BoxState.Collapsed -> MaterialTheme.colorScheme.primary BoxState.Expanded -> MaterialTheme.colorScheme.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 = rememberTransition(currentState, label = "box state") // ……
여러 구성 가능한 함수가 관련된 좀 더 복잡한 전환의 경우 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, label = "dialer state") 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, label = "selected state") val borderColor by transition.animateColor(label = "border color") { isSelected -> if (isSelected) Color.Magenta else Color.White } val elevation by transition.animateDp(label = "elevation") { isSelected -> if (isSelected) 10.dp else 2.dp } Surface( onClick = { selected = !selected }, shape = RoundedCornerShape(8.dp), border = BorderStroke(2.dp, borderColor), shadowElevation = 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, label = "box state") val color = transition.animateColor(label = "color") { state -> when (state) { BoxState.Collapsed -> Color.Gray BoxState.Expanded -> Color.Red } } val size = transition.animateDp(label = "size") { 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(label = "infinite") val color by infiniteTransition.animateColor( initialValue = Color.Red, targetValue = Color.Green, animationSpec = infiniteRepeatable( animation = tween(1000, easing = LinearEasing), repeatMode = RepeatMode.Reverse ), label = "color" ) Box( Modifier .fillMaxSize() .background(color) )
하위 수준 애니메이션 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 { mutableLongStateOf(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
에서 설정한 값으로 시작하며 시간이 지남에 따라 느려집니다.
추천 서비스
- 참고: JavaScript가 사용 중지되어 있으면 링크 텍스트가 표시됩니다.
- 애니메이션 맞춤설정 {:#customize-animations}
- Compose의 애니메이션
- 애니메이션 수정자 및 컴포저블