fornewid / material-motion-compose

Material Motion for Jetpack Compose and Compose Multiplatform
https://fornewid.github.io/material-motion-compose/
Apache License 2.0
617 stars 11 forks source link

Weird back stack motion behavior on nested graph #115

Closed uragiristereo closed 2 years ago

uragiristereo commented 2 years ago

I have 4 navigation routes: first, second, third, fourth.

When i try to navigate first -> second -> third -> fourth, the transitions behave normally as it should.

However when i try to navigate back (back press) first <- second <- third <- fourth, the only transition that behave normally is the first pop backstack (in this case third <- fourth), other pop backstack just overlaps the current backstack.

I'm not sure how to explain it well but i have included a sample project for testing it here and a video here

uragiristereo commented 2 years ago

any update?

fornewid commented 2 years ago

@uragiristereo Thanks for sharing. I missed this issue due to a notification issue. I'll check it out soon.

fornewid commented 2 years ago

I checked this issue. MaterialMotion is implemented based on AnimatedContent in Jetpack Compose Animation. And in AnimatedContent, I confirmed that the order of content is reversed when 'pop'.

The followings are sample codes using accompanist AnimatedNavHost based on AnimatedContent.

import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.zIndex
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import soup.compose.material.motion.animation.materialElevationScaleIn
import soup.compose.material.motion.animation.materialElevationScaleOut

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun NestedAnimatedNavigation() {
    val navHost = rememberAnimatedNavController()

    CompositionLocalProvider(LocalNavigation provides navHost) {
        AnimatedNavHost(
            navController = navHost,
            startDestination = "first",
        ) {
            composable(
                route = "first",
                enterTransition = {
                    when (initialState.destination.route) {
                        "second" -> slideInHorizontally { it }
                        else -> null
                    }
                },
                exitTransition = {
                    when (initialState.destination.route) {
                        "second" -> materialElevationScaleOut()
                        else -> null
                    }
                },
                popEnterTransition = {
                    when (initialState.destination.route) {
                        "second" -> materialElevationScaleIn()
                        else -> null
                    }
                },
                popExitTransition = {
                    when (initialState.destination.route) {
                        "second" -> slideOutHorizontally { it }
                        else -> null
                    }
                }
            ) {
                NextScreen(
                    title = "First",
                    color = Color.Cyan,
                    nextRoute = "second"
                )
            }

            composable(
                route = "second",
                enterTransition = {
                    when (initialState.destination.route) {
                        "first" -> slideInHorizontally { it }
                        "third" -> slideInHorizontally { it }
                        else -> null
                    }
                },
                exitTransition = {
                    when (targetState.destination.route) {
                        "first" -> materialElevationScaleOut()
                        "third" -> materialElevationScaleOut()
                        else -> null
                    }
                },
                popEnterTransition = {
                    when (initialState.destination.route) {
                        "first" -> materialElevationScaleIn()
                        "third" -> materialElevationScaleIn()
                        else -> null
                    }
                },
                popExitTransition = {
                    when (targetState.destination.route) {
                        "first" -> slideOutHorizontally { it }
                        "third" -> slideOutHorizontally { it }
                        else -> null
                    }
                }
            ) {
                NextScreen(
                    title = "Second",
                    color = Color.Red,
                    nextRoute = "third"
                )
            }

            composable(
                route = "third",
                enterTransition = {
                    when (initialState.destination.route) {
                        "second" -> slideInHorizontally { it }
                        "fourth" -> slideInHorizontally { it }
                        else -> null
                    }
                },
                exitTransition = {
                    when (targetState.destination.route) {
                        "second" -> materialElevationScaleOut()
                        "fourth" -> materialElevationScaleOut()
                        else -> null
                    }
                },
                popEnterTransition = {
                    when (initialState.destination.route) {
                        "second" -> materialElevationScaleIn()
                        "fourth" -> materialElevationScaleIn()
                        else -> null
                    }
                },
                popExitTransition = {
                    when (targetState.destination.route) {
                        "second" -> slideOutHorizontally { it }
                        "fourth" -> slideOutHorizontally { it }
                        else -> null
                    }
                }
            ) {
                NextScreen(
                    title = "Third",
                    color = Color.Green,
                    nextRoute = "fourth"
                )
            }

            composable(
                route = "fourth",
                enterTransition = {
                    when (initialState.destination.route) {
                        "third" -> slideInHorizontally { it }
                        else -> null
                    }
                },
                exitTransition = {
                    when (targetState.destination.route) {
                        "third" -> materialElevationScaleOut()
                        else -> null
                    }
                },
                popEnterTransition = {
                    when (initialState.destination.route) {
                        "third" -> materialElevationScaleIn()
                        else -> null
                    }
                },
                popExitTransition = {
                    when (targetState.destination.route) {
                        "third" -> slideOutHorizontally { it }
                        else -> null
                    }
                }
            ) {
                NextScreen(
                    title = "Fourth",
                    color = Color.Yellow
                )
            }
        }
    }
}

I found this issue before, and added the following code to fix it. https://github.com/fornewid/material-motion-compose/blob/main/core/src/main/java/soup/compose/material/motion/MaterialMotion.kt#L85

However, you have confirmed that this fix is not perfect either. So I have to think about new fixes.

    val forward: Boolean = pop.not()
    val density: Density = LocalDensity.current
+   val contentZIndex = remember { mutableStateOf(0f) }
    AnimatedContent(
        modifier = modifier,
        transitionSpec = {
            (motionSpec.enter.transition(forward, density) with motionSpec.exit.transition(forward, density))
                .apply {
                    // Show forward contents over the backward contents.
-                   targetContentZIndex = if (forward) 0.1f else 0f
+                   if (forward) {
+                       contentZIndex.value += 0.0001f
+                   } else {
+                       contentZIndex.value -= 0.0001f
+                   }
+                   targetContentZIndex = contentZIndex.value
                }
        },
        contentAlignment = contentAlignment,
    ) { currentState ->
        content(currentState)
    }

However, this is only a temporary patch. Finally, I should request to add a feature to AnimatedContent.

uragiristereo commented 2 years ago

this seems to be working fine on my end, any chance to push the workaround in the jitpack repository now?

fornewid commented 2 years ago

@uragiristereo Thanks for making sure it works. I'll release a new version after lunch.

fornewid commented 2 years ago

Fixed in version 0.8.2. Please check this again.

uragiristereo commented 2 years ago

working perfectly fine, thank you for the release!