google / accompanist

A collection of extension libraries for Jetpack Compose
https://google.github.io/accompanist
Apache License 2.0
7.43k stars 598 forks source link

[Navigation Animation] zIndex value is incorrect when switching NavGraphs while using popUpTo option #1529

Closed ln-12 closed 1 year ago

ln-12 commented 1 year ago

Description This issue is related to #1411. I noticed that the animation is broken when navigating between two NavGraphs while using the NavOption popUpTo(0) { inclusive = true }. Usually, this is no problem as val zIndex = composeNavigator.backStack.value.size.toFloat() in AnimatedNavHost returns increasing indices for subsequent screens. However, when popping all back stack entries, the index for the next screen is reset to 1.0 at some point resulting in the broken animation. In the video, the last transition is broken:

https://user-images.githubusercontent.com/36760115/221801569-9bb52af2-53aa-483e-98ef-94e2436b8ae3.mp4

Steps to reproduce

package com.example.myapplication

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.*
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.navigation
import com.example.myapplication.ui.theme.MyApplicationTheme
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import org.w3c.dom.Text

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationTheme {
                Surface {
                    MyNavHost()
                }
            }
        }
    }
}

val SUBGRAPH1 = "Subgraph1"
val FIRSTVIEW1 = "FirstView1"
val SECONDVIEW1 = "SecondView1"

val SUBGRAPH2 = "Subgraph2"
val FIRSTVIEW2 = "FirstView2"
val SECONDVIEW2 = "SecondView2"

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun MyNavHost() {
    val navController = rememberAnimatedNavController()
    val durationMillis = 1500
    val offsetAnimationSpec: FiniteAnimationSpec<IntOffset> = remember { tween(durationMillis) }
    val floatAnimationSpec: FiniteAnimationSpec<Float> = remember { tween(durationMillis) }

    // the start destination is always the welcome screen so we can popToRoot at any time
    // only when a user is set, we navigate to the map directly without animation
    AnimatedNavHost(
        navController = navController,
        startDestination = SUBGRAPH1,
        enterTransition = {
            slideInHorizontally(
                initialOffsetX = { it / 2 },
                animationSpec = offsetAnimationSpec
            ) + fadeIn(animationSpec = floatAnimationSpec)
        },
        exitTransition = {
            slideOutHorizontally(
                targetOffsetX = { -it / 2 },
                animationSpec = offsetAnimationSpec
            )
        },
        popEnterTransition = {
            slideInHorizontally(
                initialOffsetX = { -it / 2 },
                animationSpec = offsetAnimationSpec
            )
        },
        popExitTransition = {
            slideOutHorizontally(
                targetOffsetX = { it / 2 },
                animationSpec = offsetAnimationSpec
            ) + fadeOut(animationSpec = floatAnimationSpec)
        },
        modifier = Modifier.fillMaxSize()
    ) {
        navGraph1(navController)
        navGraph2(navController)
    }
}

@OptIn(ExperimentalAnimationApi::class)
fun NavGraphBuilder.navGraph1(
    navController: NavController
) {
    navigation(
        startDestination = FIRSTVIEW1,
        route = SUBGRAPH1
    ) {
        composable(FIRSTVIEW1) {
            PlaceholderComposable(
                text = FIRSTVIEW1,
                color = Color.Green,
                onNext = { navController.navigate(SECONDVIEW1) }
            )
        }

        composable(SECONDVIEW1) {
            PlaceholderComposable(
                text = SECONDVIEW1,
                color = Color.Red,
                onNext = { navController.navigate(SUBGRAPH2) },
                onPrevious = { navController.navigateUp() }
            )
        }
    }
}

@OptIn(ExperimentalAnimationApi::class)
fun NavGraphBuilder.navGraph2(
    navController: NavController
) {
    navigation(
        startDestination = FIRSTVIEW2,
        route = SUBGRAPH2
    ) {
        composable(FIRSTVIEW2) {
            PlaceholderComposable(
                text = FIRSTVIEW2,
                color = Color.Yellow,
                onNext = {
                    navController.navigate(SECONDVIEW2) {
                        popUpTo(0) { inclusive = true }
                    }
                },
                onPrevious = { navController.navigateUp() }
            )
        }

        composable(SECONDVIEW2) {
            PlaceholderComposable(
                text = SECONDVIEW2,
                color = Color.Blue,
                onNext = {
                    navController.navigate(SUBGRAPH1) {
                        popUpTo(0) { inclusive = true }
                    }
                }
            )
        }
    }
}

@Composable
fun PlaceholderComposable(
    text: String,
    color: Color,
    onNext: (() -> Unit)? = null,
    onPrevious: (() -> Unit)? = null
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(color),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(text = text)

        Row {
            onPrevious?.let { callback ->
                Button(
                    onClick = { callback() },
                    modifier = Modifier.padding(8.dp)
                ) {
                    Text(text = "Previous")
                }
            }

            onNext?.let { callback ->
                Button(
                    onClick = { callback() },
                    modifier = Modifier.padding(8.dp)
                ) {
                    Text(text = "Next")
                }
            }
        }
    }
}

Expected behavior I would expect the see the correct z ordering / animation even if I am popping the back stack entries.

Additional context In the example I am using the lastest version implementation "com.google.accompanist:accompanist-navigation-animation:0.29.1-alpha" of accompanist.

github-actions[bot] commented 1 year ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.

ln-12 commented 1 year ago

The issue is not yet solved. Could you maybe have a look at it @jbw0033 or @ianhanniballake?

claraf3 commented 1 year ago

Hi, thanks for your patience. With accompanist navigation-animation migrated back to Androidx Navigation library, I have filed this bug in Androidx Navigation issuetracker. Moving forward, you can track the bug here.