2022年11月25日金曜日

BottomNavigation / NavigationBar が消える時にレイアウトが下にずれるのを防ぎたい

このように Compose Navigation で BottomNavigation / NavigationBar を表示しない画面に遷移したときに、現在のレイアウトが下にずれてしまうことがあります。
これを防ぐ方法を紹介します。

1. NavHost に innerPadding を使わない

Scaffold( ... ) { innerPadding -> NavHost( modifier = Modifier.padding(innerPadding), // これはダメ ... ) { ... NavHost の中の Composable で innerPadding を使うようにします。 Scaffold( ... ) { innerPadding -> val modifier = Modifier.padding(innerPadding) NavHost( ... ) { composable(Destination.Home.route) { HomeScreen( onClick = { navController.navigate("profile") }, modifier = modifier, // こうする ) } ... @Composable private fun HomeScreen( onClick: () -> Unit, modifier: Modifier = Modifier, ) { Box(modifier = modifier.fillMaxSize()) { ... } } このとき currentBackStack が null でも BottomNavigation / NavigationBar が表示されるようにしないと bottom padding が 0 になる問題があるので注意してください。 val navBackStackEntry by navController.currentBackStackEntryAsState() // ここで ?: Destination.Home.route をつけないと HomeScreen() に渡される Modifier で bottom padding が 0 になってしまう val currentRoute = navBackStackEntry?.destination?.route ?: Destination.Home.route val routes = remember { Destination.values().map { it.route } } val showNavigationBar = currentRoute in routes if (showNavigationBar) { BottomNavigation { ...

2. BottomNavigation / NavigationBar が表示される画面を nested graph にする

これを NavHost( navController = navController, startDestination = Destination.Home.route ) { composable(Destination.Home.route) { HomeScreen( onClick = { navController.navigate("profile") }, modifier = modifier, ) } composable(Destination.Settings.route) { SettingsScreen( onClick = { navController.navigate("profile") }, modifier = modifier, ) } composable("profile") { ProfileScreen( modifier = modifier, ) } } こうする NavHost( navController = navController, startDestination = "main" ) { // BottomNavigation / NavigationBar が表示される画面を nested graph にする navigation( route = "main", startDestination = Destination.Home.route ) { composable(Destination.Home.route) { HomeScreen( onClick = { navController.navigate("profile") }, modifier = modifier, ) } composable(Destination.Settings.route) { SettingsScreen( onClick = { navController.navigate("profile") }, modifier = modifier, ) } } composable("profile") { ProfileScreen( modifier = modifier, ) } } これで、下にずれなくなります。
最後に全体のコードを置いておきます。 enum class Destination(val title: String, val route: String, val imageVector: ImageVector) { Home("Home", "home", Icons.Filled.Home), Settings("Settings", "settings", Icons.Filled.Settings) } @OptIn(ExperimentalMaterial3Api::class) @Composable fun MainScreen() { val navController = rememberNavController() Scaffold( topBar = { TopAppBar( title = { Text("Main") } ) }, bottomBar = { val navBackStackEntry by navController.currentBackStackEntryAsState() val routes = remember { Destination.values().map { it.route } } val currentRoute = navBackStackEntry?.destination?.route ?: Destination.Home.route val showNavigationBar = currentRoute in routes if (showNavigationBar) { NavigationBar { Destination.values().forEach { destination -> NavigationBarItem( icon = { Icon(destination.imageVector, contentDescription = null) }, label = { Text(destination.title) }, selected = currentRoute == destination.route, onClick = { navController.navigate(destination.route) { popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true restoreState = true } } ) } } } } ) { innerPadding -> val modifier = Modifier.padding(innerPadding) NavHost( navController = navController, startDestination = "main" ) { navigation( route = "main", startDestination = Destination.Home.route ) { composable(Destination.Home.route) { HomeScreen( onClick = { navController.navigate("profile") }, modifier = modifier, ) } composable(Destination.Settings.route) { SettingsScreen( onClick = { navController.navigate("profile") }, modifier = modifier, ) } } composable("profile") { ProfileScreen( modifier = modifier, ) } } } } @Composable private fun HomeScreen( onClick: () -> Unit, modifier: Modifier = Modifier, ) { Box(modifier = modifier.fillMaxSize()) { Text("home") Button(onClick = onClick, modifier = Modifier.align(Alignment.BottomCenter)) { Text("button") } } } @Composable private fun SettingsScreen( onClick: () -> Unit, modifier: Modifier = Modifier, ) { Box(modifier = modifier.fillMaxSize()) { Text("settings") Button(onClick = onClick, modifier = Modifier.align(Alignment.BottomCenter)) { Text("button") } } } @Composable private fun ProfileScreen( modifier: Modifier = Modifier, ) { Box(modifier = modifier.fillMaxSize()) { Text("profile") } }


2022年11月15日火曜日

Material 3 で TopAppBar の shadow がなくなったけど、コンテンツとの境界はどう表現する?

Material 2 では TopAppBar に shadow がついていて、スクロールアウトするコンテンツとの境界として機能していました。


Material 3 では shadow はつきません。そのため、Material 2 のコードをそのまま Material 3 に移行すると、コンテンツの境界表現がなくなってしまいます。
Material 3 では shadow の代わりに色のオーバーレイでコンテンツと分離します。
そのために TopAppBarScrollBehavior を使います。 val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() Scaffold( topBar = { TopAppBar( title = { ... }, scrollBehavior = scrollBehavior ) }, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) ) { LazyColumn( contentPadding = it ) { ... } }
TopAppBarScrollBehavior として
  • PinnedScrollBehavior
  • EnterAlwaysScrollBehavior
  • ExitUntilCollapsedScrollBehavior
が用意されています。

TopAppBarDefaults.pinnedScrollBehavior()

TopAppBarDefaults.enterAlwaysScrollBehavior()

ExitUntilCollapsedScrollBehavior