티스토리 뷰
Compose의 Animation은 간단하게 정리하면 다음과 같습니다.
어떤 애니메이션이 어떻게 동작시킨다.
어떤 애니메이션을 사용할지 여부는 Transition을 통해 지정하고, 이 Animation동작이 어떻게 작동할지는 AnimationSpec에 의해 정해집니다.
그전에 언제 애니메이션을 작동시킬 것인지가 필요합니다.
우선 간단하게 Visible 상태를 변경시키는 AnimatedVisibility에 대해 알아보겠습니다.
var visible by remember { mutableStateOf(false) }
Button(onClick = { visible = visible1.not() }) {
Text(text = "Click")
}
AnimatedVisibility(visible = visible) {
Box(modifier = Modifier.size(100.dp).background(Color.Cyan))
}
가장 기본적인 사용방법입니다.
필수 인자로 Boolean 타입으로 visible을 받게 되는데 이 값이 변경될 때 애니메이션이 동작되게 됩니다.
AnimatedVisibility(
visible = visible,
enter = slideInHorizontally() + fadeIn(),
exit = slideOutHorizontally() + fadeOut()
) {
Box(modifier = Modifier.size(100.dp).background(Color.Cyan))
}
enter와 exit의 값을 변경함으로써 애니메이션 동작을 변경할 수 있습니다.
언제 애니메이션이 작동되는지는 AnimatedVisibility와 같은 함수를 적절하게 이용하면 됩니다. 이와 관련해서는 애니메이션 동작을 먼저 다룬 후 작성하겠습니다.
Transition
Transition은 EnterTransition과 ExitTransition으로 구분되어집니다.
1) FadeIn() + FadeOut()
var state by remember { mutableStateOf(false) }
Button(onClick = { state = state.not() }) {
Text(text = "Fade Click")
}
AnimatedVisibility(
visible = state,
enter = fadeIn(),
exit = fadeOut()
) {
TransitionBox("FadeIn() + FadeOut()", Color(0xFFE53935))
}
투명도의 값이 변경되며 Visible이 변경됩니다.
2) SlideIn(), SlideOut
var state by remember { mutableStateOf(false) }
Button(onClick = { state = state.not() }) {
Text(text = "Slide Click")
}
AnimatedVisibility(
visible = state,
enter = slideIn(
tween(200, easing = LinearEasing)
) {
IntOffset(it.width / 2, it.height / 2)
},
exit = slideOut(
tween(200, easing = LinearEasing)
) {
IntOffset(it.width / 2, it.height / 2)
}
) {
TransitionBox("slideIn() + slideOut()", Color(0xFFD81B60))
}
우측 하단에서 좌측 상단 방향으로 들어왔다 반대로 나가는 애니메이션입니다.
initialOffset을 필수 값으로 넣어주어야 합니다.
initialOffset: (fullSize: IntSize) -> IntOffset
애니메이션의 content 크기를 제공해 주며 Offset 값을 통해 애니메이션의 동작 방향, 범위를 지정해 줄 수 있습니다.
방향을 바꾸기 위해서는 IntOffset의 값을 음수로 설정해 주면 됩니다.
3) slideInHorizontally() + slideOutHorizontally() / slideInVertically() + slideOutVertically()
var state by remember { mutableStateOf(false) }
Button(onClick = { state = state.not() }) {
Text(text = "Slide Horizontal Click")
}
AnimatedVisibility(
visible = state,
enter = slideInHorizontally (
tween(200, easing = LinearEasing)
) {
it / 2
},
exit = slideOutHorizontally (
tween(200, easing = LinearEasing)
) {
it / 2
}
) {
TransitionBox("slideInHorizontally() + slideOutHorizontally()", Color(0xFF8E24AA))
}
slideInHorizontally() + slideOutHorizontally()와 slideInVertically() + slideOutVertically()의 사용방법은 동일합니다.
그냥 slide의 경우 두 가지 방향이어서 IntOffset을 반환하였으나 Vertically와 Horizontally는 한 방향이므로 int값을 반환합니다.
4) scaleIn, scaleOut
var state by remember { mutableStateOf(false) }
Button(onClick = { state = state.not() }) {
Text(text = "Scale Click")
}
AnimatedVisibility(
visible = state,
enter = scaleIn(transformOrigin = TransformOrigin(0f, 0f)),
exit = scaleOut()
) {
TransitionBox("scaleIn() + scaleOut", Color(0xFF1E88E5))
}
scale은 특정 지점부터 시작해서 나타났다 사라지는 애니메이션입니다.
transformOrigine의 값을 설정해서 시작 또는 종료 위치를 바꿀 수 있습니다.
기본 값으로는 TransformOrigin.Center로 가운데를 기준으로 설정되어 있습니다.
5) expandIn, shrinkOut
var state by remember { mutableStateOf(false) }
Button(onClick = { state = state.not() }) {
Text(text = "Expand Click")
}
AnimatedVisibility(
visible = state,
enter = expandIn(expandFrom = Alignment.Center) { IntSize(100, 100) },
exit = shrinkOut(shrinkTowards = Alignment.BottomEnd) { IntSize(it.width / 2, it.height / 2) }
) {
TransitionBox("expandIn() + expandOut", Color(0xFF00897B))
}
expandIn과 shrinkOut은 유일하게 enter와 exit의 이름이 다릅니다.
expandFrom과 shrinkTowrads는 확장/축소할 때 어떤 지점을 중심으로 보여줄지를 나타냅니다.
initialSize는 확장 시에는 초기 크기, 축소 시에는 마지막 크기를 나타냅니다.
AnimationSpec
1) spring
AnimatedVisibility(
visible = state.value,
enter = fadeIn(),
exit = fadeOut()
) {
Box(
modifier = Modifier
.size(100.dp)
.background(Color(0xFFF4511E))
)
}
AnimatedVisibility(
visible = state.value,
enter = fadeIn(
animationSpec = spring(
dampingRatio = 1f,
stiffness = 1f
)
),
exit = fadeOut()
) {
Box(
modifier = Modifier
.size(100.dp)
.background(Color(0xFFFFB300))
)
}
왼쪽은 기본 FadeIn을 사용하였고 오른쪽은 FadeIn에 spring을 적용한 것입니다.
AnimatedVisibility(
visible = state.value,
enter = expandIn (
animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = 30f
)
),
exit = fadeOut()
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color(0xFFF4511E))
)
}
AnimatedVisibility(
visible = state.value,
enter = expandIn(
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = 30f
)
),
exit = fadeOut()
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color(0xFFFFB300))
)
}
AnimatedVisibility(
visible = state.value,
enter = expandIn(
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = 30f
)
),
exit = fadeOut()
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color(0xFF5E35B1))
)
}
AnimatedVisibility(
visible = state.value,
enter = expandIn(
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = 30f
)
),
exit = fadeOut()
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color(0xFF43A047))
)
}
dampingRatio는 Spring 효과가 얼마나 작동할지를 나타냅니다.
순서대로 DampingRatioNoBouncy, DampingRatioLowBouncy, DampingRatioMediumBouncy, DampingRatioHighBouncy
값으로는 1f, 0.75f, 0.5f, 0.2f입니다. 튕기는 정도를 float로 지정하여 디테일하게 설정이 가능합니다.
stiffness는 스프링 효과가 작동하는 속도로 StiffnessVeryLow, StiffnessLow, StiffnessMediumLow, StiffnessMedium, StiffnessHigh로 각각 50f, 200f, 400f, 1500, 10000f입니다. 역시 원하는 속도만큼 float로 지정해 주면 됩니다.
2) tween
AnimatedVisibility(
visible = state.value,
enter = fadeIn(),
exit = fadeOut()
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color(0xFFF4511E))
)
}
AnimatedVisibility(
visible = state.value,
enter = fadeIn(
animationSpec = tween(
durationMillis = 1000,
easing = LinearEasing
)
),
exit = fadeOut()
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color(0xFFFFB300))
)
}
tween은 일정 시간 동안 easing을 처리하는 애니메이션입니다.
easing은 특정 포물선에 따라서 가속도가 결정되는 애니메이션입니다.
easing의 경우 종류가 너무 많고 visible 효과로는 애니메이션을 구분하기 쉽지 않아서 링크남깁니다.
3) repeatable
AnimatedVisibility(
visible = state.value,
enter = fadeIn(),
exit = fadeOut()
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color(0xFFF4511E))
)
}
AnimatedVisibility(
visible = state.value,
enter = fadeIn(
animationSpec = repeatable(
iterations = 4,
animation = tween(durationMillis = 400, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
)
),
exit = fadeOut()
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color(0xFFFFB300))
)
}
repeatable은 tween이나 keyframes를 반복 수행하는 애니메이션입니다.
iterations로 반복 횟수를 설정하고 repeatMode를 설정하여 반복 동작의 방법을 선택합니다.
유형으로는 Restart와 Reverse가 있습니다.
repeatable 대신 InfiniteRepeatable를 사용하면 무한 반복하는 애니메이션을 설정할 수 있습니다.
4) snap
AnimatedVisibility(
visible = state.value,
enter = fadeIn(),
exit = fadeOut()
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color(0xFFF4511E))
)
}
AnimatedVisibility(
visible = state.value,
enter = fadeIn(
animationSpec = snap(delayMillis = 1000)
),
exit = fadeOut()
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color(0xFFFFB300))
)
}
snap은 즉시 완료되는 애니메이션으로 특정 시간 이후에 작동하게 설정이 가능합니다.
애니메이션 작동하는 방법
지금까지는 Visible을 변경하는 방법만 사용하였습니다.
그 외 애니메이션 작동시키는 방법에 대해 알아보겠습니다.
1) animate*AsState
Column(modifier = Modifier.fillMaxSize()) {
var state by remember { mutableStateOf(false) }
val alpha by animateFloatAsState(targetValue = if (state) 1f else 0.5f)
val size by animateDpAsState(targetValue = if (state) 100.dp else 50.dp)
val color by animateColorAsState(
targetValue = if (state) Color(0xFF2979FF) else Color(0xFFFF1744)
)
val number by animateIntAsState(targetValue = if (state) 100 else 50)
// animateIntOffsetAsState(targetValue = )
// animateIntSizeAsState(targetValue = )
// animateOffsetAsState(targetValue = )
// animateRectAsState(targetValue = )
// animateValueAsState(targetValue = , typeConverter = )
TextButton(text = "click") {
state = state.not()
}
Box(
modifier = Modifier
.size(size)
.alpha(alpha)
.background(color)
) {
Text(text = "$number", modifier = Modifier.align(Alignment.Center))
}
}
animate*AsState는 가장 간단한 애니메이션 사용 방법으로 기존 코드에서 if문을 통해 값을 바꿀 때 애니메이션 효과를 주고 싶다면 이 방법을 쓰면 편리하게 이용할 수 있습니다.
예시로 state의 값이 변경될 때 50과 100으로 변경되는 경우 한 번에 값이 변경되는 것이 아닌 일정 시간 동안 값을 증가시켜 최종 값으로 변경됩니다.
animationSpec을 설정하여 애니메이션 효과를 변경할 수 있습니다.
Float, Color, Dp, Size, Offset, Rect, Int, IntOffset, IntSize가 기본적으로 제공되며 animateValueAsState를 통해 원하는 유형을 추가할 수 있습니다.
2) AnimatedContent
Column(modifier = Modifier.fillMaxSize()) {
var state by remember { mutableStateOf(0) }
Row(Modifier.fillMaxWidth()) {
TextButton(text = "plus") {
state++
}
Spacer(modifier = Modifier.width(20.dp))
TextButton(text = "minus") {
state--
}
}
AnimatedContent(
targetState = state,
transitionSpec = {
if (targetState > initialState) {
enterAnimation() with exitAnimation()
} else{
enterAnimation(-1) with exitAnimation(-1)
}.using(SizeTransform(clip = false))
// enterAnimation() with exitAnimation() using SizeTransform(clip = false)
},
) { count ->
Box(
modifier = Modifier
.size(100.dp)
.background(Color(0xFF651FFF))
) {
Text(text = "$count", modifier = Modifier.align(Alignment.Center))
}
}
}
private fun enterAnimation(direction : Int = 1) =
slideIn(
tween(200, easing = LinearEasing)
) {
IntOffset(-it.width / 2 * direction, -it.height / 2 * direction)
} + fadeIn(animationSpec = tween(220, delayMillis = 90))
private fun exitAnimation(direction : Int = 1) =
slideOut(
tween(200, easing = LinearEasing)
) {
IntOffset(it.width / 2 * direction, it.height / 2 * direction)
} + fadeOut(animationSpec = tween(220, delayMillis = 90))
AnimatedContens는 아직 실험 단계의 API입니다.
targetState의 값이 변경될 때마다 하위 content가 transitionSpec에서 설정한 애니메이션 동작을 실행하게 됩니다.
transitionSpec은 다음과 같이 작성해 주어야 합니다.
EnterTransition with ExitTransition
추가로 using을 사용할 수 있는데 *SizeTransform*(clip = false)를 사용해 주어야 애니메이션 동작이 잘리지 않고 보이게 됩니다. (AnimatedContens의 content에서 여유 공간을 충분히 잡으면 안 잘리게 할 수는 있습니다.)
그리고 AnimationSpec을 추가하여 애니메이션을 관리할 수 있습니다.
3) Animatable
Box(
modifier = Modifier.fillMaxSize()
) {
val scale = remember { Animatable(initialValue = 1f) }
val color = remember { Animatable(initialValue = Color(0xFFF50057)) }
val coroutineScope = rememberCoroutineScope()
Box(
modifier = Modifier
.scale(scale.value)
.size(40.dp)
.align(Alignment.Center)
.background(color.value)
.clickable {
coroutineScope.launch {
scale.animateTo(
targetValue = 4f,
animationSpec = tween(1000)
)
}
coroutineScope.launch {
color.animateTo(
targetValue = Color(0xFFFFC400),
animationSpec = repeatable(
iterations = 3,
animation = tween(durationMillis = 500),
repeatMode = RepeatMode.Reverse
)
)
}
}
)
}
이 방식은 Coroutine에 의해 동작이 되며 Animatable을 이용하여 초기깃값을 변수에 저장하고 애니메이션 동작 시에 값을 설정할 수 있다는 특징이 있습니다.
float와 color를 기본적으로 제공해줍니다.
coroutineScope.launch {
scale.animateTo(
targetValue = 4f,
animationSpec = tween(1000)
)
color.animateTo(
targetValue = Color(0xFFFFC400),
animationSpec = repeatable(
iterations = 3,
animation = tween(durationMillis = 500),
repeatMode = RepeatMode.Reverse
)
)
}
위와 같이 하나의 launch 안에서 동작을 시키면 첫 번째 애니메이션 동작 이후 두 번째 애니메이션이 작동됩니다.
4) animateContentSize
val state = remember { mutableStateOf(false) }
val text = if (state.value) "Lorem Ipsum ..." else "before"
TextButton(
text = text,
modifier = Modifier.animateContentSize()
) {
state.value = state.value.not()
}
animateContentSize는 Modifier에서 사용할 수 있는 애니메이션 효과로 content의 사이즈가 변경될 때 작동하는 애니메이션입니다.
사용 시 주의할 점은 animateContentSize를 사용하기 전 Modifier 설정 중 UI 변동 시 영향을 줄 수 있는 요소들은 animateContentSize 이전에 사용해 주어야합니다.
5) Crossfade
val state = remember { mutableStateOf("A") }
TextButton(text = "Click") {
state.value = if (state.value == "A") "B" else "A"
}
Crossfade(targetState = state.value) {
when (it) {
"A" -> {
Box(
modifier = Modifier
.size(50.dp)
.background(Color(0xFFFF9100))
) {
Text(text = "A", modifier = Modifier.align(Alignment.Center))
}
}
"B" -> {
Box(
modifier = Modifier
.size(50.dp)
.background(Color(0xFF00E676))
) {
Text(text = "B", modifier = Modifier.align(Alignment.Center))
}
}
}
}
Crossfade는 content 전환을 위한 애니메이션입니다.
여기에 작성된 애니메이션 이외에 keyFrames나 updateTransition 등이 있으나 이 부분은 응용 부분이라서 제외하였습니다.
'안드로이드 > 코드' 카테고리의 다른 글
Compose 기초 4 : Gradient (0) | 2022.09.01 |
---|---|
Compose 기초 3 : ViewPager (0) | 2022.08.30 |
포켓몬 도감 만들기(3) : Fast Api, Compose, 상세 화면 (0) | 2022.08.21 |
포켓몬 도감 만들기(2) : Fast Api, Compose, 홈 화면, 리스트 화면 (0) | 2022.08.21 |
포켓몬 도감 만들기(1) : Fast Api (0) | 2022.08.21 |
- Total
- Today
- Yesterday
- Android Compose
- Gradient
- Compose BottomSheetScaffold
- Duplicate class fond 에러
- Duplicate class found error
- WorkManager
- WebView
- 안드로이드 구글 지도
- 포켓몬 도감
- Compose MotionLayout
- compose
- Compose 네이버 지도 api
- 안드로이드
- Compose BottomSheetDialog
- Compose BottomSheet
- Compose ModalBottomSheetLayout
- LazyColumn
- column
- Android
- Retrofit
- Compose QRCode Scanner
- 웹뷰
- Worker
- Kotlin
- Compose 네이버 지도
- Fast api
- Row
- Compose Naver Map
- Compose ConstraintLayout
- Pokedex
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |