zoukankan      html  css  js  c++  java
  • Jetpack Compose之隐藏Scaffold的BottomNavigation

    做主页导航时会用到底部导航栏,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
        }
    
  • 相关阅读:
    创建可管理的对象属性
    解析简单xml文档
    定义类的__slots__属性节省内存的开销
    读写json数据
    读写csv,excel文件数据
    常用的字符串处理方法
    sql中case when 的使用
    对字典的处理
    元组的元素命名
    列表,字典,集合按条件筛选
  • 原文地址:https://www.cnblogs.com/lizhenxin/p/15224190.html
Copyright © 2011-2022 走看看