如何使用Jetpack Compose创建一个可折叠的底部导航栏
导航是任何移动应用程序的一个重要组成部分。然而,要把它做好是很有挑战性的。许多挑战与处理应用程序生命周期的各个方面有关,深层链接、后栈处理和状态保存,仅举几例。
这意味着你只需专注于构建功能,而少花时间创建你想在Android应用程序中展示的用户界面。
本指南将使用Jetpack Compose来创建Android屏幕。我们将使用Jetpack Compose Navigation组件创建collapsed Bottom Navigation
。
前提条件
- 确保你的电脑上安装了最新版本的Android Studio。
- 对Jetpack Compose有一些基本了解。
- 我们将在这个应用程序中使用Kotlin。编写Kotlin代码的良好经验将是必不可少的。
在Jetpack组件之前
在过去,每当你想在屏幕上导航时,你必须使用像startActivity()
和Intent
。这将帮助你通过你的应用程序打开另一个活动。
如果你使用Fragments,你必须使用FragmentTransactions
来浏览不同的片段。另外,你有可能在使用FragmentManager
和SupportFragmentManager
之间感到困惑。
浏览这些屏幕仅仅是冰山一角。请记住,你必须管理你的应用程序的生命周期,使用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
,如下图所示。
然后在你的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
。
使其成为一个sealed Class
。
NavigationBarItems
数据类将包含三个属性。
route
- 这是个字符串/键,定义了通往可组合的路径。它必须是唯一的,才能作为一个键发挥作用。它将帮助我们在底部导航栏中导航视图。你可以把 作为一个URL,帮助你从一个页面导航到另一个。route
icon
- 具有每个底部导航栏项目的 的图标ImageVector
title
- 每个底部导航栏项目的名称
这就是我们将如何把它们添加到我们的类中。
sealed class NavigationBarItems(val route: String, val title: String, val icon: ImageVector)
我们的底部导航栏中的每个屏幕都有一个icon
、route
、和一个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文件,即。
-
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)
}
}
-
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)
}
}
-
帐户屏幕
将此代码添加到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)
}
}
-
主页屏幕
我们正在建立一个可折叠的底部导航栏。这意味着我们将不得不添加一个嵌套滚动。因此,一个屏幕控制器将听取屏幕周围的移动,以决定何时折叠栏。
在这种情况下,我们将在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中使用的主要导航组件。它们包括NavController
和NavHost
。让我们一边讨论它们,一边实现它们来设置一个底部导航。
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布局,它使用与XMLRelativeLayout
或LinearLayout
相同的布局元素。它仍然使用诸如顶栏、底栏和导航抽屉等组件。
使用Scaffold
,我们将在适当的地方设置一切。我们添加一个topBar
和一个bottomBar
。
请注意,我们还没有为这些可合成的东西指定对齐方式。topBar
将明确地将该组合物置于应用程序的顶部栏。一个bottomBar
将精确地把这个组合物作为应用程序的底栏。
在引擎盖下,Scaffold
知道将顶栏、正文内容和底部导航放在哪里。
由于bottomBar
正在执行一个Bottom screen,我们将在这里添加所有的参数,如nestedScrollConnection
,NavHost
, 和NavController
。
在这种情况下,nestedScrollConnection
从x和y位置设置为Modifier
到BottomAppBar
与IntOffset
。第一个参数设置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是一项了不起的技术,它正在彻底改变你编写应用程序视图和屏幕的方式。