Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[Devfest Songdo 2023] Compose Animation

강경완
December 10, 2023

[Devfest Songdo 2023] Compose Animation

Compose Animation에 대해 톺아봅니다.

강경완

December 10, 2023
Tweet

More Decks by 강경완

Other Decks in Programming

Transcript

  1. 시작하기에 앞서.. 1. View Animation? 2. Comopse Animation? 3. Animation

    을 선언형으로! 4. 매우 쉽고 간편한 Compose Animation
  2. 무엇을 바꿀 것인가?? 1. Scale (Size) 2. Position (Translation) 3.

    Rotate 4. Alpha 5. Value 1. Padding 2. Offset 3. Color 4. Shape 5. … (everything of that can change)
  3. 무엇을 바꿀 것인가?? 1. Scale (Size) 2. Position (Translation) 3.

    Rotate 4. Alpha 5. Value 1. Padding 2. Offset 3. Color 4. Shape 5. … (everything of that can change)
  4. 어떤 API 를 사용해야하지?? 1. AnimatedVisibility 2. AnimatedContent or Crossfade

    3. Transition 4. Animatable 5. Animate*AsState 6. Animation
  5. @Composable fun AnimatedVisibility( visibleState: MutableTransitionState<Boolean>, modifier: Modifier = Modifier, enter:

    EnterTransition = fadeIn() + expandIn(), exit: ExitTransition = fadeOut() + shrinkOut(), label: String = "AnimatedVisibility", content: @Composable() AnimatedVisibilityScope.() -> Unit ) { val transition = updateTransition(visibleState, label) AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content) } var visible by remember { mutableStateOf(true) } AnimatedVisibility( visible = visible, ) { Text("Hello") } AnimatedVisibility
  6. var visible by remember { mutableStateOf(true) } AnimatedContent( targetState =

    visible, label = "" ) { targetVisible -> if (targetVisible) { Text("Hello") } } AnimatedContent
  7. @Composable fun <S> AnimatedContent( targetState: S, modifier: Modifier = Modifier,

    transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = { (fadeIn(animationSpec = tween(220, delayMillis = 90)) + scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90))) .togetherWith(fadeOut(animationSpec = tween(90))) … ) { val transition = updateTransition(targetState = targetState) transition.AnimatedContent( modifier, transitionSpec, contentAlignment, contentKey, content = content ) } var visible by remember { mutableStateOf(true) } AnimatedContent( targetState = visible, label = "" ) { targetVisible -> if (targetVisible) { Text("Hello") } } AnimatedContent
  8. AnimatedVisible AnimatedContent AnimatedContent (조정) AnimatedContent( targetState = visible, label =

    "", transitionSpec = { fadeIn() + expandVertically() togetherWith fadeOut() + shrinkVertically() } ) { targetVisible -> if (targetVisible) { Text("Hello") } }
  9. @OptIn(ExperimentalAnimationApi::class) @Composable fun <S> Transition<S>.AnimatedContent() { … currentlyVisible.fastForEach { stateForContent

    -> contentMap[stateForContent] = { …. AnimatedVisibility({ it == stateForContent }) { DisposableEffect(this) { onDispose { currentlyVisible.remove(stateForContent) rootScope.targetSizeMap.remove(stateForContent) } } rootScope.targetSizeMap[stateForContent] = (this as AnimatedVisibilityScopeImpl).targetSize with(remember { AnimatedContentScopeImpl(this) }) { content(stateForContent) } } Layout( modifier = modifier.then(sizeModifier), content = { currentlyVisible.forEach { key(contentKey(it)) { contentMap[it]?.invoke() } } AnimatedContent @Composable fun <S> AnimatedContent( targetState: S, modifier: Modifier = Modifier, transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = { (fadeIn(animationSpec = tween(220, delayMillis = 90)) + scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90))) .togetherWith(fadeOut(animationSpec = tween(90))) … ) { val transition = updateTransition(targetState = targetState) transition.AnimatedContent( modifier, transitionSpec, contentAlignment, contentKey, content = content ) }
  10. @OptIn(ExperimentalAnimationApi::class) @Composable fun <S> Transition<S>.AnimatedContent() { … currentlyVisible.fastForEach { stateForContent

    -> contentMap[stateForContent] = { …. AnimatedVisibility({ it == stateForContent }) { DisposableEffect(this) { onDispose { currentlyVisible.remove(stateForContent) rootScope.targetSizeMap.remove(stateForContent) } } rootScope.targetSizeMap[stateForContent] = (this as AnimatedVisibilityScopeImpl).targetSize with(remember { AnimatedContentScopeImpl(this) }) { content(stateForContent) } } Layout( modifier = modifier.then(sizeModifier), content = { currentlyVisible.forEach { key(contentKey(it)) { contentMap[it]?.invoke() } } AnimatedContent @Composable fun <S> AnimatedContent( targetState: S, modifier: Modifier = Modifier, transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = { (fadeIn(animationSpec = tween(220, delayMillis = 90)) + scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90))) .togetherWith(fadeOut(animationSpec = tween(90))) … ) { val transition = updateTransition(targetState = targetState) transition.AnimatedContent( modifier, transitionSpec, contentAlignment, contentKey, content = content ) }
  11. @OptIn(ExperimentalAnimationApi::class) @Composable fun <S> Transition<S>.AnimatedContent() { … currentlyVisible.fastForEach { stateForContent

    -> contentMap[stateForContent] = { …. AnimatedVisibility({ it == stateForContent }) { DisposableEffect(this) { onDispose { currentlyVisible.remove(stateForContent) rootScope.targetSizeMap.remove(stateForContent) } } rootScope.targetSizeMap[stateForContent] = (this as AnimatedVisibilityScopeImpl).targetSize with(remember { AnimatedContentScopeImpl(this) }) { content(stateForContent) } } Layout( modifier = modifier.then(sizeModifier), content = { currentlyVisible.forEach { key(contentKey(it)) { contentMap[it]?.invoke() } } AnimatedContent @Composable fun <S> AnimatedContent( targetState: S, modifier: Modifier = Modifier, transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = { (fadeIn(animationSpec = tween(220, delayMillis = 90)) + scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90))) .togetherWith(fadeOut(animationSpec = tween(90))) … ) { val transition = updateTransition(targetState = targetState) transition.AnimatedContent( modifier, transitionSpec, contentAlignment, contentKey, content = content ) }
  12. var count by remember { mutableIntStateOf(0) } AnimatedContent( targetState =

    count, label = "", transitionSpec = { if (targetState > initialState) { slideInVertically { height -> height } + fadeIn() togetherWith slideOutVertically { height -> -height } + fadeOut() } else { slideInVertically { height -> -height } + fadeIn() togetherWith slideOutVertically { height -> height } + fadeOut() }.using( SizeTransform(clip = false) ) } ) { targetCount -> Text(text = "$targetCount") } AnimatedContent 1. Layout 자체에 거는 경우 2. (단순 등장/퇴장이 아닌) State 기반 Content transition
  13. AnimationSpecs spring() • dampingRatio ◦ HighBouncy (0.2) ◦ MediumBouncy (0.5)

    ◦ LowBouncy (0.75) ◦ NoBouncy (1) • stiffness ◦ High ◦ Medium ◦ MediumLow ◦ Low ◦ VeryLow
  14. AnimationSpecs tween() • 시작과 끝 값 간의 애니메이션 처리 •

    이징 곡선을 사용하여 처리 https://developer.android.com/reference/kotlin/androidx/compose/animation/core/package-summary
  15. Animate*AsState var colorChange by remember { mutableStateOf(true) } var dpChange

    by remember { mutableStateOf(true) } val animateColor by animateColorAsState(targetValue = if (colorChange) Color.Red else Color.Gray, animationSpec = tween(durationMillis = 500), label = "animateColor" ) val animateDp by animateDpAsState(targetValue = if (dpChange) 50.dp else 30.dp, animationSpec = tween(durationMillis = 500), label = "animateDp" ) Text("Hello", modifier = Modifier .height(animateDp) .background(animateColor))
  16. Start Layout 이 통채로 Animation 등장 / 퇴장 Animate*AsState Yes

    AnimatedContent AnimatedVisibility No Choose Animation API Yes No
  17. Animate*AsState @Composable fun <T, V : AnimationVector> animateValueAsState( … ):

    State<T> { val toolingOverride = remember { mutableStateOf<State<T>?>(null) } val animatable = remember { Animatable(targetValue, typeConverter, visibilityThreshold, label) } val channel = remember { Channel<T>(Channel.CONFLATED) } SideEffect { channel.trySend(targetValue) } LaunchedEffect(channel) { for (target in channel) { launch { if (newTarget != animatable.targetValue) { animatable.animateTo(newTarget, animSpec) } } } } return toolingOverride.value ?: animatable.asState() }
  18. Animate*AsState @Composable fun <T, V : AnimationVector> animateValueAsState( … ):

    State<T> { val toolingOverride = remember { mutableStateOf<State<T>?>(null) } val animatable = remember { Animatable(targetValue, typeConverter, visibilityThreshold, label) } val channel = remember { Channel<T>(Channel.CONFLATED) } SideEffect { channel.trySend(targetValue) } LaunchedEffect(channel) { for (target in channel) { launch { if (newTarget != animatable.targetValue) { animatable.animateTo(newTarget, animSpec) } } } } return toolingOverride.value ?: animatable.asState() }
  19. Animatable var colorChange by remember { mutableStateOf(true) } val color

    = remember { Animatable(Color.Gray) } LaunchedEffect(colorChange) { delay(500) color.animateTo(if (colorChange) Color.Red else Color.Green) } Text("Hello", modifier = Modifier .height(50.dp) .background(color.value))
  20. Animatable – 서로다른 animation spec var colorChange by remember {

    mutableStateOf(true) } val color = remember { Animatable(Color.Gray) } LaunchedEffect(buttonClick1) { color.animateTo( targetValue = if (colorChange) Color.Red else Color.Green, animationSpec = tween(durationMillis = 500) ) } LaunchedEffect(buttonClick2) { color.animateTo( targetValue = if (colorChange) Color.Red else Color.Green, animationSpec = tween(durationMillis = 100) ) }
  21. Animate*AsState vs Animatable Animating Object Animatable<T> Coroutine Scope animateTo animationSpec1

    targetValue1 animateTo 1 animationSpec1 targetValue1 animateTo 2 animationSpec2 Animating Object Animatable<T> targetValue2 SnapTo Animatable animate*AsState()
  22. Transition – state 에 따라 동시에 전환이 일어날 때 var

    currentState by remember { mutableStateOf(BoxState.Collapsed) } val transition = updateTransition(currentState) val offset by transition.animateDp(label = "") { state -> when (state) { BoxState.Collapsed -> 100.dp BoxState.Expanded -> 50.dp } } val borderWidth by transition.animateDp(label = "") { state -> when (state) { BoxState.Collapsed -> 1.dp BoxState.Expanded -> 0.dp } }
  23. Transition++ @Composable fun AnimatedVisibility( visibleState: MutableTransitionState<Boolean>, …. content: @Composable AnimatedVisibilityScope.()

    -> Unit ) { val transition = updateTransition(visibleState, label) AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content) } @Composable fun <S> AnimatedContent( targetState: S, …. content: @Composable() AnimatedContentScope.(targetState: S) -> Unit ) { val transition = updateTransition(targetState = targetState, label = label) transition.AnimatedContent( modifier, transitionSpec, contentAlignment, contentKey, content = content ) } AnimatedContent, AnimatedVisiblity 모두 Transition 의 확장함수 -> 함께 묶어서 사용 가능 AnimatedContentScope: AnimatedVisibilityScope 스코프 안에서도 transition 사용 가능
  24. Animatable vs Transition, spring vs tween Animatable: cancel 시 Tween

    을 존중 Transition: cancel 시 spring 처리 Cancel 시 흐름이 자유로운 spring 묵묵히 지정된대로 tween
  25. Let`s try 1. animateFloatAsState Dim fade in (alpha) 2. Transition

    Button slideVertically - animateDp Pannel fadeIn + slideVertically - AnimatedVisibility - fadeIn (with delay) - slideInVertically
  26. Let`s try val alpha by animateFloatAsState( targetValue = if (visible)

    0.4f else 0f, animationSpec = tween( durationMillis = 360, easing = LinearEasing, ), ) Box( modifier = Modifier .alpha(alpha) .background(Color.Black) .fillMaxSize() ) val transition = updateTransition(visible) val buttonOffset by transition.animateDp() Button( modifier = Modifier.offset(y = buttonOffset), } transition.AnimatedVisibility(visible = { it }, enter = fadeIn( animationSpec = tween( durationMillis = 360, delayMillis = 240, easing = LinearEasing, ) ) + slideInVertically( tween( durationMillis = 1200, ) ) { height -> height / 2 }
  27. Let`s try++ val translationY = remember { Animatable(0f) } val

    draggableState = rememberDraggableState(onDelta = {dragAmount -> coroutineScope.launch { translationY.snapTo(translationY.value + dragAmount) } })
  28. Let`s try++ val translationY = remember { Animatable(0f) } val

    draggableState = rememberDraggableState(…) Box.draggable(draggableState, Orientation.Vertical, onDragStopped = { velocity -> val decayY = decay.calculateTargetValue(translationY.value, velocity) coroutineScope.launch { if (decayY > 400) { visible = false } else { translationY.animateTo( // 임계치를 넘지 않으면 원복 targetValue = 0f, initialVelocity = velocity ) } } }) )