Android Jetpack Compose 과 XML 기반 뷰 시스템의 큰 차이 중 하나는 애니메이션 효과입니다. 컴포즈에서는 내/외부적으로 많은 Animation API 를 제공하기 때문에 더욱 좋은 사용자 경험을 제공할 수 있습니다.
굉장히 복잡한 단계여서 내가 어떤 애니메이션을 사용하고 싶을 때, 그 조건을 명확히 알고 위에 있는 트리구조를 살펴보면 좋을 것 같다.
var visible by remember {
mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(
visible = visible,
enter = ...
exit = ...
) {
// your composable here
Box(modifier = Modifier.animateEnterExit(
enter = ...
exit = ...
)
}
AnimatedVisibility 후행 람다에 애니메이션 효과를 넣고 싶은 컴포저블을 넣으면 된다.
enter 애니메이션 , exit 애니메이션 두 가지를 줄 수 있다.
https://developer.android.com/develop/ui/compose/animation/composables-modifiers?hl=ko#enter-exit-transitionAnimatedVisibility 내부 요소가 다른 애니메이션이 되도록 적용
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(
enter = slideInVertically(),
exit = slideOutVertically()
)
.sizeIn(minWidth = 256.dp, minHeight = 64.dp)
.background(Color.Red)
) {
}
}
}
Modifier.animateEnterExit 를 사용해 AnimatedVisibility 안에 있는 컴포저블에 특정 애니메이션만 따로 되도록 적용 가능!
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")
}
}
var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage, label = "cross fade") { screen ->
when (screen) {
"A" -> Text("Page A")
"B" -> Text("Page B")
}
}
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
}
) {
}
var moved by remember { mutableStateOf(false) }
val pxToMove = with(LocalDensity.current) { 100.dp.toPx().roundToInt() }
val offset by animateIntOffsetAsState(
targetValue = if (moved) IntOffset(pxToMove, pxToMove) else IntOffset.Zero,
label = "offset"
)
Box(
modifier = Modifier
.offset { offset }
.background(Color.Blue)
.size(100.dp)
.clickable { moved = !moved }
)
var expanded by remember { mutableStateOf(false) }
val padding by animateDpAsState(
targetValue = if (expanded) 32.dp else 8.dp,
label = "padding"
)
Box(
modifier = Modifier
.padding(padding)
.background(Color.Red)
.clickable { expanded = !expanded }
) {
Text("Animated Padding")
}
val h1 = MaterialTheme.typography.h1
val h2 = MaterialTheme.typography.h2
var textStyle by remember { mutableStateOf(h1) }
val animatedTextStyle by animateTextStyleAsState(
targetValue = textStyle,
animationSpec = spring(stiffness = Spring.StiffnessLow),
label = "textStyle"
)
Column {
Text("Animate me!", style = animatedTextStyle)
Button(onClick = { textStyle = h2 }) {
Text("Change Style")
}
}
@Composable
fun AnimatedText() {
val text = "Typing Animation"
val typingDelayInMs = 50L
var substringText by remember { mutableStateOf("") }
LaunchedEffect(text) {
text.forEachIndexed { index, _ ->
substringText = text.substring(0, index + 1)
delay(typingDelayInMs)
}
}
Text(substringText)
}
@Composable
fun AnimatedWaveText(word: String) {
val transition = rememberInfiniteTransition()
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
word.forEachIndexed { index, c ->
val scale by transition.animateFloat(
initialValue = 1f,
targetValue = 2f,
animationSpec = infiniteRepeatable(
tween(durationMillis = 500, delayMillis = index * 100),
RepeatMode.Reverse
),
label = "wave"
)
Text(text = c.toString(), fontSize = (24 * scale).sp)
}
}
}
Jetpack Compose의 애니메이션에서 AnimationSpec은 애니메이션의 동작을 정의하는 핵심 요소로, 애니메이션의 지속 시간, 속도, 가속/감속 곡선 등을 설정할 수 있습니다. 다양한 AnimationSpec 유형을 사용하여 애니메이션을 커스터마이징할 수 있으며, 주요 종류와 예제는 다음과 같습니다.
durationMillis: 애니메이션 지속 시간 (밀리초 단위)easing: 가속/감속 곡선 (예: LinearEasing, FastOutSlowInEasing)delayMillis: 애니메이션 시작 전 지연 시간val alpha by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
easing = FastOutSlowInEasing
),
label = "tween spec"
)`
dampingRatio: 반발 정도 (예: Spring.DampingRatioNoBouncy, Spring.DampingRatioHighBouncy)stiffness: 스프링 강도 (예: Spring.StiffnessLow, Spring.StiffnessMedium)val value by animateFloatAsState(
targetValue = 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
),
label = "spring spec"
)
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = keyframes {
durationMillis = 1000
0.5f at 300 *// 300ms에 값이 0.5f로 설정*
0.8f at 600 *// 600ms에 값이 0.8f로 설정*
},
label = "keyframes spec"
)
iterations: 반복 횟수animation: 반복할 AnimationSpec (예: tween, keyframes 등)repeatMode: 반복 모드 (RepeatMode.Restart 또는 RepeatMode.Reverse)val value by animateFloatAsState(
targetValue = 1f,
animationSpec = repeatable(
iterations = 3,
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
),
label = "repeatable spec"
)
animation: 반복할 AnimationSpec (예: tween, keyframes 등)repeatMode: 반복 모드 (RepeatMode.Restart 또는 RepeatMode.Reverse)val value by animateFloatAsState(
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
),
label = "infinite repeatable spec"
)
delayMillis: 시작 전 지연 시간val value by animateFloatAsState(
targetValue = 1f,
animationSpec = snap(delayMillis = 50),
label = "snap spec"
)
Compose의 다양한 API에서 AnimationSpec을 활용하여 동작을 커스터마이징할 수 있습니다.
AnimatedVisibility(
visible = isVisible,
enter = slideInVertically(
initialOffsetY = { fullHeight -> fullHeight },
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
),
exit = slideOutVertically(
targetOffsetY = { fullHeight -> fullHeight },
animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)
)
) {
Text("Hello!")
}
val transitionState by updateTransition(targetState).animateDp { state ->
when (state) {
State.Collapsed -> dp(100)
State.Expanded -> dp(300)
}
}.using(tween(durationMillis = 500))
tweenspringkeyframesrepeatable 또는 infiniteRepeatablesnap