做主頁導航時會用到底部導航欄,Jetpack Compose提供了基礎槽位的布局Scaffold,使用Scaffold可以構建底部導航欄,例如:
@Composable
fun Greeting(vm: VM) {
val list = listOf("One", "Two", "Three")
var selectedItem = remember {
mutableStateOf(0)
}
val navController = rememberNavController()
Scaffold(bottomBar = {
state.takeIf { it.value }?.let {
BottomNavigation {
list.forEachIndexed { index, label ->
BottomNavigationItem(
label = { Text(text = label) },
selected = index == selectedItem.value,
onClick = { selectedItem.value = index },
icon = {
Icon(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = null
)
})
}
}
}
}) {
NavHost(navController = navController, startDestination = "one") {
composable(route = "one") { PageList(navController, vm) }
composable(route = "detail") { PageDetail(vm) }
}
}
}
這是一個最簡單的Scaffold,其主頁時PageList,顯示一列數字,點擊數字后會跳轉到PageDetail頁面。
但是有個很大的問題,就是在跳轉到PageDetail頁面之后,BottomNavigation並沒有隨之消失,於是乎出現了這樣一個奇怪的現象:
二更。
其實核心思路是判斷最新的頁面是不是包含Scaffold的頁面(即MainPage),用那種方式判斷並不重要。
今天又發現一種新的判斷思路,不用ViewModel,直接用全局棧保存頁面跳轉即可,而且只需讓棧底保存MainPage的標志位即可,跳轉目的地的標志位不重要。
```
object NavUtil {
private val routeStack: Stack<String> = Stack()
private var mainRoutes = listOf<String>("Main")
fun setMainRoutes(list: List<String>) {
mainRoutes = list
}
init {
routeStack.push("Main")
}
fun isMain(): Boolean {
Log.d("TEST", "routeStack: ${routeStack.empty()}")
val result = if (routeStack.empty()) {
true
} else {
Log.d("TEST", "peek: ${routeStack.peek()}")
mainRoutes.contains(routeStack.peek())
}
return result
}
fun pushRoute() {
routeStack.push("destination")
}
fun popRoute() {
if (!routeStack.empty()) {
routeStack.pop()
}
}
}
@ExperimentalPagerApi
@Composable
fun MainView(navController: NavHostController) {
LaunchedEffect(key1 = true) {
NavUtil.popRoute()
}
val data = listOf("One", "Two", "Three")
val scope = rememberCoroutineScope()
val pagerState = rememberPagerState(pageCount = 3)
val bottomNavSelectedState = remember { mutableStateOf(0) }
Scaffold(bottomBar = {
var status = NavUtil.isMain()
Log.d("TEST", "status: ${status}")
if (status) {
BottomNavigation { }
}
}) {
HorizontalPager(state = pagerState) {
PageNav(navController = navController, title = data[pagerState.currentPage])
}
}
DisposableEffect(key1 = true) {
onDispose {
NavUtil.pushRoute()
}
}
}
```
為了解決這個問題,可以采用State去控制BottomNavigation的可見性,並將其保存在ViewModel中。
具體做法是:
1.在ViewModel中創建一個包含Boolean值的LiveData變量state。當state為true時繪制BottomNavigation,為false時不繪制
2.在包含Scaffold頁面中監聽state,並控制BottomNavigation的可見性。
3.在PageList(也就是Scaffold導航的主頁)進入時設置state為true、退出時設置state為false
// ViewModel
class VM: ViewModel() {
private val _state: MutableLiveData<Boolean> = MutableLiveData(true)
val state: LiveData<Boolean> get() = _state
fun setState(status: Boolean) {
_state.postValue(status)
}
}
// MainPage
@Compose MainPage(vm: VM) {
LaunchedEffect(key1 = true) {
vm.setState(true)
}
DisposableEffect(key1 = true) {
onDispose {
vm.setState(false)
}
}
}
// page contains Scaffold
@Composable
fun Greeting(vm: VM) {
// State of BottomNavigation`s visibility
val state = remember { mutableStateOf<Boolean>(true) }
// read the BottomNavigation`s visibility from ViewModel and send to State
vm.state.observeAsState().value?.let { state.value = it }
Scaffold(bottomBar = {
// show / hide BottomNavigation controlled by State
state.takeIf { it.value }?.let {
BottomNavigation {
list.forEachIndexed { index, label ->
BottomNavigationItem(
label = { Text(text = label) },
selected = index == selectedItem.value,
onClick = { selectedItem.value = index },
icon = {
Icon(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = null
)
})
}
}
}
}) {
NavHost(navController = navController, startDestination = "one") {
composable(route = "one") { PageList(navController, vm) }
composable(route = "detail") { PageDetail(vm) }
}
}
}
這種做法的好處是簡單,侵入性低,無需修改系統api也無需自定義view。缺點就是麻煩,需要在導航中的每個主頁都進行設置。
我在StackOverflow上提問時有人回答了另一個辦法。這個辦法是給每個屏幕添加標志位,來區分是否是導航的主頁,之后再創建BottomNavigation時進行判斷。貼一下:
You need to specify which screens you want to show and which screens you dont want; Otherwise it will show to all the screens inside Scaffold's body (which you have bottomBar). The code below was from my app.
Create a state which observes any destination changes on the navController
Inside when you can put any screens that you want to show navigationBar else just set currentScreen to NoBottomBar
@Composable
private fun NavController.currentScreen(): State<MainSubScreen> {
val currentScreen = remember { mutableStateOf<MainSubScreen>(MainSubScreen.Home) }
DisposableEffect(key1 = this) {
val listener = NavController.OnDestinationChangedListener { _, destination, _ ->
when {
destination.hierarchy.any { it.route == MainSubScreen.Home.route } -> {
currentScreen.value = MainSubScreen.Home
} else -> currentScreen.value = MainSubScreen.NoBottomBar
}
}
addOnDestinationChangedListener(listener)
}
return currentScreen
}
On the Scaffold where you put ur bottomBar
so you can check if currentScreen was NoBottomBar if it was, don't show it
// initialized currentScreeen above
val currentScreen by navController.currentScreen()
Scaffold(
bottomBar = {
if (currentScreen != MainSubScreen.NoBottomBar) {
MainBottomNavigation()
} else Unit
}
) {
// Your screen
}