diff --git a/enro-core/src/main/java/dev/enro/animation/NavigationAnimations.kt b/enro-core/src/main/java/dev/enro/animation/NavigationAnimations.kt index 6e15357c..40249cd6 100644 --- a/enro-core/src/main/java/dev/enro/animation/NavigationAnimations.kt +++ b/enro-core/src/main/java/dev/enro/animation/NavigationAnimations.kt @@ -7,7 +7,6 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.EnterExitState import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.Transition import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember @@ -15,7 +14,6 @@ import androidx.compose.ui.platform.LocalContext import dev.enro.core.AnyOpenInstruction import dev.enro.core.NavigationDirection import dev.enro.core.container.originalNavigationDirection -import dev.enro.extensions.KeepVisibleWith import dev.enro.extensions.ResourceAnimatedVisibility import dev.enro.extensions.getAttributeResourceId import dev.enro.extensions.getNestedAttributeResourceId @@ -51,9 +49,9 @@ public sealed interface NavigationAnimation { internal abstract val forView: ForView @androidx.compose.runtime.Composable - public abstract fun Animate( + internal abstract fun Animate( visible: Transition, - content: @androidx.compose.runtime.Composable () -> Unit + content: @androidx.compose.runtime.Composable (Transition) -> Unit, ) public companion object { @@ -84,9 +82,11 @@ public sealed interface NavigationAnimation { val exit: ExitTransition = ExitTransition.None, override val forView: ForView = DefaultAnimations.ForView.noneEnter, ) : Composable(), Enter, Exit { - @OptIn(ExperimentalAnimationApi::class) @androidx.compose.runtime.Composable - override fun Animate(visible: Transition, content: @androidx.compose.runtime.Composable () -> Unit) { + override fun Animate( + visible: Transition, + content: @androidx.compose.runtime.Composable (Transition) -> Unit, + ) { val context = LocalContext.current val resourceAnimation = remember(this, forView) { forView.asResource(context.theme) } visible.AnimatedVisibility( @@ -99,9 +99,8 @@ public sealed interface NavigationAnimation { enter = resourceAnimation.id, exit = resourceAnimation.id, ) { - content() + content(transition) } - KeepVisibleWith(visible) } } } diff --git a/enro-core/src/main/java/dev/enro/destination/compose/ComposableDestinationExtensions.kt b/enro-core/src/main/java/dev/enro/destination/compose/ComposableDestinationExtensions.kt index 78be2006..ad508938 100644 --- a/enro-core/src/main/java/dev/enro/destination/compose/ComposableDestinationExtensions.kt +++ b/enro-core/src/main/java/dev/enro/destination/compose/ComposableDestinationExtensions.kt @@ -2,10 +2,10 @@ package dev.enro.core.compose import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.EnterExitState import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.Transition import androidx.compose.animation.core.tween import androidx.compose.animation.fadeOut @@ -15,24 +15,22 @@ import androidx.compose.ui.platform.LocalInspectionMode import dev.enro.animation.NavigationAnimation import dev.enro.annotations.AdvancedEnroApi import dev.enro.core.navigationContext +import dev.enro.destination.compose.destination.AnimationEvent /** * Causes the ComposableDestination's transition to immediately finish */ @AdvancedEnroApi public fun ComposableDestination.finishTransition() { - val transition = owner.transitionState - if (!transition.isIdle) { - owner.transitionState = MutableTransitionState(transition.targetState) - } + owner.animations.setAnimationEvent(AnimationEvent.SnapTo(false)) } /** * Gets the transition for the ComposableDestination */ @AdvancedEnroApi -public fun ComposableDestination.getTransition() : Transition { - return owner.transition +public fun ComposableDestination.getTransition(): Transition { + return owner.animations.enterExitTransition } /** @@ -40,7 +38,7 @@ public fun ComposableDestination.getTransition() : Transition { * and will otherwise throw an exception. */ @AdvancedEnroApi -public val navigationTransition: Transition +public val navigationTransition: Transition @Composable get() { val destination = navigationContext.contextReference as ComposableDestination @@ -68,11 +66,11 @@ public fun OverrideNavigationAnimations( val navigationContext = navigationContext val destination = navigationContext.contextReference as ComposableDestination DisposableEffect(enter, exit) { - destination.owner.animationOverride = NavigationAnimation.Composable( + destination.owner.animations.animationOverride = NavigationAnimation.Composable( enter = enter, exit = exit, ) - onDispose { } + onDispose { } } } @@ -100,7 +98,7 @@ public fun OverrideNavigationAnimations( val navigationContext = navigationContext val destination = navigationContext.contextReference as ComposableDestination DisposableEffect(Unit) { - destination.owner.animationOverride = NavigationAnimation.Composable( + destination.owner.animations.animationOverride = NavigationAnimation.Composable( enter = EnterTransition.None, // We need a little fade out here to keep the animation active while the animated visibility below has a chance to run // and attach child transitions. This is a bit of a hack, but it's the only way to ensure that child exit transitions @@ -110,10 +108,10 @@ public fun OverrideNavigationAnimations( animationSpec = tween(512), ), ) - onDispose { } + onDispose { } } navigationTransition.AnimatedVisibility( - visible = {it}, + visible = { it == EnterExitState.Visible }, enter = enter, exit = exit, ) { diff --git a/enro-core/src/main/java/dev/enro/destination/compose/container/ComposableNavigationContainer.kt b/enro-core/src/main/java/dev/enro/destination/compose/container/ComposableNavigationContainer.kt index 5ef406d4..a7105c07 100644 --- a/enro-core/src/main/java/dev/enro/destination/compose/container/ComposableNavigationContainer.kt +++ b/enro-core/src/main/java/dev/enro/destination/compose/container/ComposableNavigationContainer.kt @@ -42,6 +42,7 @@ import dev.enro.core.container.NavigationInstructionFilter import dev.enro.core.container.merge import dev.enro.core.controller.get import dev.enro.core.controller.interceptor.builder.NavigationInterceptorBuilder +import dev.enro.destination.compose.destination.AnimationEvent import java.io.Closeable import kotlin.collections.set @@ -71,7 +72,7 @@ public class ComposableNavigationContainer internal constructor( get() = true public val isAnimating: Boolean by derivedStateOf { - destinationOwners.any { !it.transitionState.isIdle } + destinationOwners.any { it.animations.isAnimating } } private val onDestroyLifecycleObserver = LifecycleEventObserver { _, event -> @@ -198,7 +199,7 @@ public class ComposableNavigationContainer internal constructor( destinationOwners.forEach { if (activeDestinations[it.instruction] == null) { - it.transitionState.targetState = false + it.animations.setAnimationEvent(AnimationEvent.AnimateTo(false)) } } destinationOwners = merge(transition.previousBackstack, transition.activeBackstack) @@ -251,11 +252,13 @@ public class ComposableNavigationContainer internal constructor( val isPushedDialogOrBottomSheet = ((destinationOwner.destination is DialogDestination || destinationOwner.destination is BottomSheetDestination) && activePresented != null) - destinationOwner.transitionState.targetState = when (instruction) { + + val target = when (instruction) { activePresented -> !isParentBeingRemoved activePush -> !isParentBeingRemoved && !isPushedDialogOrBottomSheet else -> false } + destinationOwner.animations.setAnimationEvent(AnimationEvent.AnimateTo(target)) } } diff --git a/enro-core/src/main/java/dev/enro/destination/compose/destination/ComposableDestinationAnimations.kt b/enro-core/src/main/java/dev/enro/destination/compose/destination/ComposableDestinationAnimations.kt new file mode 100644 index 00000000..3ea2c0d6 --- /dev/null +++ b/enro-core/src/main/java/dev/enro/destination/compose/destination/ComposableDestinationAnimations.kt @@ -0,0 +1,96 @@ +package dev.enro.destination.compose.destination + +import androidx.compose.animation.EnterExitState +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.SeekableTransitionState +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.rememberTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeOut +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import dev.enro.animation.NavigationAnimation +import dev.enro.core.compose.destination.ComposableDestinationOwner +import dev.enro.core.compose.dialog.BottomSheetDestination +import dev.enro.core.compose.dialog.DialogDestination +import dev.enro.core.container.getAnimationsForEntering +import dev.enro.core.container.getAnimationsForExiting + +internal sealed class AnimationEvent { + data class AnimateTo(val visible: Boolean) : AnimationEvent() + data class SnapTo(val visible: Boolean) : AnimationEvent() + data class Seek(val progress: Float, val visible: Boolean) : AnimationEvent() +} + +internal class ComposableDestinationAnimations( + private val owner: ComposableDestinationOwner, +) { + private var currentAnimationEvent by mutableStateOf(AnimationEvent.SnapTo(false)) + private val visibilityState = SeekableTransitionState(false) + + internal var animationOverride by mutableStateOf(null) + + internal lateinit var enterExitTransition: Transition + + val isAnimating by derivedStateOf { + when(currentAnimationEvent) { + is AnimationEvent.AnimateTo -> visibilityState.targetState != visibilityState.currentState + is AnimationEvent.SnapTo -> false + is AnimationEvent.Seek -> true + } + } + + internal fun setAnimationEvent(event: AnimationEvent) { + currentAnimationEvent = event + } + + @OptIn(ExperimentalMaterialApi::class) + @Composable + fun Animate(content: @Composable () -> Unit) { + val targetState = visibilityState.targetState + val instruction = owner.instruction + val parentContainer = owner.parentContainer + + val animation = remember( + instruction, + targetState, + parentContainer, + animationOverride + ) { + animationOverride ?: when (owner.destination) { + is DialogDestination -> NavigationAnimation.Composable(EnterTransition.None, ExitTransition.None) + is BottomSheetDestination -> NavigationAnimation.Composable( + enter = EnterTransition.None, + exit = fadeOut(tween(75, 150)), + ) + else -> when { + visibilityState.targetState >= visibilityState.currentState -> parentContainer.getAnimationsForEntering(instruction).asComposable() + else -> parentContainer.getAnimationsForExiting(instruction).asComposable() + } + } + } + + LaunchedEffect(currentAnimationEvent) { + val event = currentAnimationEvent + when(event) { + is AnimationEvent.AnimateTo -> visibilityState.animateTo(event.visible) + is AnimationEvent.SnapTo -> visibilityState.snapTo(event.visible) + is AnimationEvent.Seek -> visibilityState.seekTo(event.progress, event.visible) + } + } + + animation.Animate( + visible = rememberTransition(visibilityState, "ComposableDestination Visibility"), + ) { + enterExitTransition = it + content() + } + } +} \ No newline at end of file diff --git a/enro-core/src/main/java/dev/enro/destination/compose/destination/ComposableDestinationOwner.kt b/enro-core/src/main/java/dev/enro/destination/compose/destination/ComposableDestinationOwner.kt index 2add9e3a..6a62bc9a 100644 --- a/enro-core/src/main/java/dev/enro/destination/compose/destination/ComposableDestinationOwner.kt +++ b/enro-core/src/main/java/dev/enro/destination/compose/destination/ComposableDestinationOwner.kt @@ -3,24 +3,14 @@ package dev.enro.core.compose.destination import android.annotation.SuppressLint import android.os.Bundle import androidx.activity.ComponentActivity -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.core.Transition -import androidx.compose.animation.core.tween -import androidx.compose.animation.core.updateTransition -import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Box import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.ReusableContent -import androidx.compose.runtime.getValue import androidx.compose.runtime.movableContentOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner @@ -39,7 +29,6 @@ import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryOwner -import dev.enro.animation.NavigationAnimation import dev.enro.core.AnyOpenInstruction import dev.enro.core.activity import dev.enro.core.compose.ComposableDestination @@ -49,12 +38,11 @@ import dev.enro.core.compose.dialog.DialogDestination import dev.enro.core.compose.dialog.EnroBottomSheetContainer import dev.enro.core.compose.dialog.EnroDialogContainer import dev.enro.core.container.NavigationContainer -import dev.enro.core.container.getAnimationsForEntering -import dev.enro.core.container.getAnimationsForExiting import dev.enro.core.controller.usecase.ComposeEnvironment import dev.enro.core.controller.usecase.OnNavigationContextCreated import dev.enro.core.controller.usecase.OnNavigationContextSaved import dev.enro.core.getNavigationHandle +import dev.enro.destination.compose.destination.ComposableDestinationAnimations import dev.enro.extensions.rememberLifecycleState import java.lang.ref.WeakReference @@ -73,27 +61,25 @@ internal class ComposableDestinationOwner( SavedStateRegistryOwner, HasDefaultViewModelProviderFactory { - internal var transitionState by mutableStateOf(MutableTransitionState(false)) - internal lateinit var transition: Transition - internal var animationOverride by mutableStateOf(null) private var _parentContainer: NavigationContainer? = parentContainer private var weakParentContainerReference: WeakReference = WeakReference(parentContainer) internal val parentContainer get() = weakParentContainerReference.get() ?: _parentContainer!! @SuppressLint("StaticFieldLeak") - @Suppress("LeakingThis") private val lifecycleRegistry = LifecycleRegistry(this) - @Suppress("LeakingThis") private val savedStateRegistryOwner = ComposableDestinationSavedStateRegistryOwner(this, savedInstanceState) - @Suppress("LeakingThis") private val viewModelStoreOwner = ComposableDestinationViewModelStoreOwner( owner = this, savedState = savedStateRegistryOwner.savedState, viewModelStore = viewModelStore ) + internal val animations = ComposableDestinationAnimations( + owner = this, + ) + override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryOwner.savedStateRegistry @@ -145,7 +131,6 @@ internal class ComposableDestinationOwner( _parentContainer = null } - @OptIn(ExperimentalMaterialApi::class) @Composable internal fun Render( backstackState: List, @@ -163,30 +148,9 @@ internal class ComposableDestinationOwner( } } } - - val animation = remember(instruction.instructionId, transitionState.targetState, parentContainer, animationOverride) { - animationOverride ?: when (destination) { - is DialogDestination -> NavigationAnimation.Composable(EnterTransition.None, ExitTransition.None) - is BottomSheetDestination -> NavigationAnimation.Composable( - enter = EnterTransition.None, - exit = fadeOut(tween(75, 150)), - ) - else -> when (transitionState.targetState) { - true -> parentContainer.getAnimationsForEntering(instruction).asComposable() - else -> parentContainer.getAnimationsForExiting(instruction).asComposable() - } - } - } - transition = updateTransition(transitionState, "ComposableDestination Visibility") - - if (!lifecycleState.isAtLeast(Lifecycle.State.STARTED) - && !transition.currentState - && !transition.targetState - ) return - ReusableContent(instruction.instructionId) { ProvideRenderingWindow(backstackState) { - animation.Animate(transition) { + animations.Animate { renderDestination() RegisterComposableLifecycleState(backstackState, parentContainer) } diff --git a/enro-core/src/main/java/dev/enro/extensions/AnimatedContentScope.KeepVisibleWith.kt b/enro-core/src/main/java/dev/enro/extensions/AnimatedContentScope.KeepVisibleWith.kt deleted file mode 100644 index 931f9deb..00000000 --- a/enro-core/src/main/java/dev/enro/extensions/AnimatedContentScope.KeepVisibleWith.kt +++ /dev/null @@ -1,35 +0,0 @@ -package dev.enro.extensions - -import androidx.compose.animation.AnimatedVisibilityScope -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.core.Transition -import androidx.compose.animation.core.animateInt -import androidx.compose.animation.core.tween -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key - -/** - * By default, an AnimatedVisibility composable created with Transition.AnimatedVisibility will stop rendering child content - * even while the parent transition is still active. In the case of dialogs, this can cause a Dialog that's doing a custom - * animation from inside of an AnimatedVisibility to exit before the animation is completed. - * - * This method allows an AnimatedVisibilityScope to bind itself to some other transition, and remain active (and rendering child - * content) while the other transition is running. - */ -@OptIn(ExperimentalAnimationApi::class) -@Composable -internal fun AnimatedVisibilityScope.KeepVisibleWith( - otherTransition: Transition -) { - key(otherTransition.currentState, otherTransition.targetState, otherTransition.hashCode()) { - if(otherTransition.totalDurationNanos > 0) { - transition.animateInt( - transitionSpec = { - tween((otherTransition.totalDurationNanos / 1000000).toInt()) - }, - label = "bindVisibilityToTransition", - targetValueByState = { it.hashCode() } - ).value - } - } -} \ No newline at end of file diff --git a/libs.versions.toml b/libs.versions.toml index d7f4f084..712ed7a5 100644 --- a/libs.versions.toml +++ b/libs.versions.toml @@ -23,13 +23,13 @@ androidx-lifecycle-process = "androidx.lifecycle:lifecycle-process:2.8.4" compose-viewmodel = "androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4" compose-compiler = "androidx.compose.compiler:compiler:1.5.15" -compose-foundation = "androidx.compose.foundation:foundation:1.6.8" -compose-foundationLayout = "androidx.compose.foundation:foundation-layout:1.6.8" -compose-ui = "androidx.compose.ui:ui:1.6.8" -compose-uiTooling = "androidx.compose.ui:ui-tooling:1.6.8" -compose-runtime = "androidx.compose.runtime:runtime:1.6.8" -compose-livedata = "androidx.compose.runtime:runtime-livedata:1.6.8" -compose-material = "androidx.compose.material:material:1.6.8" +compose-foundation = "androidx.compose.foundation:foundation:1.7.0-beta07" +compose-foundationLayout = "androidx.compose.foundation:foundation-layout:1.7.0-beta07" +compose-ui = "androidx.compose.ui:ui:1.7.0-beta07" +compose-uiTooling = "androidx.compose.ui:ui-tooling:1.7.0-beta07" +compose-runtime = "androidx.compose.runtime:runtime:1.7.0-beta07" +compose-livedata = "androidx.compose.runtime:runtime-livedata:1.7.0-beta07" +compose-material = "androidx.compose.material:material:1.7.0-beta07" compose-accompanist-systemUiController = "com.google.accompanist:accompanist-systemuicontroller:0.27.0"