가치 기반 애니메이션

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 함수를 즉시 제공합니다. 일반 유형을 취하는 animateValueAsStateTwoWayConverter를 제공하여 다른 데이터 유형의 지원 기능을 쉽게 추가할 수 있습니다.

AnimationSpec을 제공하여 애니메이션 사양을 맞춤설정할 수 있습니다. 자세한 내용은 AnimationSpec을 참고하세요.

전환으로 여러 속성을 동시에 애니메이션 처리

Transition은 하나 이상의 애니메이션을 하위 요소로 관리하며 여러 상태 간에 동시에 실행합니다.

상태는 모든 데이터 유형이 될 수 있습니다. 대부분의 경우 다음 예와 같이 맞춤 enum 유형을 사용하여 유형 안전성을 보장할 수 있습니다.

enum class BoxState {
    Collapsed,
    Expanded
}

updateTransitionTransition의 인스턴스를 만들고 저장하며 상태를 업데이트합니다.

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.currentStateTransition.targetState와 동일하게 됩니다. 이 기능은 전환이 완료되었는지 여부를 나타내는 신호로 사용할 수 있습니다.

초기 상태를 첫 번째 타겟 상태와 다르게 설정해야 하는 경우도 있습니다. updateTransitionMutableTransitionState와 함께 사용하여 이 목표를 달성할 수 있습니다. 예를 들어 이렇게 하면 코드가 컴포지션을 시작하는 즉시 애니메이션을 시작할 수 있습니다.

// 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
            }
        )
    }
}

AnimatedVisibilityAnimatedContent와 함께 전환 사용

AnimatedVisibilityAnimatedContentTransition의 확장 함수로 사용할 수 있습니다. Transition.AnimatedVisibilityTransition.AnimatedContenttargetStateTransition에서 파생되며 TransitiontargetState가 변경될 때 필요에 따라 들어가기/나가기 전환을 트리거합니다. 이러한 확장 함수를 사용하면 AnimatedVisibility/AnimatedContent 내부에 있는 모든 들어가기/나가기/sizeTransform 애니메이션을 Transition으로 호이스팅할 수 있습니다. 확장 함수를 통해 AnimatedVisibility/AnimatedContent의 상태 변경을 외부에서 관찰할 수 있습니다. 불리언 visible 매개변수 대신 이 AnimatedVisibility 버전은 상위 전환의 타겟 상태를 불리언으로 변환하는 람다를 사용합니다.

자세한 내용은 AnimatedVisibilityAnimatedContent를 참고하세요.

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를 통해 사용할 수 있습니다. AnimationVectorAnimationSpec에 관한 자세한 내용은 애니메이션 맞춤설정을 참고하세요.

다양한 하위 수준 애니메이션 API 간의 관계를 보여주는 다이어그램

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은 콘텐츠 값에 관한 추가 작업(snapToanimateDecay)을 제공합니다. snapTo는 현재 값을 즉시 타겟 값으로 설정합니다. 이 기능은 애니메이션 자체가 유일한 정보 소스가 아니고 터치 이벤트와 같은 다른 상태와 동기화해야 하는 경우 유용합니다. animateDecay는 지정된 속도보다 느려지는 애니메이션을 시작합니다. 이 기능은 플링 동작을 구현하는 데 유용합니다. 자세한 내용은 동작 및 애니메이션을 참고하세요.

Animatable은 즉시 FloatColor를 지원하지만 TwoWayConverter를 제공하면 모든 데이터 유형을 사용할 수 있습니다. 자세한 내용은 AnimationVector를 참고하세요.

AnimationSpec을 제공하여 애니메이션 사양을 맞춤설정할 수 있습니다. 자세한 내용은 AnimationSpec을 참고하세요.

Animation: 수동으로 제어되는 애니메이션

Animation은 사용 가능한 가장 낮은 수준의 애니메이션 API입니다. 지금까지 본 많은 애니메이션은 Animation을 기반으로 빌드되었습니다. Animation 하위유형에는 두 가지가 있습니다. TargetBasedAnimationDecayAnimation입니다.

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를 제공할 필요가 없습니다. 대신 initialVelocityinitialValue, 제공된 DecayAnimationSpec에 의해 설정된 시작 조건에 기반하여 targetValue를 계산합니다.

감쇠 애니메이션은 플링 동작 후에 흔히 사용되어 요소가 천천히 중지되도록 합니다. 애니메이션 속도는 initialVelocityVector에서 설정한 값으로 시작하며 시간이 지남에 따라 느려집니다.