Fixed
Status Update
Comments
cl...@google.com <cl...@google.com>
ap...@google.com <ap...@google.com> #2
Using navigation version 2.6.0 and 2.7.1 the issue still persists.
After further investigation, the behaviour can be corrected by filtering on backstack entries where current state is at least started. In my opinion the name visibleEntries is misleading if it contains entries that are not actually visible or in transition.
cl...@google.com <cl...@google.com> #3
Note that as per the visibleEntries
documentationCREATED
state - those are the entries that are in the process of being removed (i.e., they are no longer in the back stack, but have not yet marked their transition as complete).
It is a bug that those entries aren't removed when their exit animation completes and that's what we'll be looking into.
ap...@google.com <ap...@google.com> #4
Project: platform/frameworks/support
Branch: androidx-main
commit f61da31809827bfac89af629c88ad9b8423e88b6
Author: Clara Fok <clarafok@google.com>
Date: Wed Sep 06 14:14:11 2023
Fix entry not marked complete after view is destroyed
As part of a fix for b/279644470 in aosp/2566210, when the fragment view is destroyed, we would mark the transitioning (outgoing) entry as complete only if the entry has also been removed from backstack. However, in the simple navigate() case where the outgoing entry is not popped and remains in the backstack, this entry is not marked complete and ends up in the visibleEntries even though the view has been destroyed.
Now the backstack check in ON_DESTROY is no longer necessary to fix the original b/279644470 bug -- the view being destroyed should be the right signal for completing entries regardless of whether transitioning effects are used. So this check is removed.
Test: ./gradlew navigation:navigation-fragment:cC
Test: ./gradlew navigation:navigation-runtime:cC
Bug: 288520638
Change-Id: I5caa9af1b5bd7084e76d7daf9515f7430bf2489d
M navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
M navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
M navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
M navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
M navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
https://android-review.googlesource.com/2743918
Branch: androidx-main
commit f61da31809827bfac89af629c88ad9b8423e88b6
Author: Clara Fok <clarafok@google.com>
Date: Wed Sep 06 14:14:11 2023
Fix entry not marked complete after view is destroyed
As part of a fix for
Now the backstack check in ON_DESTROY is no longer necessary to fix the original
Test: ./gradlew navigation:navigation-fragment:cC
Test: ./gradlew navigation:navigation-runtime:cC
Bug: 288520638
Change-Id: I5caa9af1b5bd7084e76d7daf9515f7430bf2489d
M navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
M navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
M navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
M navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
M navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
Description
Devices/Android versions reproduced on: API 33
Animation is broken when navigating between two NavGraphs while using the NavOption popUpTo(0) { inclusive = true }. Not a problem when val zIndex = composeNavigator.backStack.value.size.toFloat() as NavHost 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. Attached video of broken animation.
Repro code
```
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")
}
}
}
}
}
```
Steps to repro
1. click "next"
2. click "previous"
3. click "next"
4. click "next"
5. click "next"