如何使用Jetpack Compose创建一个可折叠的底部导航栏

848 阅读12分钟

如何使用Jetpack Compose创建一个可折叠的底部导航栏

导航是任何移动应用程序的一个重要组成部分。然而,要把它做好是很有挑战性的。许多挑战与处理应用程序生命周期的各个方面有关,深层链接、后栈处理和状态保存,仅举几例。

这意味着你只需专注于构建功能,而少花时间创建你想在Android应用程序中展示的用户界面。

本指南将使用Jetpack Compose来创建Android屏幕。我们将使用Jetpack Compose Navigation组件创建collapsed Bottom Navigation

前提条件

  • 确保你的电脑上安装了最新版本的Android Studio。
  • Jetpack Compose有一些基本了解。
  • 我们将在这个应用程序中使用Kotlin。编写Kotlin代码的良好经验将是必不可少的。

在Jetpack组件之前

在过去,每当你想在屏幕上导航时,你必须使用像startActivity()Intent 。这将帮助你通过你的应用程序打开另一个活动。

如果你使用Fragments,你必须使用FragmentTransactions 来浏览不同的片段。另外,你有可能在使用FragmentManagerSupportFragmentManager 之间感到困惑。

浏览这些屏幕仅仅是冰山一角。请记住,你必须管理你的应用程序的生命周期,使用ChildFragmentManager ,控制后端堆栈(s)。然而,只要适合你的应用程序范围,这并不是一个坏方法。

在这个方法中,应用程序的设计布局、视图和元素是用XML代码管理的。然后使用findViewById() ,从各种布局文件中用其独特的ID访问和inflater 不同的视图。

新的Jetpack Compose时代

Jetpack Compose是一个用于Android应用开发的现代本地UI工具包。它是一种使用Kotlin设计原生Android应用的新方式,不需要XML。设计逻辑是在Kotlin中实现的。

这种方法很直观,从头开始构建,以加速开发,让你用更少的代码编写UI。这是一个激进的新设计,专注于为每一种风格提供个性化的体验,为每一种需求提供便利,并为每一个屏幕提供适应性。

使用Jetpack Compose,屏幕被称为Composables。

Jetpack Compose还使你的应用程序之间的导航变得简单和容易。它允许你使用Jetpack Compose导航组件来浏览不同的屏幕。这引入了对多个后堆栈的支持。

导航组件可以在底部导航项目之间进行切换,每个项目都保持自己的状态。本指南创建了一个基本的可折叠的底部导航,以帮助你了解更多关于Jetpack Compose Navigation的信息。

创建一个Jetpack Compose应用程序

首先,前往你的Android Studio,创建一个新的Android项目。Android为你提供了一个Jetpack Compose模板应用程序。由于我们正在使用Jetpack Compose,我们将选择一个empty compose activity ,如下图所示。

Empty compose activity

然后在你的app.gradle 文件中添加一个 Compose Navigation 依赖关系。

//Compose Navigation
def nav_compose_version = "2.4.0-alpha10"
implementation "androidx.navigation:navigation-compose:$nav_compose_version"

这个依赖关系将帮助我们定义通过不同屏幕的路由概念。这意味着你可以从一个屏幕导航到另一个屏幕,也就是在可合成物之间导航。它还有助于维护应用程序的后堆栈状态,比如当你点击后退按钮时,你会回到前一个屏幕。

为导航栏项目创建一个模型类

要设置底部导航栏,你首先需要关于它应该显示的项目的信息。在我们的案例中,我们希望在底部导航栏中显示four items 。每个项目都需要一些参数,如图标、标题、项目路线等。

我们将创建一个类来定义这些项目并保存其属性。让我们首先创建一个模型,用来保存底部导航栏的项目和参数。

在我们的根项目包中,创建一个新的包并将其命名为navigationBar 。在navigationBar 包内,创建一个新的Kotlin类文件,命名为NavigationBarItems

kotlin class

使其成为一个sealed Class

kotlin sealed class

NavigationBarItems 数据类将包含三个属性。

  • route - 这是个字符串/键,定义了通往可组合的路径。它必须是唯一的,才能作为一个键发挥作用。它将帮助我们在底部导航栏中导航视图。你可以把 作为一个URL,帮助你从一个页面导航到另一个。route
  • icon - 具有每个底部导航栏项目的 的图标ImageVector
  • title - 每个底部导航栏项目的名称

这就是我们将如何把它们添加到我们的类中。

sealed class NavigationBarItems(val route: String, val title: String, val icon: ImageVector)

我们的底部导航栏中的每个屏幕都有一个iconroute 、和一个title

让我们为这四个不同的屏幕定义对象。

{
    object Home: NavigationBarItems("home", "Home", Icons.Filled.Home)
    object Categories: NavigationBarItems("categories", "Categories", Icons.Filled.List)
    object Cart: NavigationBarItems("cart", "Cart", Icons.Default.ShoppingCart)
    object Account: NavigationBarItems("account", "Account", Icons.Filled.Person)
}

在这种情况下,每个屏幕将继承自NavigationBarItems 屏幕,然后为该屏幕传递值。例如,我们有Home 屏幕,home 作为路线,Home 作为标题,以及导入Icons.Filled.Home 作为屏幕图标。

最后,创建一个这些项目的集合,作为一个给定元素的列表。返回的列表是可序列化的(JVM)。我们将使用这个列表来迭代每个屏幕项目。

val bottomNavigationItems = listOf(
    NavigationBarItems.Home,
    NavigationBarItems.Categories,
    NavigationBarItems.Cart,
    NavigationBarItems.Account,
)

设置每个导航栏项目的屏幕

每个项目将导航到一个不同的屏幕。让我们创建四个不同的屏幕,容纳一个特定的屏幕。当点击时,每个项目就会导航到一个新的不同屏幕。

在我们的根项目包中,创建一个新的包,并将其命名为screens 。在screens 包内,创建四个不同的新Kotlin文件,即。

  1. CategoriesScreen

将这段代码添加到CategoriesScreen.kt 。这个屏幕将有一个Box ,其中有简单的"Categories Screen" 文本。

@Composable
fun CategoriesScreen() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ){
        Text(text = "Categories Screen",
            style = TextStyle(color = Color.Black, fontSize = 10.sp),
            textAlign = TextAlign.Center)
    }
}
  1. CartScreen

将这段代码添加到CartScreen.kt 。这个屏幕将有一个Box ,其中有一个简单的"Cart Screen" 文本。

@Composable
fun CartScreen() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ){
        Text(text = "Cart Screen",
            style = TextStyle(color = Color.Black, fontSize = 10.sp),
            textAlign = TextAlign.Center)
    }
}
  1. 帐户屏幕

将此代码添加到AccountScreen.kt 。此屏幕将有一个Box ,其中有一个简单的"Account Screen" 文本。

@Composable
fun AccountScreen() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ){
        Text(text = "Account Screen",
            style = TextStyle(color = Color.Black, fontSize = 10.sp),
            textAlign = TextAlign.Center)
    }
}
  1. 主页屏幕

我们正在建立一个可折叠的底部导航栏。这意味着我们将不得不添加一个嵌套滚动。因此,一个屏幕控制器将听取屏幕周围的移动,以决定何时折叠栏。

在这种情况下,我们将在Home 屏幕上添加一个时间列表。这样,我们将能够根据屏幕滚动的方向,在导航变得可见或不可见时向下滚动该列表。

让我们在主屏幕中添加一些可合成的东西。

@ExperimentalMaterialApi
@Composable
fun HomeScreen(innerPadding: PaddingValues) {
    // Describes a padding to be applied along the edges inside a box
    LazyColumn(contentPadding = innerPadding) {
        // Repeat a single ItemView with 20 rows
        items(count = 20) {
            ItemView()
        }
    }
}
@ExperimentalMaterialApi
@Composable
private fun ItemView() {
    // Add the ItemView Card
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .height(150.dp)
            .padding(start = 10.dp, end = 10.dp, top = 5.dp, bottom = 5.dp),
        elevation = 10.dp,
        shape = RoundedCornerShape(5.dp)
    ) {
        Column(
            modifier = Modifier.padding(10.dp)
        ) {
            // Within this Column scope, add a child layout a Row with an image
            Row(
                verticalAlignment = Alignment.CenterVertically
            ) {
                Image(
                    rememberVectorPainter(image = Icons.Rounded.Done),
                    contentDescription = "A dummy image",
                    contentScale = ContentScale.Crop,
                    modifier = Modifier
                        .size(80.dp)
                        .clip(CircleShape)
                )
                Spacer(modifier = Modifier.padding(5.dp))
                // Within this Row scope, add a child layout - two Columns each with a dummy text
                Column {
                    Text(
                        text = "This is a sample title",
                        fontSize = 18.sp,
                        fontWeight = FontWeight.Bold
                    )
                    Spacer(modifier = Modifier.padding(2.dp))
                    Text(
                        text = "This is simply a dummy text. Let's create a collapsible Bottom Navigation using Jetpack Compose Navigation",
                        color = Color.Gray,
                        fontSize = 14.sp
                    )
                }
            }
        }
    }
}

在这里,我们只是简单地添加一个卡片,其中有一排矢量图片和两列虚拟文本。我们这样做不需要任何XML代码。通过Composable ,我们可以创建我们想要显示的不同屏幕。这使我们更容易设计元素和不同的视图。

上面的卡片的工作原理与XML中的RecyclerView widget差不多。唯一的区别是,我们不需要使用XML布局和布局管理器来安排这些元素的位置。我们也没有模型来保存每个项目的视图,也没有适配器来将对象的列表转换成视图的列表并重用它们。

我们简单地添加Card,将一个图片视图作为行包装到该Card中,并将两个文本作为列包装到该Card中。这将建立一个单一的Card视图。然后我们对这个Card进行充气,重用并重复那个有20行的单一ItemView。

Jetpack Compose Navigation的主要组件

让我们来讨论一些我们可以在Jetpack Compose中使用的主要导航组件。它们包括NavControllerNavHost 。让我们一边讨论它们,一边实现它们来设置一个底部导航。

NavHost

它定义了导航图。这设置了所有的屏幕、路线和参数。将NavHost 视为一个图,在这里表示可用于导航的不同节点。在这种情况下,把这些节点看作是通往其他可合成物/屏幕的路线。

让我们添加一个NavHost来详细了解这个。导航到navigationBar 包,创建一个新的Kotlin文件,并将其命名为BottomScreenNavHost

下面是我们将如何执行不同的路由来访问各自的可组成。

@ExperimentalMaterialApi
@Composable
fun BottomScreenNavHost(innerPadding: PaddingValues, navController: NavHostController) {
    NavHost(navController = navController, startDestination = NavigationBarItems.Home.route) {
        (NavigationBarItems.Home.route) {
            HomeScreen(innerPadding)
        }
        composable(NavigationBarItems.Categories.route) {
            CategoriesScreen()
        }
        composable(NavigationBarItems.Cart.route) {
            CartScreen()
        }
        composable(NavigationBarItems.Account.route) {
            AccountScreen()
        }
    }
}

NavHost 来定义你的路由,有助于应用程序理解你正在执行另一个可组合的。例如,如果你需要从HomeScreen() 移动到CartScreen() ,你需要调用执行各自可组合的route ,即NavigationBarItems.Cart.route 。这样,它就能理解你是要执行可组合的CartScreen()

NavController

它的主要功能是跟踪应用程序的后堆栈和可组合/屏幕的状态。这使得视图即使在被销毁或重新创建后也能保持在内存中。

NavController 它可以访问上面的 composables,以跟踪back stack条目,膨胀项目,以及对这些项目的点击处理程序。NavHost

要创建一个NavController ,导航到navigationBar 包,并创建一个新的Kotlin文件,命名为BottomScreenNavController 。下面是我们将如何执行不同的路由来访问各自的可组成。

@Composable
fun BottomScreenNavController(navController: NavHostController) {
    BottomNavigation {
        val navBackStackEntry by navController.currentBackStackEntryAsState()
        val currentDestination = navBackStackEntry?.destination
        bottomNavigationItems.forEach { screen ->
            BottomNavigationItem(
                selectedContentColor = Teal200,
                alwaysShowLabel = true,
                icon = {
                    Column(horizontalAlignment = Alignment.CenterHorizontally) {
                        Icon(
                            imageVector = screen.icon,
                            contentDescription = screen.title
                        )
                        if (currentDestination?.hierarchy?.any { it.route == screen.route } == true) {
                            Text(
                                text = screen.title,
                                textAlign = TextAlign.Center,
                                fontSize = 10.sp
                            )
                        }
                    }
                },
                selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,

                // handle bottom bar items onClick
                onClick = {
                    navController.navigate(screen.route) {
                        // Pop up to the start destination of the graph to
                        // avoid building up a large stack of destinations
                        // on the back stack as users select items
                        popUpTo(navController.graph.findStartDestination().id) {
                            saveState = true
                        }
                        // Avoid multiple copies of the same destination when
                        // re selecting the same item
                        launchSingleTop = true
                        // Restore state when re selecting a previously selected item
                        restoreState = true
                    }
                })
        }
    }
}

在这里,我们正在添加一个预定义的可组合函数,BottomNavigation ,负责创建底部导航栏,其中包含bottomNavigationItems

在这个navController ,我们正在调用currentBackStackEntryAsState() 。这将观察导航模式,一旦后堆栈发生变化,它就会重新组合navController ,并用新的State 值来更新导航。

我们将观察navBackStackEntry 。每当它的值发生变化时,navController 就会被通知,并且当前屏幕中更新的State 应该对用户可见。

BottomNavigationItem 是另一个预定义的可组合函数。它接受的参数包括: (一个可组合的函数)、 、 、 、和 。icon onClick selected alwaysShowLabel selectedContentColor

  • icon - 将设置每个项目imageVector 和标题。
  • selected - 每当选择一个底部导航项目时,将通知 。navController

每当currentDestination 的项目hierarchy 有一个活动的屏幕路线时,与该路线相关的项目将是当前的选择和活动的可组成/屏幕。

  • selectedContentColor - 将为活动的 项目添加内容颜色。selected
  • alwaysShowLabel - 它可以是true或false来显示项目标题何时可见。
  • onClick - 每当一个项目被点击,我们希望 ,以导航方式执行新选择的路线,并保存其 ,以便在回叠时可以恢复。navController State

折叠导航栏

我们的导航组件已经设置好了,我们可以设置一个可组合的bottomBar ,使整个底部导航栏可以折叠。导航到navigationBar 包并创建一个新的Kotlin文件,命名为BottomCollapse

下面是我们将如何设置这个bottomBar 可折叠。

@ExperimentalMaterialApi
@Composable
 fun BottomCollapse() {
    val bottomBarHeight = 55.dp
    val bottomBarHeightPx = with(LocalDensity.current) {
        bottomBarHeight.roundToPx().toFloat()
    }
    val bottomBarOffsetHeightPx = remember { mutableStateOf(0f) }
    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(
                available: Offset,
                source: NestedScrollSource
            ): Offset {
                val delta = available.y
                val newOffset = bottomBarOffsetHeightPx.value + delta
                bottomBarOffsetHeightPx.value =
                    newOffset.coerceIn(-bottomBarHeightPx, 0f)
                return Offset.Zero
            }
        }
    }
    val scaffoldState = rememberScaffoldState()
    val navController = rememberNavController()
    Scaffold(
        modifier = Modifier.nestedScroll(nestedScrollConnection),
        scaffoldState = scaffoldState,
        topBar = {
            TopAppBar(
                title = {
                    Text(
                        text = "Collapse Bottom Navigation",
                        textAlign = TextAlign.Center,
                        modifier = Modifier.fillMaxWidth()
                    )
                })
        },
        bottomBar = {
            BottomAppBar(
                modifier = Modifier
                    .height(bottomBarHeight)
                    .offset {
                        IntOffset(
                            x = 0,
                            y = -bottomBarOffsetHeightPx.value.roundToInt())
                    }) {
                BottomScreenNavController(navController)
            }
        }) { innerPadding ->
        BottomScreenNavHost(innerPadding, navController)
    }
}

在这里,我们要添加一个bottomBarHeight ,它应该是最大的可见bottomBar ,它应该是可变的。

首先,我们需要创建一个接口,使用对象NestedScrollConnection ,连接到嵌套的滚动系统。实现这个连接允许对嵌套的滚动相关事件做出反应,并影响滚动的子代和父代。

一个onPreScroll() 事件允许父屏幕根据滚动事件的来源(子代和父代)消耗一部分拖动事件。一旦确定了滚动源,我们将在bottomBar 内创建一个Offset

最后,我们把所有东西都包在一个Scaffold 。一个支架是一个Jetpack Compose布局,它使用与XMLRelativeLayoutLinearLayout 相同的布局元素。它仍然使用诸如顶栏、底栏和导航抽屉等组件。

使用Scaffold ,我们将在适当的地方设置一切。我们添加一个topBar 和一个bottomBar

请注意,我们还没有为这些可合成的东西指定对齐方式。topBar 将明确地将该组合物置于应用程序的顶部栏。一个bottomBar 将精确地把这个组合物作为应用程序的底栏。

在引擎盖下,Scaffold 知道将顶栏、正文内容和底部导航放在哪里。

由于bottomBar 正在执行一个Bottom screen,我们将在这里添加所有的参数,如nestedScrollConnection,NavHost, 和NavController

在这种情况下,nestedScrollConnection 从x和y位置设置为ModifierBottomAppBarIntOffset 。第一个参数设置x,水平分量,第二个参数设置y,垂直分量。当一个滚动事件被识别时,这些参数将调整bottomBar 可变的高度,使该栏可折叠。

有了这个设置,你可以在你的MainActivity.kt'ssetContent 中调用BottomCollapse() ,如下所示。

setContent {
    // Note: CollapsibleBottomNavigationUsingJetpackComposeTheme is derived
    // from the name you gave your application when setting up a new project
    CollapsibleBottomNavigationUsingJetpackComposeTheme{
        // A surface container using the 'background' color from the theme
        Surface(color = MaterialTheme.colors.background) {
            BottomCollapse()
        }
    }
}

注意:CollapsibleBottomNavigationUsingJetpackComposeTheme 可以不同。这个名字来自于你在设置新项目时给你的应用程序起的名字。

你的应用程序现在已经设置好了,你可以运行它来进行测试。

总结

Jetpack Compose是一项了不起的技术,它正在彻底改变你编写应用程序视图和屏幕的方式。