2022年11月25日金曜日

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

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

1. NavHost に innerPadding を使わない

  1. Scaffold(  
  2.     ...  
  3. ) { innerPadding ->  
  4.     NavHost(  
  5.         modifier = Modifier.padding(innerPadding), // これはダメ  
  6.         ...  
  7.     ) {  
  8. .  
NavHost の中の Composable で innerPadding を使うようにします。
  1.     Scaffold(  
  2.         ...  
  3.     ) { innerPadding ->  
  4.         val modifier = Modifier.padding(innerPadding)  
  5.   
  6.         NavHost(  
  7.             ...  
  8.         ) {  
  9.                 composable(Destination.Home.route) {  
  10.                     HomeScreen(  
  11.                         onClick = {  
  12.                             navController.navigate("profile")  
  13.                         },  
  14.                         modifier = modifier, // こうする  
  15.                     )  
  16.                 }  
  17.   ...  
  18.     
  19. @Composable  
  20. private fun HomeScreen(  
  21.     onClick: () -> Unit,  
  22.     modifier: Modifier = Modifier,  
  23. ) {  
  24.     Box(modifier = modifier.fillMaxSize()) {  
  25.         ...  
  26.     }  
  27. }  
このとき currentBackStack が null でも BottomNavigation / NavigationBar が表示されるようにしないと bottom padding が 0 になる問題があるので注意してください。
  1. val navBackStackEntry by navController.currentBackStackEntryAsState()  
  2.   
  3. // ここで ?: Destination.Home.route をつけないと HomeScreen() に渡される Modifier で bottom padding が 0 になってしまう  
  4. val currentRoute = navBackStackEntry?.destination?.route ?: Destination.Home.route  
  5.   
  6. val routes = remember { Destination.values().map { it.route } }  
  7. val showNavigationBar = currentRoute in routes  
  8. if (showNavigationBar) {  
  9.     BottomNavigation {  

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

これを
  1. NavHost(  
  2.     navController = navController,  
  3.     startDestination = Destination.Home.route  
  4. ) {  
  5.     composable(Destination.Home.route) {  
  6.         HomeScreen(  
  7.             onClick = {  
  8.                 navController.navigate("profile")  
  9.             },  
  10.             modifier = modifier,  
  11.         )  
  12.     }  
  13.   
  14.     composable(Destination.Settings.route) {  
  15.         SettingsScreen(  
  16.             onClick = {  
  17.                 navController.navigate("profile")  
  18.             },  
  19.             modifier = modifier,  
  20.         )  
  21.     }  
  22.   
  23.     composable("profile") {  
  24.         ProfileScreen(  
  25.             modifier = modifier,  
  26.         )  
  27.     }  
  28. }  
こうする
  1. NavHost(  
  2.     navController = navController,  
  3.     startDestination = "main"  
  4. ) {  
  5.     // BottomNavigation / NavigationBar が表示される画面を nested graph にする  
  6.     navigation(  
  7.         route = "main",  
  8.         startDestination = Destination.Home.route  
  9.     ) {  
  10.         composable(Destination.Home.route) {  
  11.             HomeScreen(  
  12.                 onClick = {  
  13.                     navController.navigate("profile")  
  14.                 },  
  15.                 modifier = modifier,  
  16.             )  
  17.         }  
  18.   
  19.         composable(Destination.Settings.route) {  
  20.             SettingsScreen(  
  21.                 onClick = {  
  22.                     navController.navigate("profile")  
  23.                 },  
  24.                 modifier = modifier,  
  25.             )  
  26.         }  
  27.     }  
  28.   
  29.     composable("profile") {  
  30.         ProfileScreen(  
  31.             modifier = modifier,  
  32.         )  
  33.     }  
  34. }  
これで、下にずれなくなります。
最後に全体のコードを置いておきます。
  1. enum class Destination(val title: String, val route: String, val imageVector: ImageVector) {  
  2.     Home("Home""home", Icons.Filled.Home),  
  3.     Settings("Settings""settings", Icons.Filled.Settings)  
  4. }  
  5.   
  6. @OptIn(ExperimentalMaterial3Api::class)  
  7. @Composable  
  8. fun MainScreen() {  
  9.     val navController = rememberNavController()  
  10.   
  11.     Scaffold(  
  12.         topBar = {  
  13.             TopAppBar(  
  14.                 title = {  
  15.                     Text("Main")  
  16.                 }  
  17.             )  
  18.         },  
  19.         bottomBar = {  
  20.             val navBackStackEntry by navController.currentBackStackEntryAsState()  
  21.             val routes = remember { Destination.values().map { it.route } }  
  22.             val currentRoute = navBackStackEntry?.destination?.route ?: Destination.Home.route  
  23.   
  24.             val showNavigationBar = currentRoute in routes  
  25.             if (showNavigationBar) {  
  26.                 NavigationBar {  
  27.                     Destination.values().forEach { destination ->  
  28.                         NavigationBarItem(  
  29.                             icon = { Icon(destination.imageVector, contentDescription = null) },  
  30.                             label = { Text(destination.title) },  
  31.                             selected = currentRoute == destination.route,  
  32.                             onClick = {  
  33.                                 navController.navigate(destination.route) {  
  34.                                     popUpTo(navController.graph.findStartDestination().id) {  
  35.                                         saveState = true  
  36.                                     }  
  37.                                     launchSingleTop = true  
  38.                                     restoreState = true  
  39.                                 }  
  40.                             }  
  41.                         )  
  42.                     }  
  43.                 }  
  44.             }  
  45.         }  
  46.     ) { innerPadding ->  
  47.         val modifier = Modifier.padding(innerPadding)  
  48.   
  49.         NavHost(  
  50.             navController = navController,  
  51.             startDestination = "main"  
  52.         ) {  
  53.             navigation(  
  54.                 route = "main",  
  55.                 startDestination = Destination.Home.route  
  56.             ) {  
  57.                 composable(Destination.Home.route) {  
  58.                     HomeScreen(  
  59.                         onClick = {  
  60.                             navController.navigate("profile")  
  61.                         },  
  62.                         modifier = modifier,  
  63.                     )  
  64.                 }  
  65.   
  66.                 composable(Destination.Settings.route) {  
  67.                     SettingsScreen(  
  68.                         onClick = {  
  69.                             navController.navigate("profile")  
  70.                         },  
  71.                         modifier = modifier,  
  72.                     )  
  73.                 }  
  74.             }  
  75.   
  76.             composable("profile") {  
  77.                 ProfileScreen(  
  78.                     modifier = modifier,  
  79.                 )  
  80.             }  
  81.         }  
  82.     }  
  83. }  
  84.   
  85. @Composable  
  86. private fun HomeScreen(  
  87.     onClick: () -> Unit,  
  88.     modifier: Modifier = Modifier,  
  89. ) {  
  90.     Box(modifier = modifier.fillMaxSize()) {  
  91.         Text("home")  
  92.         Button(onClick = onClick, modifier = Modifier.align(Alignment.BottomCenter)) {  
  93.             Text("button")  
  94.         }  
  95.     }  
  96. }  
  97.   
  98. @Composable  
  99. private fun SettingsScreen(  
  100.     onClick: () -> Unit,  
  101.     modifier: Modifier = Modifier,  
  102. ) {  
  103.     Box(modifier = modifier.fillMaxSize()) {  
  104.         Text("settings")  
  105.         Button(onClick = onClick, modifier = Modifier.align(Alignment.BottomCenter)) {  
  106.             Text("button")  
  107.         }  
  108.     }  
  109. }  
  110.   
  111. @Composable  
  112. private fun ProfileScreen(  
  113.     modifier: Modifier = Modifier,  
  114. ) {  
  115.     Box(modifier = modifier.fillMaxSize()) {  
  116.         Text("profile")  
  117.     }  
  118. }  



2022年11月15日火曜日

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

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


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

TopAppBarDefaults.pinnedScrollBehavior()

TopAppBarDefaults.enterAlwaysScrollBehavior()

ExitUntilCollapsedScrollBehavior