compose-destinations
compose-destinations copied to clipboard
Animated Style pass custom variable
Amazing work with the library, it really makes things easier!
One question, if you want animations between screen you can create custom style for each screen that overrides the default AnimatedNavHostEngine
you can define for all screens in that graph. But in styles I can't seem to find a way to pass a variable to it. For example in the documentation under ProfileTransitions.kt
you have hardcoded initialOffsetX = 1000
, but what if I wanted a different number (for example the exact width of the screen. I can define this for default animations, but not for each individual screen.
For example:
ProvideWindowInsets {
AppTheme {
val navController = rememberAnimatedNavController()
val viewModel: CustomViewModel = hiltViewModel()
// We can get width of the screen this way
BoxWithConstraints {
val width = constraints.maxWidth
val navHostEngine = rememberAnimatedNavHostEngine(
rootDefaultAnimations = RootNavGraphDefaultAnimations(
enterTransition = {
slideInHorizontally(
initialOffsetX = { width },
animationSpec = tween(
durationMillis = 700,
easing = FastOutSlowInEasing
)
)
},
exitTransition = {
slideOutHorizontally(
targetOffsetX = { -width },
animationSpec = tween(
durationMillis = 700,
easing = FastOutSlowInEasing
)
)
},
popEnterTransition = {
slideInHorizontally(
initialOffsetX = { -width },
animationSpec = tween(
durationMillis = 700,
easing = FastOutSlowInEasing
)
)
},
popExitTransition = {
slideOutHorizontally(
targetOffsetX = { width },
animationSpec = tween(
durationMillis = 700,
easing = FastOutSlowInEasing
)
)
}
),
)
DestinationsNavHost(
navGraph = NavGraphs.root,
startRoute = NavGraphs.root.startRoute,
engine = navHostEngine,
navController = navController,
modifier = Modifier
) {
animatedComposable(AScreenDestination) {
AScreen(
modifier = Modifier.statusBarsPadding(),
viewModel = viewModel,
)
}
animatedComposable(BScreenDestination) {
BScreen(
modifier = Modifier.statusBarsPadding(),
viewModel = viewModel,
)
}
}
}
}
}
In this case both will have default, then If I wanted to change the transition of BScreen
, you would need to add:
object BScreenTransition: DestinationStyle.Animated {
override fun AnimatedContentScope<NavBackStackEntry>.enterTransition(): EnterTransition? {
return when (initialState.navDestination) {
BScreenTransitionDestination ->
slideInHorizontally(
initialOffsetX = { 1000 },
animationSpec = tween(2500)
)
else -> null
}
}
override fun AnimatedContentScope<NavBackStackEntry>.exitTransition(): ExitTransition? {
return when (targetState.navDestination) {
BScreenTransitionDestination ->
slideOutHorizontally(
targetOffsetX = { -1000 },
animationSpec = tween(2500)
)
else -> null
}
}
override fun AnimatedContentScope<NavBackStackEntry>.popEnterTransition(): EnterTransition? {
return when (initialState.navDestination) {
BScreenTransitionDestination ->
slideInHorizontally(
initialOffsetX = { -1000 },
animationSpec = tween(2500)
)
else -> null
}
}
override fun AnimatedContentScope<NavBackStackEntry>.popExitTransition(): ExitTransition? {
return when (targetState.navDestination) {
BScreenTransitionDestination ->
slideOutHorizontally(
targetOffsetX = { 1000 },
animationSpec = tween(2500)
)
else -> null
}
}
}
Then in BScreen add:
@Destination(
style = BScreenTransition::class,
)
So the animation style would be applied.
This way you can't pass the width
to the BScreenTransition
you can only change a fixed number inside this object.
Maybe it can be done and I just don't know how.
Thanks for the help!
Hi @pesjak !
At the moment I don't think there is a way to do this other than saving the screen size somewhere accessible from the transition classes.
Is the screen size all you need? Or so you specifically need arbitrary arguments?
Thanks for reporting 👍
I think I don't need anything else. These animations that you can create are sometimes weird for me, because if you want to have a clear transition from A->B, you need to calculate the width and pass it as offset, because they both need to exist. That is why I wanted to pass the width, but I'll survive :).
No problem, thanks for the reply and again, amazing work @raamcosta!
I will check a way to get screen size in all transitions then 🙂 Tbh I focused only on samples to get the current animations APIs, so I'm glad you're trying them so I can iterate on your feedback!
I'll leave this open until I have time to check it.
First of all, amazing work and amazing library!
I've tinkered a bit with it for the past few days, and I've also encountered this issue. A quick and dirty fix might be to use the android system resources to get the screen width/height:
val height = Resources.getSystem().displayMetrics.heightPixels
val width = Resources.getSystem().displayMetrics.widthPixels
The only issue here is that the DisplayMetrics
don't account for the sizes of system bars, like the navigation bar and the top system bar. For horizontal transitions this is fine(I don't think there are any system bars impacting the width of the screen), but for height
based transitions this is something to be aware of.
Thank you @nikolaDrljaca !
I'll try to look at this soon.
I also have found just now that there's no easy way to do that for now. It would be very nice if there's a way to get LocalDensity.current to create those animations. I'm trying to use this library https://github.com/fornewid/material-motion-compose/ so for now I'm doing a ugly hack using DestinationStyle.Runtime
I would also appreciate a way to pass arguments to be able to do an animation from A to B, where the Screen-Position of A is needed. I am trying to create a similiar animation like in the Google Photos App. I have a Grid with Items, and if the User clicks on one of the Items, then there should be an animation from the Grid-Item to the Detail-View of it. Herefore it would be nice to pass the offset and the scale from the Griditem, and do the transition to the Detail Page based on that.
So I could do something similiar to this:
enterTransition = {
slideIn(initialOffset = { fullSize ->
targetState.center.offsetFrom(fullSize)
}) +
scaleIn(initialScale = targetState.scale) + fadeIn()
},
exitTransition = {
slideOut(targetOffset = { fullSize ->
initialState.center.offsetFrom(fullSize)
}) +
scaleOut(targetScale = initialState.scale) + fadeOut()
}
where initialState is the Grid-Item.
Hi guys!
So, the solution I came up with here was to allow us to add animations at runtime to allow you guys to have the same capabilities you do with official compose navigation. In this case, we lost something when using annotations because the animation functions run outside the context of the NavHost call. So, in these cases, we will on v2 be able to do this:
DestinationsNavHost(
//...
) {
MyScreenDestination.animateWith(
enterTransition = { /*...*/ },
exitTransition = { /*...*/ },
popEnterTransition = { /*...*/ },
popExitTransition = { /*...*/ }
)
// OR you can also pass any instance of DestinationStyle.Animated declared somewhere else
MyScreenDestination animateWith SomeDestinationStyleAnimated
//...
}
What do you guys think? This allows you to have a Compose based state above DestinationsNavHost call and do conditional logic inside the animations.
@raamcosta looks like it’s exactly what I wanted. That’s perfect to me