애니메이션 수정자 및 컴포저블

Compose에는 일반적인 애니메이션 사용 사례를 처리하기 위한 컴포저블과 수정자가 내장되어 있습니다.

내장된 애니메이션 컴포저블

AnimatedVisibility로 나타남과 사라짐 애니메이션 처리

자체를 표시하고 숨기는 녹색 컴포저블
그림 1. 열에서 항목의 표시와 사라짐 애니메이션 처리

AnimatedVisibility 컴포저블은 콘텐츠 표시와 사라짐을 애니메이션 처리합니다.

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

기본적으로는 콘텐츠는 페이드인 및 확장 방식으로 표시되고 페이드아웃 및 축소 방식으로 사라집니다. EnterTransitionExitTransition을 지정하여 전환을 맞춤설정할 수 있습니다.

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 객체를 결합할 수 있으며 각 객체에 선택적 매개변수를 사용하여 동작을 맞춤설정할 수 있습니다. 자세한 내용은 참고 자료를 확인하세요.

EnterTransitionExitTransition 예시

EnterTransition ExitTransition
fadeIn
페이드 인 애니메이션
fadeOut
페이드 아웃 애니메이션
slideIn
슬라이드 인 애니메이션
slideOut
슬라이드 아웃 애니메이션
slideInHorizontally
가로 슬라이드 인 애니메이션
slideOutHorizontally
가로 슬라이드 아웃 애니메이션
slideInVertically
세로 슬라이드 인 애니메이션
slideOutVertically
세로 슬라이드 아웃 애니메이션
scaleIn
스케일 인 애니메이션
scaleOut
스케일 아웃 애니메이션
expandIn
확장 애니메이션
shrinkOut
축소 애니메이션
expandHorizontally
가로 확장 애니메이션
shrinkHorizontally
가로 축소 애니메이션
expandVertically
세로 확장 애니메이션
shrinkVertically
세로 축소 애니메이션

AnimatedVisibilityMutableTransitionState를 사용하는 변형도 제공합니다. 이를 통해 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 컴포저블에 지정된 애니메이션과 하위 요소의 자체 들어가기 및 나가기 애니메이션의 조합입니다.

var visible by remember { mutableStateOf(true) }

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.NoneExitTransition.None을 지정하세요.

맞춤 애니메이션 추가

기본 제공 들어가기 및 나가기 애니메이션 외에 맞춤 애니메이션 효과를 추가하려면 AnimatedVisibility 콘텐츠 람다 내의 transition 속성을 통해 기본 Transition 인스턴스에 액세스합니다. Transition 인스턴스에 추가된 애니메이션 상태는 AnimatedVisibility의 들어가기 및 나가기 애니메이션과 동시에 실행됩니다. AnimatedVisibility는 콘텐츠를 삭제하기 전에 Transition의 모든 애니메이션이 완료될 때까지 기다립니다. Transition과는 별개로 만들어진 나가기 애니메이션(예: animate*AsState 사용)의 경우 AnimatedVisibility는 이를 고려할 수 없으므로 완료되기 전에 콘텐츠 컴포저블을 삭제할 수 있습니다.

var visible by remember { mutableStateOf(true) }

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(label = "color") { state ->
        if (state == EnterExitState.Visible) Color.Blue else Color.Gray
    }
    Box(
        modifier = Modifier
            .size(128.dp)
            .background(background)
    )
}

Transition에 관한 자세한 내용은 updateTransition을 참고하세요.

AnimatedContent를 사용하여 타겟 상태에 따라 애니메이션 처리

AnimatedContent 컴포저블은 타겟 상태에 따라 콘텐츠가 변경될 때 콘텐츠에 애니메이션을 적용합니다.

Row {
    var count by remember { mutableIntStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Add")
    }
    AnimatedContent(
        targetState = count,
        label = "animated content"
    ) { targetCount ->
        // Make sure to use `targetCount`, not `count`.
        Text(text = "Count: $targetCount")
    }
}

항상 람다 매개변수를 사용하고 이를 콘텐츠에 반영해야 합니다. API는 이 값을 키로 사용하여 현재 표시되는 콘텐츠를 식별합니다.

기본적으로 초기 콘텐츠는 페이드 아웃되고 타겟 콘텐츠가 페이드 인됩니다. 이 동작을 페이드 스루라고 합니다. transitionSpec 매개변수에 ContentTransform 객체를 지정하여 이 애니메이션 동작을 맞춤설정할 수 있습니다. with 중위 함수로 EnterTransitionExitTransition과 결합하여 ContentTransform을 만들 수 있습니다. SizeTransformusing 중위 함수로 연결하여 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() togetherWith
                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() togetherWith
                slideOutVertically { height -> height } + fadeOut()
        }.using(
            // Disable clipping since the faded slide-in/out should
            // be displayed out of bounds.
            SizeTransform(clip = false)
        )
    }, label = "animated content"
) { targetCount ->
    Text(text = "$targetCount")
}

EnterTransition은 타겟 콘텐츠가 표시되는 방식을 정의하고 ExitTransition은 초기 콘텐츠가 사라지는 방식을 정의합니다. AnimatedVisibility에서 사용할 수 있는 모든 EnterTransitionExitTransition 함수 외에도 AnimatedContentslideIntoContainerslideOutOfContainer를 제공합니다. 이는 초기 콘텐츠 크기와 AnimatedContent 콘텐츠의 타겟 콘텐츠에 따라 슬라이드 거리를 계산하는 slideInHorizontally/VerticallyslideOutHorizontally/Vertically의 편리한 대안입니다.

SizeTransform은 초기 콘텐츠와 타겟 콘텐츠 사이에 크기가 애니메이션되는 방식을 정의합니다. 애니메이션을 만들 때 초기 크기와 타겟 크기에 모두 액세스할 수 있습니다. SizeTransform은 애니메이션 중에 콘텐츠를 구성요소 크기로 잘라야 하는지도 제어합니다.

var expanded by remember { mutableStateOf(false) }
Surface(
    color = MaterialTheme.colorScheme.primary,
    onClick = { expanded = !expanded }
) {
    AnimatedContent(
        targetState = expanded,
        transitionSpec = {
            fadeIn(animationSpec = tween(150, 150)) togetherWith
                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
                        }
                    }
                }
        }, label = "size transform"
    ) { targetExpanded ->
        if (targetExpanded) {
            Expanded()
        } else {
            ContentIcon()
        }
    }
}

하위 요소의 들어가기 및 나가기 전환 애니메이션

AnimatedVisibility와 마찬가지로 animateEnterExit 수정자는 AnimatedContent 콘텐츠 람다 내에서 사용할 수 있습니다. 이를 사용하여 EnterAnimationExitAnimation을 직접 하위 요소 또는 간접 하위 요소 각각에 별도로 적용합니다.

맞춤 애니메이션 추가

AnimatedVisibility와 마찬가지로 transition 필드는 AnimatedContent 콘텐츠 람다 내에서 사용할 수 있습니다. AnimatedContent 전환과 동시에 실행되는 맞춤 애니메이션 효과를 만드는 데 사용합니다. 자세한 내용은 updateTransition을 참고하세요.

Crossfade를 사용하여 두 레이아웃 간 애니메이션 처리

Crossfade는 크로스페이드 애니메이션을 사용하여 두 레이아웃 사이의 전환을 애니메이션 처리합니다. current 매개변수로 전달된 값을 전환하면 콘텐츠가 크로스페이드 애니메이션을 사용하여 전환됩니다.

var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage, label = "cross fade") { screen ->
    when (screen) {
        "A" -> Text("Page A")
        "B" -> Text("Page B")
    }
}

내장 애니메이션 수정자

animateContentSize를 사용하여 컴포저블 크기 변경 애니메이션 처리

크기를 부드럽게 변경하는 애니메이션을 적용한 녹색 컴포저블
그림 2. 작고 큰 크기 간에 원활하게 애니메이션되는 컴포저블

animateContentSize 수정자는 크기를 변경하는 애니메이션을 표시합니다.

var expanded by remember { mutableStateOf(false) }
Box(
    modifier = Modifier
        .background(colorBlue)
        .animateContentSize()
        .height(if (expanded) 400.dp else 200.dp)
        .fillMaxWidth()
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            expanded = !expanded
        }

) {
}

목록 항목 애니메이션

지연 목록 또는 그리드 내에서 항목 재정렬에 애니메이션을 적용하려면 지연 레이아웃 항목 애니메이션 문서를 참고하세요.