Jetpack Compose - Animation

MUNGI JO·2024년 9월 6일

Android Jetpack Compose

목록 보기
5/7

Composable에 Animation 넣기

Animation을 넣는 방법은 다양하게 있지만 여기선 7가지를 소개해보려 한다.

1. animate*AsState

애니메이션이 필요한 값을 상태로 관리하고 값의 변경을 애니메이션으로 부드럽게 처리한다. 다양한 타입에 대해 지원되며 상태가 변경될 때 부드럽게 전환된다.

animationSpec을 통해 애니메이션 지속시간 및 애니메이션 효과를 지정할 수 있는데 tween, spring등 여러가지가 있다. 자세한건 공식사이트를 참고하면된다.

@Composable
fun AnimatedPadding() {
    var expanded by remember { mutableStateOf(false) }
    val extraPadding by animateDpAsState(
        targetValue = if (expanded) 48.dp else 0.dp,
        animationSpec = tween(durationMillis = 500) // 애니메이션 지속 시간 설정
       /* animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        ) */       
    )

    Column(
        Modifier
            .padding(16.dp)
            .background(Color.Gray)
    ) {
        Text(
            "Tap the button to animate padding",
            modifier = Modifier.padding(extraPadding)
        )
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = { expanded = !expanded }) {
            Text("Toggle Padding")
        }
    }
}

2. AnimatedVisibility

컴포저블의 가시성을 애니메이션으로 처리할 수 있는 API로, 컴포넌트를 보이거나 숨길 때 자연스러운 효과를 제공한다.

@Composable
fun AnimatedVisibilityDemo(shown: Boolean) {
    var isVisible by remember { mutableStateOf(true) }

    Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.background(Color.Red).fillMaxWidth()) { 
        AnimatedVisibility( // 가시성 제어
        visible = shown,
        enter = slideInHorizontally(
            initialOffsetX = { fullHeight -> -fullHeight },
            animationSpec = spring(
                stiffness = Spring.StiffnessMediumLow,
                dampingRatio = Spring.DampingRatioMediumBouncy
            )
        ),
        exit = slideOutHorizontally(targetOffsetX = { fullHeight -> -fullHeight })
        ) { 
        
            Text("Hello, Jetpack Compose!", Modifier.background(Color.LightGray))
        }

        Spacer(modifier = Modifier.height(16.dp))

        Button(onClick = { isVisible = !isVisible }) {
            Text(if (isVisible) "Hide" else "Show")
        }
    }
}

3. updateTransition

여러 상태 간의 복잡한 애니메이션 전환을 처리한다. 하나의 상태에 여러 애니메이션을 동시에 적용하거나 상태 변화에 따른 애니메이션 제어가 가능하다.

@Composable
fun UpdateTransitionDemo() {
    var isExpanded by remember { mutableStateOf(false) }
    // targetState로 목표 상태를 정의
    val transition = updateTransition(targetState = isExpanded, label = "")

    // ixExpanded 상태 변경에 따라서만 작동
    val backgroundColor by transition.animateColor(label = "") { state ->
        if (state) Color.Green else Color.Red
    }

    val size by transition.animateDp(label = "") { state ->
        if (state) 200.dp else 100.dp
    }

    Box(
        modifier = modifier
            .size(size)
            .background(backgroundColor)
            .clickable { isExpanded = !isExpanded },
        contentAlignment = Alignment.Center
    ) {
        Text("Click me", color = Color.White)
    }
}

4. rememberInfiniteTransition

무한 반복 애니메이션을 구현할 때 사용되며, 주기적인 애니메이션 효과를 적용하는 데 적합하다. 예를 들어, 요소가 계속해서 커졌다가 작아지는 애니메이션을 반복하는 효과를 만들 수 있다.

@Composable
fun InfinitePulseAnimation() {
    val infiniteTransition = rememberInfiniteTransition()
    val scale by infiniteTransition.animateFloat(
        initialValue = 1f,
        targetValue = 1.5f,
        animationSpec = infiniteRepeatable( // 애니메이션이 무한 반복되도록 설정
            animation = tween(1000),
            repeatMode = RepeatMode.Reverse // initialValue로 돌아가도록
        )
    )

    Box(
        modifier = Modifier
            .size(100.dp)
            .scale(scale)
            .background(Color.Blue),
        contentAlignment = Alignment.Center
    ) {
        Text("Pulsing", color = Color.White)
    }
}

5. Animatable

수동으로 애니메이션을 제어할 때 사용되며, 특정 사용자 상호작용에 맞춘 애니메이션을 구현하거나 애니메이션을 멈추거나 다시 시작하는 등의 복잡한 동작을 처리할 때 유용하다.

@Composable
fun AnimatableDemo() {
     val animatable = remember { Animatable(0f) }
    val coroutineScope = rememberCoroutineScope() // 코루틴 스코프를 기억
    var animationValue by remember { mutableFloatStateOf(1f) }

    // 컴포저블이 시작될 때 애니메이션 실행
    LaunchedEffect(Unit) {
        animatable.animateTo(
            targetValue = animationValue, // 초기 목표 값
            animationSpec = tween(durationMillis = 1000)
        )
    }

    Box(
        modifier
            .size(100.dp)
            .background(Color.Blue.copy(alpha = animatable.value.coerceIn(0f, 1f))) // value값을 특정 범위 이내(0~1)로 제한 - f크기를 1이상으로 잡아도 문제없음
    )

    Button(onClick = {
        animationValue = if (animationValue != 0f) 0f else 1f
        // 코루틴 스코프 내에서 애니메이션 실행
        coroutineScope.launch {
            animatable.animateTo(
                targetValue = animationValue, // 새로운 목표 값
                animationSpec = tween(durationMillis = 1000)
            )
        }
    }) {
        Text("Animate to 0f or 1f")
    }
}

6. Crossfade

두 상태 간의 전환을 부드러운 페이드 효과로 처리하는 애니메이션으로 상태가 변경될 때 이전 상태와 새로운 상태가 부드럽게 전환되며 페이드 인/아웃 효과가 적용된다.

@Composable
fun CrossfadeDemo() {
    var isToggled by remember { mutableStateOf(true) }

    Crossfade(targetState = isToggled) { toggled ->
        if (toggled) {
            Text("State A", Modifier.background(Color.Cyan).fillMaxWidth().padding(16.dp))
        } else {
            Text("State B", Modifier.background(Color.Magenta).fillMaxWidth().padding(16.dp))
        }
    }

    Button(onClick = { isToggled = !isToggled }, Modifier.padding(16.dp)) {
        Text("Toggle State")
    }
}

7. animateContentSize

animateContentSize는 해당 컴포넌트의 크기 변화가 감지될 때, 크기의 변화에 맞추어 자동으로 애니메이션을 적용한다. 예를 들어 텍스트나 이미지의 크기가 동적으로 변하거나, 새로운 콘텐츠가 추가되면서 레이아웃의 크기가 변경될 때 이 함수는 애니메이션을 통해 자연스럽게 전환이 이루어지도록 한다.

특별한 것은 Modifier에서 제공하는 메서드로 체이닝 방식으로 적용할 수 있다. animationSpec과 initialValue, targetValue를 통해 custom값을 설정할 수도 있다.

Box(
    modifier = Modifier
        .animateContentSize(
            animationSpec = tween(
                durationMillis = 500, // 애니메이션 지속 시간 500ms
                easing = LinearOutSlowInEasing // 서서히 느려지는 애니메이션
            )
        )
        .padding(16.dp)
) {
    // 동적으로 변화하는 콘텐츠
}

animation 종류

1. spring

스프링 애니메이션은 Jetpack Compose에서 물리 기반 애니메이션을 구현하는 데 사용된다. 이 애니메이션은 실제 스프링의 운동을 모델링하여 UI 요소가 자연스럽고 부드럽게 목표 위치로 이동하는 효과를 제공한다. 스프링 애니메이션의 주요 구성 요소로는 Stiffness(강성)와 DampingRatio(감쇠 비율)가 있으며, 이 두 가지는 스프링의 물리적 특성을 결정짓는 중요한 요소이다.

1. Stiffness (강성)

Stiffness는 스프링의 강도를 나타내며, 스프링이 목표 위치로 얼마나 빠르게 복원되는지를 결정한다. 스프링의 강성은 스프링이 더 단단할수록 더 빠르게 목표 위치에 도달하게 하며, 값이 낮을수록 스프링은 더 느리고 부드럽게 움직인다.

높은 Stiffness는 스프링이 매우 단단하여 목표 위치에 빠르게 도달하는 것을 의미한다. 이때 애니메이션은 짧고 빠르게 끝나며, 강한 반응을 보인다.
낮은 Stiffness는 스프링이 유연하고 부드러워 목표 위치에 천천히 도달한다. 이는 느리고 부드러운 애니메이션을 만들며, 부드러운 전환을 원하는 경우 적합하다.

spring(stiffness = Spring.StiffnessVeryLow)

이 코드는 스프링 강성을 매우 낮게 설정하여 애니메이션이 천천히, 부드럽게 진행되도록 한다.

2. DampingRatio (감쇠 비율)

DampingRatio는 스프링 애니메이션의 감쇠 특성을 나타내며, 애니메이션이 목표 위치에 도달한 후 얼마나 흔들림 없이 안정화되는지를 결정한다. 감쇠 비율은 스프링이 목표 위치에 도달할 때 발생할 수 있는 진동 또는 바운스 효과를 제어한다.

낮은 DampingRatio는 스프링이 목표 위치에 도달한 후 여러 번 흔들리며 안정화되는 효과를 준다. 이를 통해 애니메이션은 마치 바운스하는 것처럼 보인다.
높은 DampingRatio는 스프링이 목표 위치에 도달한 후 즉시 정지하는 것을 의미하며, 진동이나 흔들림 없이 부드럽게 멈추게 된다.

spring(dampingRatio = Spring.DampingRatioLowBouncy)

이 코드는 낮은 감쇠 비율을 설정하여 목표 위치에서 약간의 바운스(흔들림)가 있는 애니메이션을 만든다.

3. Stiffness와 DampingRatio의 조합

스프링 애니메이션에서 Stiffness와 DampingRatio를 적절히 조합함으로써 다양한 애니메이션 효과를 구현할 수 있다. 예를 들어, 높은 Stiffness와 낮은 DampingRatio를 함께 사용하면 빠르게 목표 위치에 도달하면서 바운스 효과가 있는 애니메이션을 구현할 수 있다.

val springSpec = spring(
    stiffness = Spring.StiffnessVeryLow,    // 부드럽고 느린 움직임
    dampingRatio = Spring.DampingRatioLowBouncy // 약간의 바운스 효과
)

이 코드는 스프링 강성을 낮게 설정하여 애니메이션이 느리게 진행되고, 목표 위치에 도달할 때 약간의 바운스 효과를 나타내는 애니메이션을 구현한다.

👉 요약
- Stiffness: 스프링의 강성(빠르고 강한 움직임 vs 느리고 부드러운 움직임).
- DampingRatio: 스프링이 목표 위치에 도달할 때 진동 또는 흔들림 없이 멈추는지, 바운스 효과가 있는지를 결정.

2. Tween

Tween 애니메이션은 시간 기반의 애니메이션으로, 설정된 지속 시간(durationMillis) 동안 애니메이션 값을 선형 또는 비선형으로 변화시킨다. tween 애니메이션은 속도의 변화 곡선(가속/감속)을 결정하는 easing 옵션을 제공하여, 시작부터 끝까지 애니메이션이 어떤 방식으로 진행될지를 정의한다.

val tweenSpec = tween(
    durationMillis = 1000,  // 애니메이션 지속 시간 1초
    easing = LinearOutSlowInEasing // 빠르게 시작해 천천히 멈춤
)

👉 요약
- 지속 시간(durationMillis): 애니메이션이 진행되는 시간을 밀리초 단위로 지정한다.
- easing: 애니메이션 속도 변화 패턴을 정의하는 함수. LinearEasing(일정한 속도), FastOutSlowInEasing(빠르게 시작해서 천천히 멈춤) 등 다양한 패턴을 제공한다.

3. Keyframes

Keyframes 애니메이션은 시간에 따라 애니메이션 값을 단계별로 설정하는 방식이다. 특정 시간대에 도달했을 때 애니메이션 값이 어떻게 변하는지를 정의할 수 있으며, 이를 통해 복잡한 애니메이션을 세부적으로 제어할 수 있다. 각 키프레임을 통해 특정 시점에서의 애니메이션 상태를 지정하고, 해당 상태에 도달할 때의 easing 방식도 설정할 수 있다.

val keyframeSpec = keyframes {
    durationMillis = 1000 // 총 애니메이션 시간 1초
    0f at 0 with LinearEasing // 0초에 시작, 값은 0
    0.5f at 500 with FastOutSlowInEasing // 0.5초에 값은 0.5, 빠르게 시작해 천천히 멈춤
    1f at 1000 // 1초에 값은 1로 완료
}

👉 요약
- keyframes: 시간에 따른 값을 지정하며, 여러 시점에서 애니메이션 상태를 구체적으로 정의할 수 있다.
- 각 시점의 easing: 키프레임마다 easing을 다르게 적용할 수 있어, 애니메이션의 특정 구간에서 속도를 다르게 설정할 수 있다.

4. Repeatable

Repeatable 애니메이션은 특정 애니메이션을 반복하는 기능을 제공한다. iterations 속성을 사용하여 애니메이션을 몇 번 반복할지 지정할 수 있으며, repeatMode를 통해 반복할 때 애니메이션이 다시 시작할지, 혹은 역방향으로 실행될지를 선택할 수 있다.

val repeatableSpec = repeatable(
    iterations = 3, // 3회 반복
    animation = tween(1000), // 각 애니메이션은 1초간 진행
    repeatMode = RepeatMode.Reverse // 역방향으로 반복
)

👉 요약
iterations: 애니메이션이 몇 번 반복될지 설정한다.
repeatMode: 반복될 때 애니메이션이 Restart(다시 시작)할지, Reverse(역방향)로 실행될지를 정의한다.

5. InfiniteRepeatable

InfiniteRepeatable 애니메이션은 애니메이션을 무한히 반복하는 기능을 제공한다. iterations 없이 애니메이션을 계속 반복하며, 무한 루프 애니메이션을 구현하는 데 적합하다. 이때도 repeatMode를 설정하여 애니메이션이 순방향으로만 반복될지, 역방향으로도 반복될지를 정의할 수 있다.

val infiniteRepeatableSpec = infiniteRepeatable(
    animation = tween(1000), // 1초짜리 애니메이션을 반복
    repeatMode = RepeatMode.Reverse // 역방향으로 반복
)

👉 요약
animation: 반복될 애니메이션을 지정한다.
repeatMode: 순방향 또는 역방향 반복 여부를 설정한다.

6. animateDecay

animateDecay는 감쇠 애니메이션(Decay Animation)으로 속도가 줄어드는 방식으로 진행되는 애니메이션이다.
fling을 통해 동작 후 남은 속도를 바탕으로 애니메이션의 멈추는 위치

val offsetX = remember { Animatable(0f) }
val decay = splineBasedDecay<Float>(this)

LaunchedEffect(Unit) {
    val velocity = 500f // 드래그 후 손을 뗐을 때의 속도
    offsetX.animateDecay(velocity, decay) // 속도가 줄어들며 멈추는 애니메이션
}

splineBasedDecay이 많이 사용되며 외에도 exponentialDecay를 사용할 수 있다.

참고

애니메이션만 테스트하고 싶다면 Start Animation Preview를 누르면 각 frame별로 애니메이션이 어떻게 움직이는 지, 애니메이션이 시작되면 어떻게 움직이는지 확인이 가능하다. 제공하는 기능은 활용하는게 좋다.

참고 자료

Android Developer

profile
안녕하세요. 개발에 이제 막 뛰어든 신입 개발자 입니다.

0개의 댓글