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

Material Motion for Compose

Material Motion for Compose

2024년 07월 27일 (토) I/O Extended 2024 Incheon 2024 발표자료입니다.
https://festa.io/events/5477

Sungyong An

July 27, 2024
Tweet

More Decks by Sungyong An

Other Decks in Programming

Transcript

  1. enum class BottomTabs { Albums, Photos, Search } val (selectedTab,

    setSelectedTab) = remember { mutableStateOf(BottomTabs.Albums) } Scaffold(bottomBar = { BottomNavigation { BottomTabs.values().forEach { tab -> BottomNavigationItem( ... selected = tab == selectedTab, onClick = { setSelectedTab(tab) } ) } } }) { innerPadding -> BottomTabsContents(selectedTab, ...) } Example #1
  2. enum class BottomTabs { Albums, Photos, Search } val (selectedTab,

    setSelectedTab) = remember { mutableStateOf(BottomTabs.Albums) } Scaffold(bottomBar = { BottomNavigation { BottomTabs.values().forEach { tab -> BottomNavigationItem( ... selected = tab == selectedTab, onClick = { setSelectedTab(tab) } ) } } }) { innerPadding -> BottomTabsContents(selectedTab, ...) } Example #1
  3. enum class BottomTabs { Albums, Photos, Search } val (selectedTab,

    setSelectedTab) = remember { mutableStateOf(BottomTabs.Albums) } Scaffold(bottomBar = { BottomNavigation { BottomTabs.values().forEach { tab -> BottomNavigationItem( ... selected = tab == selectedTab, onClick = { setSelectedTab(tab) } ) } } }) { innerPadding -> BottomTabsContents(selectedTab, ...) } Example #1
  4. Scaffold(bottomBar = { ... }) { innerPadding -> Crossfade( targetState

    = selectedTab, modifier = Modifier.padding(innerPadding) ) { currentTab -> BottomTabsContents(currentTab) } } Crossfade
  5. Scaffold(bottomBar = { ... }) { innerPadding -> Crossfade( targetState

    = selectedTab, modifier = Modifier.padding(innerPadding) ) { currentTab -> BottomTabsContents(currentTab) } } Crossfade
  6. Compose 1.6 @Composable fun <T> Crossfade( targetState: T, modifier: Modifier

    = Modifier, animationSpec: FiniteAnimationSpec<Float> = tween(), label: String = "Crossfade", content: @Composable (T) -> Unit ) { val transition = updateTransition(targetState, label) transition.Crossfade(modifier, animationSpec, content = content) }
  7. Compose 1.6 @Composable fun <T> Crossfade( targetState: T, modifier: Modifier

    = Modifier, animationSpec: FiniteAnimationSpec<Float> = tween(), label: String = "Crossfade", content: @Composable (T) -> Unit ) { val transition = updateTransition(targetState, label) transition.Crossfade(modifier, animationSpec, content = content) }
  8. Compose 1.6 @Composable fun <T> Crossfade( targetState: T, modifier: Modifier

    = Modifier, animationSpec: FiniteAnimationSpec<Float> = tween(), label: String = "Crossfade", content: @Composable (T) -> Unit ) { val transition = updateTransition(targetState, label) transition.Crossfade(modifier, animationSpec, content = content) }
  9. Compose 1.6 @Composable fun <T> Crossfade( targetState: T, modifier: Modifier

    = Modifier, animationSpec: FiniteAnimationSpec<Float> = tween(), label: String = "Crossfade", content: @Composable (T) -> Unit ) { val transition = updateTransition(targetState, label) transition.Crossfade(modifier, animationSpec, content = content) }
  10. Compose 1.6 @Composable fun <T> Crossfade( targetState: T, modifier: Modifier

    = Modifier, animationSpec: FiniteAnimationSpec<Float> = tween(), label: String = "Crossfade", content: @Composable (T) -> Unit ) { val transition = updateTransition(targetState, label) transition.Crossfade(modifier, animationSpec, content = content) }
  11. Compose 1.6 @Composable fun <T> Transition<T>.Crossfade( modifier: Modifier = Modifier,

    animationSpec: FiniteAnimationSpec<Float> = tween(), contentKey: (targetState: T) -> Any? = { it }, content: @Composable (targetState: T) -> Unit ) { val currentlyVisible = remember { mutableStateListOf<T>().apply { add(currentState) } } val contentMap = remember { mutableMapOf<T, @Composable () -> Unit>() } if (currentState == targetState) { // If not animating, just display the current state if (currentlyVisible.size != 1 || currentlyVisible[0] != targetState) { // Remove all the intermediate items from the list once the animation is finished. currentlyVisible.removeAll { it != targetState } contentMap.clear() } }
  12. @Composable fun <T> Transition<T>.Crossfade( modifier: Modifier = Modifier, animationSpec: FiniteAnimationSpec<Float>

    = tween(), contentKey: (targetState: T) -> Any? = { it }, content: @Composable (targetState: T) -> Unit ) { val currentlyVisible = remember { mutableStateListOf<T>().apply { add(currentState) } } val contentMap = remember { mutableMapOf<T, @Composable () -> Unit>() } if (currentState == targetState) { // If not animating, just display the current state if (currentlyVisible.size != 1 || currentlyVisible[0] != targetState) { // Remove all the intermediate items from the list once the animation is finished. currentlyVisible.removeAll { it != targetState } contentMap.clear() } } Compose 1.6
  13. val currentlyVisible = remember { mutableStateListOf<T>().apply { add(currentState) } }

    val contentMap = remember { mutableMapOf<T, @Composable () -> Unit>() } if (currentState == targetState) { // If not animating, just display the current state if (currentlyVisible.size != 1 || currentlyVisible[0] != targetState) { // Remove all the intermediate items from the list once the animation is finished. currentlyVisible.removeAll { it != targetState } contentMap.clear() } } if (!contentMap.contains(targetState)) { // Replace target with the same key if any val replacementId = currentlyVisible.indexOfFirst { contentKey(it) == contentKey(targetState) } if (replacementId == -1) { currentlyVisible.add(targetState) } else { currentlyVisible[replacementId] = targetState } contentMap.clear() Compose 1.6
  14. } if (replacementId == -1) { currentlyVisible.add(targetState) } else {

    currentlyVisible[replacementId] = targetState } contentMap.clear() currentlyVisible.fastForEach { stateForContent -> contentMap[stateForContent] = { val alpha by animateFloat( transitionSpec = { animationSpec } ) { if (it == stateForContent) 1f else 0f } Box(Modifier.graphicsLayer { this.alpha = alpha }) { content(stateForContent) } } } } Box(modifier) { currentlyVisible.fastForEach { key(contentKey(it)) { contentMap[it]?.invoke() Compose 1.6
  15. } if (replacementId == -1) { currentlyVisible.add(targetState) } else {

    currentlyVisible[replacementId] = targetState } contentMap.clear() currentlyVisible.fastForEach { stateForContent -> contentMap[stateForContent] = { val alpha by animateFloat( transitionSpec = { animationSpec } ) { if (it == stateForContent) 1f else 0f } Box(Modifier.graphicsLayer { this.alpha = alpha }) { content(stateForContent) } } } } Box(modifier) { currentlyVisible.fastForEach { key(contentKey(it)) { contentMap[it]?.invoke() Compose 1.6
  16. } if (replacementId == -1) { currentlyVisible.add(targetState) } else {

    currentlyVisible[replacementId] = targetState } contentMap.clear() currentlyVisible.fastForEach { stateForContent -> contentMap[stateForContent] = { val alpha by animateFloat( transitionSpec = { animationSpec } ) { if (it == stateForContent) 1f else 0f } Box(Modifier.graphicsLayer { this.alpha = alpha }) { content(stateForContent) } } } } Box(modifier) { currentlyVisible.fastForEach { key(contentKey(it)) { contentMap[it]?.invoke() Compose 1.6
  17. contentMap[stateForContent] = { val alpha by animateFloat( transitionSpec = {

    animationSpec } ) { if (it == stateForContent) 1f else 0f } Box(Modifier.graphicsLayer { this.alpha = alpha }) { content(stateForContent) } } } } Box(modifier) { currentlyVisible.fastForEach { key(contentKey(it)) { contentMap[it]?.invoke() } } } } Compose 1.6
  18. contentMap[stateForContent] = { val alpha by animateFloat( transitionSpec = {

    animationSpec } ) { if (it == stateForContent) 1f else 0f } Box(Modifier.graphicsLayer { this.alpha = alpha }) { content(stateForContent) } } } } Box(modifier) { currentlyVisible.fastForEach { key(contentKey(it)) { contentMap[it]?.invoke() } } } } Compose 1.6 복잡해보이지만,간단합니다. Box(modifier) { Box(Modifier.graphicsLayer { alpha = 1f -> 0f }) { BottomTabsContents(previousTab) } Box(Modifier.graphicsLayer { alpha = 0f -> 1f }) { BottomTabsContents(currentTab) } }
  19. Crossfade ... contentMap[stateForContent] = { val alpha by animateFloat( transitionSpec

    = { animationSpec } ) { if (it == stateForContent) 1f else 0f } Box(Modifier.graphicsLayer { this.alpha = alpha }) { content(stateForContent) } } ...
  20. ... contentMap[stateForContent] = { val scale by animateFloat( transitionSpec =

    { animationSpec } ) { if (it == stateForContent) 1f else 0.92f } val alpha by animateFloat( transitionSpec = { animationSpec } ) { if (it == stateForContent) 1f else 0f } Box(Modifier.graphicsLayer { this.alpha = alpha this.scale = scale }) { content(stateForContent) } } ... Fade Through
  21. ... contentMap[stateForContent] = { val scale by animateFloat( transitionSpec =

    { animationSpec } ) { if (it == stateForContent) 1f else 0.92f } val alpha by animateFloat( transitionSpec = { animationSpec } ) { if (it == stateForContent) 1f else 0f } Box(Modifier.graphicsLayer { this.alpha = alpha this.scale = scale }) { content(stateForContent) } } ... Fade Through
  22. Scaffold(bottomBar = { ... }) { innerPadding -> MaterialFadeThrough( targetState

    = selectedTab, modifier = Modifier.padding(innerPadding) ) { currentTab -> BottomTabsContents(currentTab) } } Fade Through Box(modifier) { Box(Modifier.graphicsLayer { alpha = 1f -> 0f, scale = 1f }) { BottomTabsContents(previousTab) } Box(Modifier.graphicsLayer { alpha = 0f -> 1f, scale = 0.92f -> 1f }) { BottomTabsContents(currentTab) } }
  23. Example #2 @Composable fun LibraryContents(state: LibraryState, ...) { Surface(modifier =

    Modifier.fillMaxSize()) { val sortType = state.sortType val items = remember(sortType) { when (sortType) { SortType.A_TO_Z -> MusicData.albums SortType.Z_TO_A -> MusicData.albums.asReversed() } } when (state.listType) { ListType.Grid -> LibraryGridContents(items, ...) ListType.Linear -> LibraryLinearContents(items, ...) } } }
  24. Example #2 @Composable fun LibraryScreen(...) { val (state, onStateChanged) =

    remember { mutableStateOf(LibraryState(SortType.A_TO_Z, ListType.Grid)) } Scaffold(...) { LibraryContents(state, ...) } }
  25. @Composable fun LibraryScreen(...) { val (state, onStateChanged) = remember {

    mutableStateOf(LibraryState(SortType.A_TO_Z, ListType.Grid)) } Scaffold(...) { AnimatedContent( targetState = state, transitionSpec = { ... }, modifier = Modifier.padding(innerPadding), ) { currentDestination -> LibraryContents(state, ...) } } } AnimatedContent
  26. @Composable fun <S> AnimatedContent( targetState: S, modifier: Modifier = Modifier,

    transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = { ... }, contentAlignment: Alignment = Alignment.TopStart, label: String = "AnimatedContent", contentKey: (targetState: S) -> Any? = { it }, content: @Composable AnimatedContentScope.(targetState: S) -> Unit ) { val transition = updateTransition(targetState = targetState, label = label) transition.AnimatedContent( modifier, transitionSpec, contentAlignment, contentKey, content = content ) } Compose 1.6
  27. @Composable fun <S> AnimatedContent( targetState: S, modifier: Modifier = Modifier,

    transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = { ... }, contentAlignment: Alignment = Alignment.TopStart, label: String = "AnimatedContent", contentKey: (targetState: S) -> Any? = { it }, content: @Composable AnimatedContentScope.(targetState: S) -> Unit ) { val transition = updateTransition(targetState = targetState, label = label) transition.AnimatedContent( modifier, transitionSpec, contentAlignment, contentKey, content = content ) } Compose 1.6
  28. @Composable fun <S> AnimatedContent( targetState: S, modifier: Modifier = Modifier,

    transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = { ... }, contentAlignment: Alignment = Alignment.TopStart, label: String = "AnimatedContent", contentKey: (targetState: S) -> Any? = { it }, content: @Composable AnimatedContentScope.(targetState: S) -> Unit ) { val transition = updateTransition(targetState = targetState, label = label) transition.AnimatedContent( modifier, transitionSpec, contentAlignment, contentKey, content = content ) } Compose 1.6
  29. @Composable fun <S> AnimatedContent( targetState: S, modifier: Modifier = Modifier,

    transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = { ... }, contentAlignment: Alignment = Alignment.TopStart, label: String = "AnimatedContent", contentKey: (targetState: S) -> Any? = { it }, content: @Composable AnimatedContentScope.(targetState: S) -> Unit ) { val transition = updateTransition(targetState = targetState, label = label) transition.AnimatedContent( modifier, transitionSpec, contentAlignment, contentKey, content = content ) } Compose 1.6
  30. @Composable fun <S> AnimatedContent( targetState: S, modifier: Modifier = Modifier,

    transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = { ... }, contentAlignment: Alignment = Alignment.TopStart, label: String = "AnimatedContent", contentKey: (targetState: S) -> Any? = { it }, content: @Composable AnimatedContentScope.(targetState: S) -> Unit ) { val transition = updateTransition(targetState = targetState, label = label) transition.AnimatedContent( modifier, transitionSpec, contentAlignment, contentKey, content = content ) } Compose 1.6
  31. @Composable fun <S> Transition<S>.AnimatedContent( modifier: Modifier = Modifier, transitionSpec: AnimatedContentTransitionScope<S>.()

    -> ContentTransform = { ... }, contentAlignment: Alignment = Alignment.TopStart, contentKey: (targetState: S) -> Any? = { it }, content: @Composable AnimatedContentScope.(targetState: S) -> Unit ) { val layoutDirection = LocalLayoutDirection.current val rootScope = remember(this) { AnimatedContentTransitionScopeImpl(this, contentAlignment, layoutDirection) } // TODO: remove screen as soon as they are animated out val currentlyVisible = remember(this) { mutableStateListOf(currentState) } val contentMap = remember(this) { mutableMapOf<S, @Composable() () -> Unit>() } // This is needed for tooling because it could change currentState directly, // as opposed to changing target only. When that happens we need to clear all the // visible content and only display the content for the new current state and target state. if (!currentlyVisible.contains(currentState)) { Compose 1.6
  32. contentKey: (targetState: S) -> Any? = { it }, content:

    @Composable AnimatedContentScope.(targetState: S) -> Unit ) { val layoutDirection = LocalLayoutDirection.current val rootScope = remember(this) { AnimatedContentTransitionScopeImpl(this, contentAlignment, layoutDirection) } // TODO: remove screen as soon as they are animated out val currentlyVisible = remember(this) { mutableStateListOf(currentState) } val contentMap = remember(this) { mutableMapOf<S, @Composable() () -> Unit>() } // This is needed for tooling because it could change currentState directly, // as opposed to changing target only. When that happens we need to clear all the // visible content and only display the content for the new current state and target state. if (!currentlyVisible.contains(currentState)) { currentlyVisible.clear() currentlyVisible.add(currentState) } if (currentState == targetState) { if (currentlyVisible.size != 1 || currentlyVisible[0] != currentState) { currentlyVisible.clear() currentlyVisible.add(currentState) } if (contentMap.size != 1 || contentMap.containsKey(currentState)) { Compose 1.6
  33. if (currentState != targetState && !currentlyVisible.contains(targetState)) { // Replace the

    target with the same key if any val id = currentlyVisible.indexOfFirst { contentKey(it) == contentKey(targetState) } if (id == -1) { currentlyVisible.add(targetState) } else { currentlyVisible[id] = targetState } } if (!contentMap.containsKey(targetState) || !contentMap.containsKey(currentState)) { contentMap.clear() currentlyVisible.fastForEach { stateForContent -> contentMap[stateForContent] = { val specOnEnter = remember { transitionSpec(rootScope) } // NOTE: enter and exit for this AnimatedVisibility will be using different spec, // naturally. val exit = remember(segment.targetState == stateForContent) { if (segment.targetState == stateForContent) { ExitTransition.None } else { rootScope.transitionSpec().initialContentExit } Compose 1.6
  34. if (currentState != targetState && !currentlyVisible.contains(targetState)) { // Replace the

    target with the same key if any val id = currentlyVisible.indexOfFirst { contentKey(it) == contentKey(targetState) } if (id == -1) { currentlyVisible.add(targetState) } else { currentlyVisible[id] = targetState } } if (!contentMap.containsKey(targetState) || !contentMap.containsKey(currentState)) { contentMap.clear() currentlyVisible.fastForEach { stateForContent -> contentMap[stateForContent] = { val specOnEnter = remember { transitionSpec(rootScope) } // NOTE: enter and exit for this AnimatedVisibility will be using different spec, // naturally. val exit = remember(segment.targetState == stateForContent) { if (segment.targetState == stateForContent) { ExitTransition.None } else { rootScope.transitionSpec().initialContentExit } Compose 1.6
  35. if (currentState != targetState && !currentlyVisible.contains(targetState)) { // Replace the

    target with the same key if any val id = currentlyVisible.indexOfFirst { contentKey(it) == contentKey(targetState) } if (id == -1) { currentlyVisible.add(targetState) } else { currentlyVisible[id] = targetState } } if (!contentMap.containsKey(targetState) || !contentMap.containsKey(currentState)) { contentMap.clear() currentlyVisible.fastForEach { stateForContent -> contentMap[stateForContent] = { val specOnEnter = remember { transitionSpec(rootScope) } // NOTE: enter and exit for this AnimatedVisibility will be using different spec, // naturally. val exit = remember(segment.targetState == stateForContent) { if (segment.targetState == stateForContent) { ExitTransition.None } else { rootScope.transitionSpec().initialContentExit } Compose 1.6
  36. } else { rootScope.transitionSpec().initialContentExit } } val childData = remember

    { AnimatedContentTransitionScopeImpl.ChildData(stateForContent == targetState) } // TODO: Will need a custom impl of this to: 1) get the signal for when // the animation is finished, 2) get the target size properly AnimatedEnterExitImpl( this, { it == stateForContent }, enter = specOnEnter.targetContentEnter, exit = exit, modifier = Modifier .layout { measurable, constraints -> val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { placeable.place(0, 0, zIndex = specOnEnter.targetContentZIndex) } } .then(childData.apply { isTarget = stateForContent == targetState }), shouldDisposeBlock = { currentState, targetState -> Compose 1.6
  37. !exit.data.hold } ) { // TODO: Should Transition.AnimatedVisibility have an

    end listener? DisposableEffect(this) { onDispose { currentlyVisible.remove(stateForContent) rootScope.targetSizeMap.remove(stateForContent) } } rootScope.targetSizeMap[stateForContent] = (this as AnimatedVisibilityScopeImpl).targetSize with(remember { AnimatedContentScopeImpl(this) }) { content(stateForContent) } } } } } val contentTransform = remember(rootScope, segment) { transitionSpec(rootScope) } val sizeModifier = rootScope.createSizeAnimationModifier(contentTransform) Layout( modifier = modifier.then(sizeModifier), Compose 1.6
  38. with(remember { AnimatedContentScopeImpl(this) }) { content(stateForContent) } } } }

    } val contentTransform = remember(rootScope, segment) { transitionSpec(rootScope) } val sizeModifier = rootScope.createSizeAnimationModifier(contentTransform) Layout( modifier = modifier.then(sizeModifier), content = { currentlyVisible.fastForEach { key(contentKey(it)) { contentMap[it]?.invoke() } } }, measurePolicy = remember { AnimatedContentMeasurePolicy(rootScope) } ) } Compose 1.6
  39. with(remember { AnimatedContentScopeImpl(this) }) { content(stateForContent) } } } }

    } val contentTransform = remember(rootScope, segment) { transitionSpec(rootScope) } val sizeModifier = rootScope.createSizeAnimationModifier(contentTransform) Layout( modifier = modifier.then(sizeModifier), content = { currentlyVisible.fastForEach { key(contentKey(it)) { contentMap[it]?.invoke() } } }, measurePolicy = remember { AnimatedContentMeasurePolicy(rootScope) } ) } Compose 1.6 Layout(modifier) { AnimatedEnterExitImpl(exitTransition) { LibraryContents(previousState) } AnimatedEnterExitImpl(enterTransition) { LibraryContents(currentState) } }
  40. @Composable fun LibraryScreen(...) { val (state, onStateChanged) = remember {

    mutableStateOf(LibraryState(SortType.A_TO_Z, ListType.Grid)) } Scaffold(...) { val slideDistance = rememberSlideDistance() AnimatedContent( targetState = state, transitionSpec = { if (initialState.listType != targetState.listType) { materialFadeThrough() } else { materialSharedAxisY(forward = true, slideDistance) } }, modifier = Modifier.padding(innerPadding), ) { currentDestination -> LibraryContents(state, ...) } } } Enter/Exit Transition
  41. @Composable fun <S> AnimatedContent( targetState: S, modifier: Modifier = Modifier,

    transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = { ... }, contentAlignment: Alignment = Alignment.TopStart, label: String = "AnimatedContent", contentKey: (targetState: S) -> Any? = { it }, content: @Composable AnimatedContentScope.(targetState: S) -> Unit ) { val transition = updateTransition(targetState = targetState, label = label) transition.AnimatedContent( modifier, transitionSpec, contentAlignment, contentKey, content = content ) } Compose 1.6
  42. class ContentTransform( val targetContentEnter: EnterTransition, val initialContentExit: ExitTransition, targetContentZIndex: Float

    = 0f, sizeTransform: SizeTransform? = SizeTransform() ) { var targetContentZIndex by mutableFloatStateOf(targetContentZIndex) var sizeTransform: SizeTransform? = sizeTransform internal set } Compose 1.6
  43. class ContentTransform( val targetContentEnter: EnterTransition, val initialContentExit: ExitTransition, targetContentZIndex: Float

    = 0f, sizeTransform: SizeTransform? = SizeTransform() ) { var targetContentZIndex by mutableFloatStateOf(targetContentZIndex) var sizeTransform: SizeTransform? = sizeTransform internal set } Compose 1.6
  44. @Immutable sealed class EnterTransition { internal abstract val data: TransitionData

    } @Immutable sealed class ExitTransition { internal abstract val data: TransitionData } @Immutable internal data class TransitionData( val fade: Fade? = null, val slide: Slide? = null, val changeSize: ChangeSize? = null, val scale: Scale? = null, val hold: Boolean = false, val effectsMap: Map<TransitionEffectKey<*>, TransitionEffect> = emptyMap() ) Compose 1.6
  45. @Immutable sealed class EnterTransition { internal abstract val data: TransitionData

    } @Immutable sealed class ExitTransition { internal abstract val data: TransitionData } @Immutable internal data class TransitionData( val fade: Fade? = null, val slide: Slide? = null, val changeSize: ChangeSize? = null, val scale: Scale? = null, val hold: Boolean = false, val effectsMap: Map<TransitionEffectKey<*>, TransitionEffect> = emptyMap() ) Compose 1.6
  46. @Composable fun LibraryScreen(...) { val (state, onStateChanged) = remember {

    mutableStateOf(LibraryState(SortType.A_TO_Z, ListType.Grid)) } Scaffold(...) { val slideDistance = rememberSlideDistance() AnimatedContent( targetState = state, transitionSpec = { if (initialState.listType != targetState.listType) { materialFadeThrough() } else { materialSharedAxisY(forward = true, slideDistance) } }, modifier = Modifier.padding(innerPadding), ) { currentDestination -> LibraryContents(state, ...) } } } Fade Through
  47. public fun materialFadeThrough( durationMillis: Int = 300, ): ContentTransform =

    materialFadeThroughIn( durationMillis = durationMillis, ) togetherWith materialFadeThroughOut( durationMillis = durationMillis, ) Fade Through
  48. public fun materialFadeThrough( durationMillis: Int = 300, ): ContentTransform =

    materialFadeThroughIn( durationMillis = durationMillis, ) togetherWith materialFadeThroughOut( durationMillis = durationMillis, ) Fade Through
  49. public fun materialFadeThrough( durationMillis: Int = 300, ): ContentTransform =

    materialFadeThroughIn( durationMillis = durationMillis, ) togetherWith materialFadeThroughOut( durationMillis = durationMillis, ) infix fun EnterTransition.togetherWith(exit: ExitTransition) = ContentTransform(this, exit) Fade Through
  50. Fade Through public fun materialFadeThroughOut( durationMillis: Int = 300, ):

    ExitTransition = fadeOut( animationSpec = tween( durationMillis = durationMillis.ForOutgoing, delayMillis = 0, easing = FastOutLinearInEasing, ), )
  51. Fade Through public fun materialFadeThroughIn( initialScale: Float = 0.92f, durationMillis:

    Int = 300, ): EnterTransition = fadeIn( animationSpec = tween( delayMillis = durationMillis.ForOutgoing, durationMillis = durationMillis.ForIncoming, easing = LinearOutSlowInEasing, ), ) + scaleIn( animationSpec = tween( delayMillis = durationMillis.ForOutgoing, durationMillis = durationMillis.ForIncoming, easing = LinearOutSlowInEasing, ), initialScale = initialScale, )
  52. Fade Through public fun materialFadeThroughIn( initialScale: Float = 0.92f, durationMillis:

    Int = 300, ): EnterTransition = fadeIn( animationSpec = tween( delayMillis = durationMillis.ForOutgoing, durationMillis = durationMillis.ForIncoming, easing = LinearOutSlowInEasing, ), ) + scaleIn( animationSpec = tween( delayMillis = durationMillis.ForOutgoing, durationMillis = durationMillis.ForIncoming, easing = LinearOutSlowInEasing, ), initialScale = initialScale, )
  53. @Composable fun LibraryScreen(...) { val (state, onStateChanged) = remember {

    mutableStateOf(LibraryState(SortType.A_TO_Z, ListType.Grid)) } Scaffold(...) { val slideDistance = rememberSlideDistance() AnimatedContent( targetState = state, transitionSpec = { if (initialState.listType != targetState.listType) { materialFadeThrough() } else { materialSharedAxisY(forward = true, slideDistance) } }, modifier = Modifier.padding(innerPadding), ) { currentDestination -> LibraryContents(state, ...) } } } Shared Axis
  54. public fun materialSharedAxisY ( forward: Boolean, slideDistance: Int, durationMillis: Int

    = 300, ): ContentTransform = materialSharedAxisYIn( forward = forward, slideDistance = slideDistance, durationMillis = durationMillis, ) togetherWith materialSharedAxisYOut( forward = forward, slideDistance = slideDistance, durationMillis = durationMillis, ) Shared Axis
  55. public fun materialSharedAxisY ( forward: Boolean, slideDistance: Int, durationMillis: Int

    = 300, ): ContentTransform = materialSharedAxisYIn( forward = forward, slideDistance = slideDistance, durationMillis = durationMillis, ) togetherWith materialSharedAxisYOut( forward = forward, slideDistance = slideDistance, durationMillis = durationMillis, ) Shared Axis
  56. public fun materialSharedAxisYOut( forward: Boolean, slideDistance: Int, durationMillis: Int =

    300, ): ExitTransition = slideOutVertically( animationSpec = tween( durationMillis = durationMillis, easing = FastOutSlowInEasing, ), targetOffsetY = { if (forward) -slideDistance else slideDistance }, ) + fadeOut( animationSpec = tween( durationMillis = durationMillis.ForOutgoing, delayMillis = 0, easing = FastOutLinearInEasing, ), ) Shared Axis
  57. public fun materialSharedAxisYIn( forward: Boolean, slideDistance: Int, durationMillis: Int =

    300, ): EnterTransition = slideInVertically( animationSpec = tween( durationMillis = durationMillis, easing = FastOutSlowInEasing, ), initialOffsetY = { if (forward) slideDistance else -slideDistance }, ) + fadeIn( animationSpec = tween( durationMillis = durationMillis.ForIncoming, delayMillis = durationMillis.ForOutgoing, easing = LinearOutSlowInEasing, ), ) Shared Axis
  58. public fun materialSharedAxisYIn( forward: Boolean, slideDistance: Int, durationMillis: Int =

    300, ): EnterTransition = slideInVertically( animationSpec = tween( durationMillis = durationMillis, easing = FastOutSlowInEasing, ), initialOffsetY = { if (forward) slideDistance else -slideDistance }, ) + fadeIn( animationSpec = tween( durationMillis = durationMillis.ForIncoming, delayMillis = durationMillis.ForOutgoing, easing = LinearOutSlowInEasing, ), ) Shared Axis
  59. @Composable fun LibraryScreen(...) { val (state, onStateChanged) = remember {

    mutableStateOf(LibraryState(SortType.A_TO_Z, ListType.Grid)) } Scaffold(...) { val slideDistance = rememberSlideDistance() AnimatedContent( targetState = state, transitionSpec = { if (initialState.listType != targetState.listType) { materialFadeThrough() } else { materialSharedAxisY(forward = true, slideDistance) } }, modifier = Modifier.padding(innerPadding), ) { currentDestination -> LibraryContents(state, ...) } } } Shared Axis
  60. @Composable fun DemoScreen() { var albumId by remember { ...

    } if (albumId != null) { AlbumScreen(albumId) } else { LibraryScreen(onItemClick = { albumId = it.id }) } } Example #3
  61. @Composable fun DemoScreen() { var albumId by remember { ...

    } AnimatedContent( targetState = albumId, transitionSpec = { ... }, ) { currentId -> if (currentId != null) { AlbumScreen(currentId) } else { LibraryScreen(onItemClick = { albumId = it.id }) } } } AnimatedContent
  62. @Composable fun DemoScreen() { var albumId by remember { ...

    } AnimatedContent( targetState = albumId, transitionSpec = { if (targetState != null) { translateYIn { it } togetherWith holdOut() } else { holdIn() togetherWith translateYOut { it } } }, ) { currentId -> ... } } AnimatedContent
  63. ... AnimatedContent( targetState = albumId, transitionSpec = { if (targetState

    != null) { ContentTransform( translateYIn { it }, holdOut(), targetContentZIndex = 1f ) } else { ContentTransform( holdIn(), translateYOut { it }, targetContentZIndex = 0f ) } }, ) { currentId -> ... } ... Z-Index
  64. @Composable fun DemoScreen() { var albumId by remember { ...

    } AnimatedContent( targetState = albumId, transitionSpec = { ... }, ) { currentId -> if (currentId != null) { AlbumScreen(currentId) } else { LibraryScreen(onItemClick = { albumId = it.id }) } } } Issue Point #1
  65. @Composable fun LibraryScreen(...) { val (state, ...) = remember {

    mutableStateOf(LibraryState(SortType.A_TO_Z, ListType.Grid)) } Scaffold(...) { AnimatedContent(...) { LibraryContents(...) } } } Issue Point #1
  66. @Composable fun LibraryScreen(...) { val (state, ...) = rememberSaveable(stateSaver =

    Saver) { mutableStateOf(LibraryState(SortType.A_TO_Z, ListType.Grid)) } Scaffold(...) { AnimatedContent(...) { LibraryContents(...) } } } Saveable val Saver = run { val sortTypeKey = "SortType" val listTypeKey = "ListType" mapSaver( save = { mapOf( sortTypeKey to it.sortType, listTypeKey to it.listType, )}, restore = { LibraryState( it[sortTypeKey] as SortType, it[listTypeKey] as ListType, )} )
  67. @Composable fun DemoScreen() { var albumId by remember { ...

    } AnimatedContent(...) { currentId -> if (currentId != null) { AlbumScreen(currentId) } else { LibraryScreen(onItemClick = { albumId = it.id }) } } } Issue Point #2
  68. @Composable fun DemoScreen() { val saveableStateHolder = rememberSaveableStateHolder() var albumId

    by remember { ... } AnimatedContent(...) { currentId -> saveableStateHolder.SaveableStateProvider(currentId) { if (currentId != null) { AlbumScreen(currentId) } else { LibraryScreen(onItemClick = { albumId = it.id }) } } } } Saveable
  69. @Composable fun DemoScreen() { val saveableStateHolder = rememberSaveableStateHolder() var albumId

    by remember { ... } AnimatedContent(...) { currentId -> saveableStateHolder.SaveableStateProvider(currentId) { if (currentId != null) { AlbumScreen(currentId) } else { LibraryScreen(onItemClick = { albumId = it.id }) } } } } Saveable
  70. private class SaveableStateHolderImpl( private val savedStates: MutableMap<Any, Map<String, List<Any?>>> =

    mutableMapOf() ) : SaveableStateHolder { private val registryHolders = mutableMapOf<Any, RegistryHolder>() var parentSaveableStateRegistry: SaveableStateRegistry? = null @Composable override fun SaveableStateProvider(key: Any, content: @Composable () -> Unit) { ReusableContent(key) { val registryHolder = remember { ... RegistryHolder(key) } CompositionLocalProvider( LocalSaveableStateRegistry provides registryHolder.registry, content = content ) ... } } ... Compose 1.6
  71. @Composable fun DemoScreen() { val saveableStateHolder = rememberSaveableStateHolder() var albumId

    by remember { ... } AnimatedContent(...) { currentId -> saveableStateHolder.SaveableStateProvider(currentId) { if (currentId != null) { AlbumScreen(currentId) } else { LibraryScreen(onItemClick = { albumId = it.id }) } } } } AnimatedContent
  72. @Composable fun DemoScreen() { val navController = rememberNavController() NavHost(navController, startDestination

    = "library") { composable("library", enterTransition = { holdIn() }, exitTransition = { holdOut() }, ) { LibraryScreen { navController.navigate("album/${it.id}") } } composable("album/{albumId}", arguments = listOf(navArgument("albumId") { ... }), enterTransition = { translateYIn { it } }, exitTransition = { translateYOut { it } }, ) { backStackEntry -> AlbumScreen(backStackEntry.arguments?.getLong("albumId")) } } } NavHost
  73. @Composable public fun NavHost(...) { ... val saveableStateHolder = rememberSaveableStateHolder()

    ... if (backStackEntry != null) { ... val transition = updateTransition(backStackEntry, label = "entry") transition.AnimatedContent( modifier, transitionSpec = { ... }, contentAlignment, contentKey = { it.id } ) { val currentEntry = visibleEntries.lastOrNull { entry -> it == entry } currentEntry?.LocalOwnersProvider(saveableStateHolder) { (currentEntry.destination as ComposeNavigator.Destination).content(this, currentEntry) } } Navigation 2.7
  74. @Composable public fun NavHost(...) { ... val saveableStateHolder = rememberSaveableStateHolder()

    ... if (backStackEntry != null) { ... val transition = updateTransition(backStackEntry, label = "entry") transition.AnimatedContent( modifier, transitionSpec = { ... }, contentAlignment, contentKey = { it.id } ) { val currentEntry = visibleEntries.lastOrNull { entry -> it == entry } currentEntry?.LocalOwnersProvider(saveableStateHolder) { (currentEntry.destination as ComposeNavigator.Destination).content(this, currentEntry) } } Navigation 2.7
  75. @Composable public fun NavHost(...) { ... val saveableStateHolder = rememberSaveableStateHolder()

    ... if (backStackEntry != null) { ... val transition = updateTransition(backStackEntry, label = "entry") transition.AnimatedContent( modifier, transitionSpec = { ... }, contentAlignment, contentKey = { it.id } ) { val currentEntry = visibleEntries.lastOrNull { entry -> it == entry } currentEntry?.LocalOwnersProvider(saveableStateHolder) { (currentEntry.destination as ComposeNavigator.Destination).content(this, currentEntry) } } Navigation 2.7
  76. @Composable public fun NavBackStackEntry.LocalOwnersProvider( saveableStateHolder: SaveableStateHolder, content: @Composable () ->

    Unit ) { CompositionLocalProvider(...) { saveableStateHolder.SaveableStateProvider(content) } } @Composable private fun SaveableStateHolder.SaveableStateProvider(content: @Composable () -> Unit) { val viewModel = viewModel<BackStackEntryIdViewModel>() viewModel.saveableStateHolderRef = WeakReference(this) SaveableStateProvider(viewModel.id, content) } Navigation 2.7
  77. @Composable public fun NavBackStackEntry.LocalOwnersProvider( saveableStateHolder: SaveableStateHolder, content: @Composable () ->

    Unit ) { CompositionLocalProvider(...) { saveableStateHolder.SaveableStateProvider(content) } } @Composable private fun SaveableStateHolder.SaveableStateProvider(content: @Composable () -> Unit) { val viewModel = viewModel<BackStackEntryIdViewModel>() viewModel.saveableStateHolderRef = WeakReference(this) SaveableStateProvider(viewModel.id, content) } Navigation 2.7
  78. @Composable fun DemoScreen() { val navController = rememberNavController() NavHost(navController, startDestination

    = "library") { composable("library", enterTransition = { holdIn() }, exitTransition = { holdOut() }, ) { LibraryScreen { navController.navigate("album/${it.id}") } } composable("album/{albumId}", arguments = listOf(navArgument("albumId") { ... }), enterTransition = { translateYIn { it } }, exitTransition = { translateYOut { it } }, ) { backStackEntry -> AlbumScreen(backStackEntry.arguments?.getLong("albumId")) } } } NavHost
  79. @Composable fun DemoScreen() { val navController = rememberNavController() NavHost(navController, startDestination

    = "library") { composable("library", ...) { LibraryScreen { navController.navigate("album/${it.id}") } } composable("album/{albumId}", ...) { backStackEntry -> AlbumScreen(backStackEntry.arguments?.getLong("albumId")) } } } NavHost
  80. @Composable fun DemoScreen() { val navController = rememberNavController() NavHost(navController, startDestination

    = "library") { composable("library", ...) { LibraryScreen { navController.navigate("album/${it.id}") } } composable("album/{albumId}", ...) { backStackEntry -> AlbumScreen(backStackEntry.arguments?.getLong("albumId")) } } } NavHost
  81. Navigation 2.8 @Composable public fun NavHost(...) { ... val currentBackStack

    by composeNavigator.backStack.collectAsState() var progress by remember { mutableFloatStateOf(0f) } var inPredictiveBack by remember { mutableStateOf(false) } PredictiveBackHandler(currentBackStack.size > 1) { backEvent -> progress = 0f val currentBackStackEntry = currentBackStack.lastOrNull() composeNavigator.prepareForTransition(currentBackStackEntry!!) val previousEntry = currentBackStack[currentBackStack.size - 2] composeNavigator.prepareForTransition(previousEntry) try { backEvent.collect { inPredictiveBack = true progress = it.progress } inPredictiveBack = false
  82. Navigation 2.8 @Composable public fun NavHost(...) { ... val currentBackStack

    by composeNavigator.backStack.collectAsState() var progress by remember { mutableFloatStateOf(0f) } var inPredictiveBack by remember { mutableStateOf(false) } PredictiveBackHandler(currentBackStack.size > 1) { backEvent -> progress = 0f val currentBackStackEntry = currentBackStack.lastOrNull() composeNavigator.prepareForTransition(currentBackStackEntry!!) val previousEntry = currentBackStack[currentBackStack.size - 2] composeNavigator.prepareForTransition(previousEntry) try { backEvent.collect { inPredictiveBack = true progress = it.progress } inPredictiveBack = false
  83. val currentBackStack by composeNavigator.backStack.collectAsState() var progress by remember { mutableFloatStateOf(0f)

    } var inPredictiveBack by remember { mutableStateOf(false) } PredictiveBackHandler(currentBackStack.size > 1) { backEvent -> progress = 0f val currentBackStackEntry = currentBackStack.lastOrNull() composeNavigator.prepareForTransition(currentBackStackEntry!!) val previousEntry = currentBackStack[currentBackStack.size - 2] composeNavigator.prepareForTransition(previousEntry) try { backEvent.collect { inPredictiveBack = true progress = it.progress } inPredictiveBack = false composeNavigator.popBackStack(currentBackStackEntry, false) } catch (e: CancellationException) { inPredictiveBack = false } } ... if (backStackEntry != null) { Navigation 2.8
  84. val currentBackStack by composeNavigator.backStack.collectAsState() var progress by remember { mutableFloatStateOf(0f)

    } var inPredictiveBack by remember { mutableStateOf(false) } PredictiveBackHandler(currentBackStack.size > 1) { backEvent -> progress = 0f val currentBackStackEntry = currentBackStack.lastOrNull() composeNavigator.prepareForTransition(currentBackStackEntry!!) val previousEntry = currentBackStack[currentBackStack.size - 2] composeNavigator.prepareForTransition(previousEntry) try { backEvent.collect { inPredictiveBack = true progress = it.progress } inPredictiveBack = false composeNavigator.popBackStack(currentBackStackEntry, false) } catch (e: CancellationException) { inPredictiveBack = false } } ... if (backStackEntry != null) { Navigation 2.8
  85. } catch (e: CancellationException) { inPredictiveBack = false } }

    ... if (backStackEntry != null) { ... val transitionState = remember { SeekableTransitionState(backStackEntry) } val transition = rememberTransition(transitionState, label = "entry") if (inPredictiveBack) { LaunchedEffect(progress) { val previousEntry = currentBackStack[currentBackStack.size - 2] transitionState.seekTo(progress, previousEntry) } } else { LaunchedEffect(backStackEntry) { if (transitionState.currentState != backStackEntry) { transitionState.animateTo(backStackEntry) } else { ... Navigation 2.8
  86. } catch (e: CancellationException) { inPredictiveBack = false } }

    ... if (backStackEntry != null) { ... val transitionState = remember { SeekableTransitionState(backStackEntry) } val transition = rememberTransition(transitionState, label = "entry") if (inPredictiveBack) { LaunchedEffect(progress) { val previousEntry = currentBackStack[currentBackStack.size - 2] transitionState.seekTo(progress, previousEntry) } } else { LaunchedEffect(backStackEntry) { if (transitionState.currentState != backStackEntry) { transitionState.animateTo(backStackEntry) } else { ... Navigation 2.8
  87. if (backStackEntry != null) { ... val transitionState = remember

    { SeekableTransitionState(backStackEntry) } val transition = rememberTransition(transitionState, label = "entry") if (inPredictiveBack) { LaunchedEffect(progress) { val previousEntry = currentBackStack[currentBackStack.size - 2] transitionState.seekTo(progress, previousEntry) } } else { LaunchedEffect(backStackEntry) { if (transitionState.currentState != backStackEntry) { transitionState.animateTo(backStackEntry) } else { ... } } } transition.AnimatedContent(...) { ... } Navigation 2.8
  88. @Composable fun DemoScreen() { val navController = rememberNavController() NavHost(navController, startDestination

    = "library") { composable("library", ...) { LibraryScreen { navController.navigate("album/${it.id}") } } composable("album/{albumId}", ...) { backStackEntry -> AlbumScreen(backStackEntry.arguments?.getLong("albumId")) BackHandler { if (collapse && listState.isScrollInProgress.not()) { // scroll to top item } else { upPress() } } } } } Custom BackHandler
  89. @Composable fun DemoScreen() { val navController = rememberNavController() NavHost(navController, startDestination

    = "library") { composable("library", ...) { LibraryScreen { navController.navigate("album/${it.id}") } } composable("album/{albumId}", ...) { backStackEntry -> AlbumScreen(backStackEntry.arguments?.getLong("albumId")) BackHandler { if (collapse && listState.isScrollInProgress.not()) { // scroll to top item } else { upPress() } } } } } Custom BackHandler
  90. @Composable fun DemoScreen() { val navController = rememberNavController() NavHost(navController, startDestination

    = "library") { composable("library", ...) { LibraryScreen { navController.navigate("album/${it.id}") } } composable("album/{albumId}", ...) { backStackEntry -> AlbumScreen(backStackEntry.arguments?.getLong("albumId")) BackHandler( enabled = collapse && listState.isScrollInProgress.not() ) { // scroll to top item } } } } BackHandler Enabled
  91. val enterTransition: EnterTransition = ... val exitTransition: ExitTransition = ...

    AnimatedContent( targetState = state, transitionSpec = { enterTransition to exitTransition }, ) { currentState -> // composable according to screen } materialSharedAxis(Axis.X, forward = true) materialFadeThrough() materialFade() materialElevationScale(growing = false) hold() ... Axis.Y Axis.Z Material Motion Link: h"ps://github.com/fornewid/material-motion-compose
  92. @Composable fun DemoScreen() { val navController = rememberNavController() NavHost(navController, startDestination

    = "library") { composable("library", ...) { LibraryScreen { navController.navigate("album/${it.id}") } } composable("album/{albumId}", ...) { backStackEntry -> AlbumScreen(backStackEntry.arguments?.getLong("albumId")) } } } NavHost
  93. @Composable fun DemoScreen() { val navController = rememberNavController() SharedTransitionLayout {

    NavHost(navController, startDestination = "library") { composable("library", ...) { LibraryScreen(...) } composable("album/{albumId}", ...) { AlbumScreen(...) } } } } Shared Elements
  94. @Composable fun LibraryGridItem(...) { Card( modifier = Modifier.sharedBounds( rememberSharedContentState(key =

    "album-${album.id}"), animatedVisibilityScope = animatedVisibilityScope, ), ) { ... } } Shared Elements
  95. @Composable fun LibraryGridItem(...) { Card( modifier = Modifier.sharedBounds( rememberSharedContentState(key =

    "album-${album.id}"), animatedVisibilityScope = animatedVisibilityScope, ), ) { ... } } Shared Elements
  96. @Composable fun LibraryGridItem(...) { Card( modifier = Modifier.sharedBounds( rememberSharedContentState(key =

    "album-${album.id}"), animatedVisibilityScope = animatedVisibilityScope, ), ) { ... } } Shared Elements
  97. @Composable fun LibraryGridItem(...) { Card( modifier = Modifier.sharedBounds( rememberSharedContentState(key =

    "album-${album.id}"), animatedVisibilityScope = animatedVisibilityScope, ), ) { ... } } @Composable fun AlbumScreen(...) { Box( modifier = Modifier.sharedBounds( rememberSharedContentState(key = "album-${album.id}"), animatedVisibilityScope = animatedVisibilityScope, ), ) { ... } } Shared Elements
  98. @Composable fun SharedTransitionLayout( modifier: Modifier = Modifier, content: @Composable SharedTransitionScope.()

    -> Unit ) { SharedTransitionScope { sharedTransitionModifier -> // Put shared transition modifier *after* user provided modifier to support user provided // modifiers to influence the overlay's size, position, clipping, etc. Box(modifier.then(sharedTransitionModifier)) { content() } } } Compose 1.7
  99. @Composable fun <S> AnimatedContent( ... content: @Composable() AnimatedContentScope.(targetState: S) ->

    Unit ) { ... } sealed interface AnimatedContentScope : AnimatedVisibilityScope Compose 1.7
  100. @Composable fun <S> AnimatedContent( ... content: @Composable() AnimatedContentScope.(targetState: S) ->

    Unit ) { ... } sealed interface AnimatedContentScope : AnimatedVisibilityScope Compose 1.7
  101. @Stable interface SharedTransitionScope : LookaheadScope { fun Modifier.sharedBounds( sharedContentState: SharedContentState,

    animatedVisibilityScope: AnimatedVisibilityScope, ... ): Modifier ... Compose 1.7
  102. @Stable interface SharedTransitionScope : LookaheadScope { fun Modifier.sharedBounds( sharedContentState: SharedContentState,

    animatedVisibilityScope: AnimatedVisibilityScope, ... ): Modifier ... Compose 1.7
  103. SharedTransitionLayout { AnimatedContent { AnimatedContent { Box( modifier = Modifier.sharedBounds(

    rememberSharedContentState("album-${album.id}"), animatedVisibilityScope = this@AnimatedContent, ), ) } } } Shared Elements ⚠
  104. SharedTransitionLayout { AnimatedContent { val parentScope: AnimatedVisibilityScope = this AnimatedContent

    { Box( modifier = Modifier.sharedBounds( rememberSharedContentState("album-${album.id}"), animatedVisibilityScope = parentScope, ), ) } } } Shared Elements
  105. forked from androidx/androidx Compose Multiplatform Core Link: h"ps://github.com/JetBrains/compose-multipla$orm-core commonMain androidMain

    jbMain ... nativeMain desktopMain … webMain or skikoMain Jetpack Compose Compose Multiplatform Compose Multiplatform
  106. // Jetpack Compose dependencies { implementation("androidx. compose.runtime :runtime:1.6.7") implementation("androidx. compose.ui

    :ui:1.6.7") implementation("androidx. compose.foundation :foundation:1.6.7") implementation("androidx. compose.animation :animation:1.6.7") implementation("androidx. compose.material :material:1.6.7") implementation("androidx. compose.material3 :material3:1.2.1") } Link: h"ps://developer.android.com/jetpack/androidx/releases/compose#versions
  107. // Compose Multiplatform commonMain.dependencies { implementation("org.jetbrains. compose.runtime :runtime:1.6.11") implementation("org.jetbrains. compose.ui

    :ui:1.6.11") implementation("org.jetbrains. compose.foundation :foundation:1.6.11") implementation("org.jetbrains. compose.animation :animation:1.6.11") implementation("org.jetbrains. compose.material :material:1.6.11") implementation("org.jetbrains. compose.material3 :material3:1.6.11") } Link: h"ps://www.jetbrains.com/help/kotlin-multipla$orm-dev/compose-compatibility-and-versioning.html
  108. // core/src/commonMain/kotlin/soup/compose/material/motion/CircularReveal.kt import androidx.annotation.FloatRange private class CircularRevealShape( @FloatRange(from = 0.0,

    to = 1.0) // Compose Web X private val progress: Float, private val center: (fullSize: Size) -> Offset = { Offset.Unspecified }, ) : Shape { ... #1. Annotation Link: h"ps://github.com/fornewid/material-motion-compose/pull/190/%les#di&
  109. // core/src/commonMain/kotlin/soup/compose/material/motion/CircularReveal.kt import androidx.annotation.FloatRange private class CircularRevealShape( @FloatRange(from = 0.0,

    to = 1.0) private val progress: Float, private val center: (fullSize: Size) -> Offset = { Offset.Unspecified }, ) : Shape { init { require(progress !in 0.0..1.0) } ... Link: h"ps://github.com/fornewid/material-motion-compose/pull/190/%les#di& A. Preconditions
  110. JetBrains Internal ۄ੉࠳۞ܻ androidx.annotation:annotation ✅ ✅ ✅ ❌ androidx.collection:collection ✅

    ✅ ✅ ❌ org.jetbrains.compose.annotation-internal:annotation ✅ ✅ ✅ ✅ org.jetbrains.compose.collection-internal:collection ✅ ✅ ✅ ✅ Android iOS Desktop Web module B. Jetbrains Internal
  111. @Composable fun DemoScreen() { val navController = rememberNavController() NavHost(navController, startDestination

    = "library") { composable("library", ...) { LibraryScreen { navController.navigate("album/${it.id}") } } composable("album/{albumId}", ...) { backStackEntry -> AlbumScreen(backStackEntry.arguments?.getLong("albumId")) BackHandler( enabled = collapse && listState.isScrollInProgress.not() ) { // scroll to top item } } } } #2. BackHandler
  112. Activity Compose @Composable public fun BackHandler(enabled: Boolean = true, onBack:

    () -> Unit) { // Safely update the current `onBack` lambda when a new one is provided val currentOnBack by rememberUpdatedState(onBack) // Remember in Composition a back callback that calls the `onBack` lambda val backCallback = remember { object : OnBackPressedCallback(enabled) { override fun handleOnBackPressed() { currentOnBack() } } } // On every successful composition, update the callback with the `enabled` value SideEffect { backCallback.isEnabled = enabled } val backDispatcher = checkNotNull(LocalOnBackPressedDispatcherOwner.current) { "No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner” }.onBackPressedDispatcher
  113. Activity Compose @Composable public fun BackHandler(enabled: Boolean = true, onBack:

    () -> Unit) { // Safely update the current `onBack` lambda when a new one is provided val currentOnBack by rememberUpdatedState(onBack) // Remember in Composition a back callback that calls the `onBack` lambda val backCallback = remember { object : OnBackPressedCallback(enabled) { // Android only override fun handleOnBackPressed() { currentOnBack() } } } // On every successful composition, update the callback with the `enabled` value SideEffect { backCallback.isEnabled = enabled } val backDispatcher = checkNotNull(LocalOnBackPressedDispatcherOwner.current) { "No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner” }.onBackPressedDispatcher
  114. @Composable fun DemoScreen() { val navController = rememberNavController() NavHost(navController, startDestination

    = "library") { composable("library", ...) { LibraryScreen { navController.navigate("album/${it.id}") } } composable("album/{albumId}", ...) { backStackEntry -> AlbumScreen(backStackEntry.arguments?.getLong("albumId")) // BackHandler( // enabled = collapse && listState.isScrollInProgress.not() // ) { // // scroll to top item // } } } } A. Comment Out
  115. B. expect - actual commonMain/ androidMain/ iosMain/ desktopMain/ expect actual

    actual actual Link: h"ps://kotlinlang.org/docs/multipla$orm-expect-actual.html
  116. Sample // src/commonMain/kotlin/BackHandler.kt @Composable expect fun BackHandler(enabled: Boolean = true,

    onBack: () -> Unit) // src/androidMain/kotlin/BackHandler.kt @Composable actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) { androidx.activity.compose.BackHandler(enabled = enabled, onBack = onBack) } // src/iosMain/kotlin/BackHandler.kt @Composable actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) { // do nothing! }
  117. Sample // src/commonMain/kotlin/BackHandler.kt @Composable expect fun BackHandler(enabled: Boolean = true,

    onBack: () -> Unit) // src/androidMain/kotlin/BackHandler.kt @Composable actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) { androidx.activity.compose.BackHandler(enabled = enabled, onBack = onBack) } // src/iosMain/kotlin/BackHandler.kt @Composable actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) { // do nothing! }
  118. Sample // src/commonMain/kotlin/BackHandler.kt @Composable expect fun BackHandler(enabled: Boolean = true,

    onBack: () -> Unit) // src/androidMain/kotlin/BackHandler.kt @Composable actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) { androidx.activity.compose.BackHandler(enabled = enabled, onBack = onBack) } // src/iosMain/kotlin/BackHandler.kt @Composable actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) { // do nothing! }
  119. @Composable fun DemoScreen() { val navController = rememberNavController() NavHost(navController, startDestination

    = "library") { composable("library", ...) { LibraryScreen { navController.navigate("album/${it.id}") } } composable("album/{albumId}", ...) { backStackEntry -> AlbumScreen(backStackEntry.arguments?.getLong("albumId")) BackHandler( enabled = collapse && listState.isScrollInProgress.not() ) { // scroll to top item } } } } B. expect - actual
  120. @Composable fun DemoScreen() { val navController = rememberNavController() NavHost(navController, startDestination

    = "library") { composable("library", ...) { LibraryScreen { navController.navigate("album/${it.id}") } } composable("album/{albumId}", ...) { backStackEntry -> AlbumScreen(backStackEntry.arguments?.getLong("albumId")) } } } #3. Predictive Back
  121. @Composable public fun NavHost(...) { ... val currentBackStack by composeNavigator.backStack.collectAsState()

    var progress by remember { mutableFloatStateOf(0f) } var inPredictiveBack by remember { mutableStateOf(false) } PredictiveBackHandler(currentBackStack.size > 1) { backEvent -> // Android only ... Jetpack Navigation 2.8
  122. @Composable public fun NavHost(...) { ... val currentBackStack by composeNavigator.backStack.collectAsState()

    var progress by remember { mutableFloatStateOf(0f) } var inPredictiveBack by remember { mutableStateOf(false) } /* TODO: Support PredictiveBackHandler on multiplatform PredictiveBackHandler(currentBackStack.size > 1) { backEvent -> ... */ ... Link: h"ps://github.com/JetBrains/compose-multipla$orm/issues/4419 Jetbrains Navigation 2.8 '
  123. Summary Multiplatform BackHandler ❌ Compose*, Annotation * expect - actual

    ContainerTransform AnimatedContent Shared Elements Predictive Back Pre-release NavHost Enter/Exit Transition List-Detail NavHost Enter/Exit Transition List Sort/Type Enter/Exit Transition AnimatedContent Tab-Contents Crossfade Fade Through or