Skip to content

Commit

Permalink
Updated ComposableDestinationOwner to use a separate ComposableDestin…
Browse files Browse the repository at this point in the history
…ationAnimations class for managing animation behaviour, based on SeekableTransitionState
  • Loading branch information
isaac-udy committed Aug 12, 2024
1 parent d7a6eae commit fd18ca3
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 108 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@ 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
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
Expand Down Expand Up @@ -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<Boolean>,
content: @androidx.compose.runtime.Composable () -> Unit
content: @androidx.compose.runtime.Composable (Transition<EnterExitState>) -> Unit,
)

public companion object {
Expand Down Expand Up @@ -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<Boolean>, content: @androidx.compose.runtime.Composable () -> Unit) {
override fun Animate(
visible: Transition<Boolean>,
content: @androidx.compose.runtime.Composable (Transition<EnterExitState>) -> Unit,
) {
val context = LocalContext.current
val resourceAnimation = remember(this, forView) { forView.asResource(context.theme) }
visible.AnimatedVisibility(
Expand All @@ -99,9 +99,8 @@ public sealed interface NavigationAnimation {
enter = resourceAnimation.id,
exit = resourceAnimation.id,
) {
content()
content(transition)
}
KeepVisibleWith(visible)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -15,32 +15,30 @@ 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<Boolean> {
return owner.transition
public fun ComposableDestination.getTransition(): Transition<EnterExitState> {
return owner.animations.enterExitTransition
}

/**
* Gets the transition for the current navigationContext. This is only valid if the current context is a ComposableDestination,
* and will otherwise throw an exception.
*/
@AdvancedEnroApi
public val navigationTransition: Transition<Boolean>
public val navigationTransition: Transition<EnterExitState>
@Composable
get() {
val destination = navigationContext.contextReference as ComposableDestination
Expand Down Expand Up @@ -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 { }
}
}

Expand Down Expand Up @@ -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
Expand All @@ -110,10 +108,10 @@ public fun OverrideNavigationAnimations(
animationSpec = tween(512),
),
)
onDispose { }
onDispose { }
}
navigationTransition.AnimatedVisibility(
visible = {it},
visible = { it == EnterExitState.Visible },
enter = enter,
exit = exit,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 ->
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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>(AnimationEvent.SnapTo(false))
private val visibilityState = SeekableTransitionState(false)

internal var animationOverride by mutableStateOf<NavigationAnimation.Composable?>(null)

internal lateinit var enterExitTransition: Transition<EnterExitState>

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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -73,27 +61,25 @@ internal class ComposableDestinationOwner(
SavedStateRegistryOwner,
HasDefaultViewModelProviderFactory {

internal var transitionState by mutableStateOf(MutableTransitionState(false))
internal lateinit var transition: Transition<Boolean>
internal var animationOverride by mutableStateOf<NavigationAnimation.Composable?>(null)
private var _parentContainer: NavigationContainer? = parentContainer
private var weakParentContainerReference: WeakReference<NavigationContainer> = 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

Expand Down Expand Up @@ -145,7 +131,6 @@ internal class ComposableDestinationOwner(
_parentContainer = null
}

@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun Render(
backstackState: List<AnyOpenInstruction>,
Expand All @@ -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)
}
Expand Down
Loading

0 comments on commit fd18ca3

Please sign in to comment.