TopAppBar мигает при навигации с помощью Compose Navigation

У меня есть 2 экрана, у которых есть свои и. Когда я перемещаюсь между ними с помощью библиотеки Jetpack Navigation Compose, панель приложения мигает. Почему это происходит и как от этого избавиться?

Код:

Навигация:

      @Composable
fun TodoNavHost(
    navController: NavHostController,
    modifier: Modifier = Modifier
) {
    NavHost(
        navController = navController,
        startDestination = TodoScreen.TodoList.name,
        modifier = modifier
    ) {
        composable(TodoScreen.TodoList.name) {
            TodoListScreen(
                onTodoEditClicked = { todo ->
                    navController.navigate("${TodoScreen.AddEditTodo.name}?todoId=${todo.id}")
                },
                onFabAddNewTodoClicked = {
                    navController.navigate(TodoScreen.AddEditTodo.name)
                }
            )
        }
        composable(
            "${TodoScreen.AddEditTodo.name}?todoId={todoId}", 
            arguments = listOf(
                navArgument("todoId") {
                    type = NavType.LongType
                    defaultValue = -1L
                }
            )
        ) {
            AddEditTodoScreen(
                onNavigateUp = {
                    navController.popBackStack() 
                },
                onNavigateBackWithResult = { result ->
                    navController.navigate(TodoScreen.TodoList.name)
                }
            )
        }
    }
}

Экран списка задач с:

      @Composable
fun TodoListBody(
    todos: List<Todo>,
    todoExpandedStates: Map<Long, Boolean>,
    onTodoItemClicked: (Todo) -> Unit,
    onTodoCheckedChanged: (Todo, Boolean) -> Unit,
    onTodoEditClicked: (Todo) -> Unit,
    onFabAddNewTodoClicked: () -> Unit,
    onDeleteAllCompletedConfirmed: () -> Unit,
    modifier: Modifier = Modifier,
    errorSnackbarMessage: String = "",
    errorSnackbarShown: Boolean = false
) {

    var menuExpanded by remember { mutableStateOf(false) }
    var showDeleteAllCompletedConfirmationDialog by rememberSaveable { mutableStateOf(false) }

    Scaffold(
        modifier,
        topBar = {
            TopAppBar(
                title = { Text("My Todos") },
                actions = {
                    IconButton(
                        onClick = { menuExpanded = !menuExpanded },
                        modifier = Modifier.semantics {
                            contentDescription = "Options Menu"
                        }
                    ) {
                        Icon(Icons.Default.MoreVert, contentDescription = "Show menu")
                    }
                    DropdownMenu(
                        expanded = menuExpanded,
                        onDismissRequest = { menuExpanded = false }) {
                        DropdownMenuItem(
                            onClick = {
                                showDeleteAllCompletedConfirmationDialog = true
                                menuExpanded = false
                            },
                            modifier = Modifier.semantics {
                                contentDescription = "Option Delete All Completed"
                            }) {
                            Text("Delete all completed")
                        }
                    }
                }

            )
        },
[...]

Экран добавления / редактирования Scaffold с TopAppBar:

      @Composable
fun AddEditTodoBody(
    todo: Todo?,
    todoTitle: String,
    setTitle: (String) -> Unit,
    todoImportance: Boolean,
    setImportance: (Boolean) -> Unit,
    onSaveClick: () -> Unit,
    onNavigateUp: () -> Unit,
    modifier: Modifier = Modifier
) {
    Scaffold(
        modifier,
        topBar = {
            TopAppBar(
                title = { Text(todo?.let { "Edit Todo" } ?: "Add Todo") },
                actions = {
                    IconButton(onClick = onSaveClick) {
                        Icon(Icons.Default.Save, contentDescription = "Save Todo")
                    }
                },
                navigationIcon = {
                    IconButton(onClick = onNavigateUp) {
                        Icon(Icons.Default.ArrowBack, contentDescription = "Back")
                    }
                }
            )
        },
    ) { innerPadding ->
        BodyContent(
            todoTitle = todoTitle,
            setTitle = setTitle,
            todoImportance = todoImportance,
            setImportance = setImportance,
            modifier = Modifier.padding(innerPadding)
        )
    }
}

9 ответов

Мигание вызвано анимацией плавного затухания по умолчанию в более поздних версиях программы. navigation-composeбиблиотека. Единственный способ избавиться от него прямо сейчас (без понижения зависимости) — использовать библиотеку анимации Accompanist :

implementation "com.google.accompanist:accompanist-navigation-animation:0.20.0"

А потом заменить на обычный NavHostс концертмейстером AnimatedNavHost, заменять rememberNavController()с rememberAnimatedNavController()и отключите анимацию переходов:

      AnimatedNavHost(
        navController = navController,
        startDestination = bottomNavDestinations[0].fullRoute,
        enterTransition = { _, _ -> EnterTransition.None },
        exitTransition = { _, _ -> ExitTransition.None },
        popEnterTransition = { _, _ -> EnterTransition.None },
        popExitTransition = { _, _ -> ExitTransition.None },
        modifier = modifier,
    ) {
        [...}
    }

Кажется, я нашел простое решение этой проблемы (работает в Compose версии 1.4.0).

Моя установка - мигает

Все мои экраны имеют собственную панель инструментов, завернутую в каркас:

      // Some Composable screnn

Scaffold(
    topBar = { TopAppBar(...) }
) {
    ScreenContent()
}

Основное действие, которое содержит навигационный хост, определяется следующим образом:

      // Activity with NavHost

setContent {
    AppTheme {
        NavHost(...) { }
    }
}

Решение - не мигать!

Оберните NavHost активностью на поверхности:

      setContent {
    AppTheme {
        Surface {
            NavHost(...) { }
        }
    }
}

Остальные экраны остаются прежними. Нет мерцания, а анимация перехода между пунктами назначения почти такая же, как и с фрагментами (мягкое затухание/нарастание). До сих пор я не нашел каких-либо негативных побочных эффектов этого.

У меня такая же проблема с архитектурой «эшафот на экран». К моему удивлению, помогло снижение androidx.navigation:navigation-compose версия для 2.4.0-alpha04.

Чтобы не моргнуть (или соскользнуть, если у вас естьAnimatedNavHost) вы должны поместить в действие и вне , иначе это просто часть экрана и делает переходы, как и любой другой элемент экрана:

      // Activity with your navigation host
setContent {
    MyAppTheme {
        Scaffold(
            topBar = { TopAppBar(...) }
        ) { padding ->
            TodoNavHost(padding, ...) { }
        }
    }
}

ИзScaffoldсодержащийTopAppBarприходитpaddingпараметр, который вы должны передать вNavHostи используйте его на экране, как вы сделали в своем примере

Это ожидаемое поведение. Вы создаете две отдельные панели приложений для обоих экранов, поэтому они обязательно должны мигать. Это неправильный путь. Правильный способ - это фактически поместить основу в ваше основное действие и разместить NavHost в качестве его содержимого. Если вы хотите изменить панель приложения, создайте переменные для хранения состояния. Затем измените их из Composables. В идеале хранить в модели просмотра. Вот как это делается в compose. Через переменные.

Спасибо

С более новой библиотекой implementation "com.google.accompanist:accompanist-navigation-animation:0.24.1-alpha"у вас должно быть вот так

      AnimatedNavHost(
            navController = navController,
            startDestination = BottomNavDestinations.TimerScreen.route,
            enterTransition = { EnterTransition.None },
            exitTransition = { ExitTransition.None },
            popEnterTransition = { EnterTransition.None },
            popExitTransition = { ExitTransition.None },
            modifier = Modifier.padding(innerPadding)

Также

Заменять rememberNavController()с rememberAnimatedNavController()

Заменять NavHostс AnimatedNavHost

Заменять import androidx.navigation.compose.navigationс import com.google.accompanist.navigation.animation.navigation

Заменять import androidx.navigation.compose.composableс import com.google.accompanist.navigation.animation.composable

Не добавляя еще один пакет, по состоянию на декабрь 2023 г. вы можете удалить анимацию по умолчанию следующим образом.

      NavHost(
    navController = navController,
    startDestination = HOME_ROUTE,
    route = ROOT_ROUTE,
    enterTransition = {
        EnterTransition.None // removes the blinking/fade effect
    },
    exitTransition = {
        ExitTransition.None // remove the blinking/fade effect
    }
) {
    // composables
}

Поместите это в свой корневой навигационный хост. Вот версия, которую я использую (по состоянию на 2 декабря 2023 г.)

      implementation("androidx.navigation:navigation-compose:2.7.5")

В качестве альтернативы удалению анимации вы можете изменить анимацию, например:

      @Composable
private fun ScreenContent() {
    val navController = rememberAnimatedNavController()
    val springSpec = spring<IntOffset>(dampingRatio = Spring.DampingRatioMediumBouncy)
    val tweenSpec = tween<IntOffset>(durationMillis = 2000, easing = CubicBezierEasing(0.08f, 0.93f, 0.68f, 1.27f))
    ...
    ) { innerPadding ->
        AnimatedNavHost(
            navController = navController,
            startDestination = BottomNavDestinations.TimerScreen.route,
            enterTransition = { slideInHorizontally(initialOffsetX = { 1000 }, animationSpec = springSpec) },
            exitTransition = { slideOutHorizontally(targetOffsetX = { -1000 }, animationSpec = springSpec) },
            popEnterTransition = { slideInHorizontally(initialOffsetX = { 1000 }, animationSpec = tweenSpec) },
            popExitTransition = { slideOutHorizontally(targetOffsetX = { -1000 }, animationSpec = tweenSpec) },
            modifier = Modifier.padding(innerPadding)
    ) {}

Проблема в том, что представление в NavHost имеет анимацию плавного затухания по умолчанию. Вы должны переопределить его, чтобы остановить мигание, как показано в примере ниже.noEnterTransitionиnoExistTransitionдляMainScreen.

      class MainActivity : ComponentActivity() {
    private val noEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition =
        {
            fadeIn(
                animationSpec = tween(durationMillis = 200),
                initialAlpha = 0.99f
            )
        }

    private val noExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition =
        {
            fadeOut(
                animationSpec = tween(durationMillis = 300),
                targetAlpha = 0.99f
            )
        }

    /.../

    override fun onCreate(savedInstanceState: Bundle?) {
        setContent {
            val navController = rememberNavController()
            YourTheme() {

                Surface(color = MaterialTheme.colorScheme.background) {
                    NavHost(navController = navController, startDestination = "main") {
                        composable(
                            route = "main",
                            popEnterTransition = noEnterTransition,
                            exitTransition = noExitTransition,
                            popExitTransition = noExitTransition
                        ) {
                            // MainScreen(navController = navController)
                        }

                        composable(
                            route = "detail",
                            enterTransition = {
                                slideIntoContainer(
                                    towards = AnimatedContentTransitionScope.SlideDirection.Companion.Left,
                                    animationSpec = tween(200, easing = EaseOut)
                                )
                            },
                            popExitTransition = {
                                slideOutOfContainer(
                                    towards = AnimatedContentTransitionScope.SlideDirection.Companion.Right,
                                    animationSpec = tween(150, easing = EaseOut)
                                )
                            },
                        ) {
                            // DetailScreen(navController = navController)
                        }
                    }
                }
            }
        }
    }
}