Android Jetpack Compose之底部导航栏的实现

目录

  • 1.概述
  • 2. 效果展示
  • 3. 代码实现
    • 3.1 定义底部导航栏的tab项
    • 3.2 整体页面架构搭建
    • 3.3 底部导航栏的实现
    • 3.4 所有代码
  • 4.总结

1.概述

写过一段Android jetpack compose 界面的小伙伴应该都用过Compose的脚手架Scaffold,利用它我们可以很快的实现一个现代APP的主流界面架构,即一个带顶部导航栏和底部导航栏的界面架构,我们基于这个架构可以快速的搭建出我们想要的页面效果。而今天的文章就是要介绍如何实现一个有特点的底部导航栏。底部导航栏一般都是在界面的最底部有可供切换的几个按钮,点击对应的按钮可以切换到对应的页面,例如微信的底部导航栏,分为“微信、通讯录、发现、我”四个选项,这四个选项也比较中规中矩,使用Compose实现起来也很简单,只要配置好按钮和对应的文字就可以。但是如果设计的同学不按常理出牌,比如像咸鱼那样,搞5个按钮,其中有一个还特别大。如下图:
在这里插入图片描述那阁下该如何应对呢。本文就介绍下如何实现这样的底部导航栏。

2. 效果展示

实现其实也不难,只需要设计的小朋友给咱们切一张背景图,就是上图中的带弧形的背景图给我们,我们再绘制到底部导航栏的背后就行了,先看下效果:

在这里插入图片描述

3. 代码实现

3.1 定义底部导航栏的tab项

经过观察我们可以发现底部导航栏的显示有图标和文字,并且选中的时候颜色会变化,所以我们需要定义一个类来保存这些状态,代码如下:

sealed class ScreenPage(val route: String,@StringRes val resId: Int = 0, // 如果没有文字标题,就不需要使用这个属性val iconSelect: Int,val iconUnselect: Int,var isShowText: Boolean = true
) {object Home : ScreenPage(route = "home",resId = R.string.str_main_title_home,iconSelect = R.drawable.ic_home_selected,iconUnselect = R.drawable.ic_home_unselected)object Recommend : ScreenPage(route = "recommend",resId = R.string.str_main_title_recommend,iconSelect = R.drawable.ic_recom_selected,iconUnselect = R.drawable.ic_recom_unselected)object Capture : ScreenPage(route = "add",iconSelect = R.drawable.ic_add_selected,iconUnselect = R.drawable.ic_add_unselected,isShowText = false)object Find : ScreenPage(route = "find",resId = R.string.str_main_title_find,iconSelect = R.drawable.ic_find_selected,iconUnselect = R.drawable.ic_find_unselected)object Mine : ScreenPage(route = "mine",resId = R.string.str_main_title_mine,iconSelect = R.drawable.ic_mine_selected,iconUnselect = R.drawable.ic_mine_unselected)
}

如上面的代码所示,我们在对应的tab中添加上展示的文字的资源ID,选中和未选中的图片资源ID,以及路由,当我们需要切换到其他tab时改变这些属性就可以了,路由可以帮助我们跳转到其他页面。是否显示title的属性可以帮助我们自定义底部Tab的样式
注意:图中的图标资源可以去阿里的矢量图标库下载 阿里矢量图标库地址

3.2 整体页面架构搭建

使用Scaffold搭建页面的架构,这里的Scaffold需要特别注意,我们用到的是material中的Scafold,不是material3中的那个 代码如下:

    val items = listOf(ScreenPage.Home,ScreenPage.Recommend,ScreenPage.Capture,ScreenPage.Find,ScreenPage.Mine)val navController = rememberNavController()val context = LocalContext.currentScaffold(bottomBar = {.....省略底部导航栏的代码,这部分单独介绍......}},backgroundColor = Color.LightGray) { paddingValues ->Log.d("walt-zhong", "paddingValues: $paddingValues")NavHost(navController,startDestination = ScreenPage.Home.route,
//            modifier = Modifier.padding(paddingValues) 
// 加了会导致底部多出一些padding导致影响透明背景的显示) {composable(ScreenPage.Home.route) {HomePage()}composable(ScreenPage.Recommend.route) {RecPage()}composable(ScreenPage.Capture.route) {// CapturePage()}composable(ScreenPage.Find.route) {// FindPage()}composable(ScreenPage.Mine.route) {// MinePage()}}}

我们使用Compose的navigation做页面导航,这里就不介绍相关的知识了,有兴趣的自行百度。然后配置好需要跳转的页面
这里需要注意的是,不要将Scaffold提供的padding值设置给底部导航栏或者是NavHost,因为这样会导致我们的透明背景被遮挡,导致无法显示弧形的底部导航栏效果。

3.3 底部导航栏的实现

底部导航栏的实现主要有背景的绘制,选中tab的状态变更以及对应页面的切换,代码如下:

  BottomAppBar(elevation = 0.dp,backgroundColor = Color.Transparent,contentColor = Color.Transparent,modifier = Modifier.wrapContentHeight().fillMaxWidth().drawWithCache {val bgImg = ContextCompat.getDrawable(context,R.drawable.main_nav_bg)onDrawBehind {bgImg!!.updateBounds(0,0, // 这里可以调整中间的大按钮的上下位置。size.width.toInt(),size.height.toInt())bgImg.draw(drawContext.canvas.nativeCanvas)}}) {val navBackStackEntry by navController.currentBackStackEntryAsState()val currentDestination = navBackStackEntry?.destinationvar isSelected: Booleanitems.forEach { screenPage ->isSelected =currentDestination?.hierarchy?.any { it.route == screenPage.route } == trueCompositionLocalProvider(LocalRippleTheme provides NoRippleTheme) {BottomNavigationItem(selected = isSelected,selectedContentColor = Color(0xFF037FF5),unselectedContentColor = Color(0xFF31373D),onClick = {navController.navigate(screenPage.route) {//点击Item时,清空栈内到NavOptionsBuilder.popUpTo ID之间的所有Item// 避免栈内节点的持续增加,同时saveState用于界面状态的恢复popUpTo(navController.graph.findStartDestination().id) {saveState = true}// 避免多次点击Item时产生多个实列launchSingleTop = true// 当再次点击之前的Item时,恢复状态restoreState = true}},icon = {Image(painter = if (isSelected) {painterResource(screenPage.iconSelect)} else {painterResource(screenPage.iconUnselect)},null,modifier = if (!screenPage.isShowText) {Modifier.size(58.dp)} else {Modifier.size(25.dp)},contentScale = ContentScale.Crop)},alwaysShowLabel = screenPage.isShowText,label =if (!screenPage.isShowText) {null} else {{Text(text = stringResource(screenPage.resId),style = TextStyle(fontSize = 10.sp,fontWeight = FontWeight.Medium,color = if (isSelected) {Color.Yellow} else {Color.Black}))}},modifier = if (screenPage.isShowText) {Modifier.padding(top = 10.dp)} else {Modifier.padding(top = 0.dp)})}}}

上面的代码应该都很好懂,所以我们就只讲下绘制背景部分,其他的读者可以自行阅读代码,绘制背景部分的代码是:

   Modifier.drawWithCache {val bgImg = ContextCompat.getDrawable(context,R.drawable.main_nav_bg)onDrawBehind {bgImg!!.updateBounds(0,0, // 这里可以调整中间的大按钮的上下位置。size.width.toInt(),size.height.toInt())bgImg.draw(drawContext.canvas.nativeCanvas)}
}

这里我们可以使用Modiofier.drawBehind { }方法,但是这个方法会在每次重组的时候重新走一遍,所以我们使用Modifier.drawWithCache来优化它。这里我们将弧形背景绘制到底部导航栏的后面。就呈现出来一个弧形的底部导航栏,这时候我们还需要绘制tab,我们可以根据配置去改变TAB的图标大小和状态。添加动画等。
在这里我们还需要注意的是我们需将底部导航栏BottomAppBar的背景设置成透明的,否则他会影响我们的弧形背景的显示

还有设置文字的时候需要特别注意,如下面的代码所示:

BottomNavigationItem(...省略掉部分不相干代码....alwaysShowLabel = screenPage.isShowText,label =if (!screenPage.isShowText) {null} else {{Text(text = stringResource(screenPage.resId),style = TextStyle(fontSize = 10.sp,fontWeight = FontWeight.Medium,color = if (isSelected) {Color.Yellow} else {Color.Black}))}},modifier = if (screenPage.isShowText) {Modifier.padding(top = 10.dp)} else {Modifier.padding(top = 0.dp)}
)

如上面的代码所示,我们想要底部的部分Tab显示的时候不展示文字,这时就需要将alwaysShowLabel设置成false,但是这时候设置 label的时候,需要设置成null,否则我们的Tab显示会不正常,因为文字部分虽然不显示,但是内容还是占据着UI中的位置,导致不显示文字的TAB位置不正确。

3.4 所有代码

class BottomNavAct : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {MyComposeTheme {// A surface container using the 'background' color from the themeSurface(modifier = Modifier.fillMaxSize(),color = MaterialTheme.colorScheme.background) {MainContainerPage()}}}}
@Composable
fun MainContainerPage() {val items = listOf(ScreenPage.Home,ScreenPage.Recommend,ScreenPage.Capture,ScreenPage.Find,ScreenPage.Mine)val navController = rememberNavController()val context = LocalContext.currentScaffold(bottomBar = {BottomAppBar(elevation = 0.dp,backgroundColor = Color.Transparent,contentColor = Color.Transparent,modifier = Modifier.wrapContentHeight().fillMaxWidth().drawWithCache {val bgImg = ContextCompat.getDrawable(context,R.drawable.main_nav_bg)onDrawBehind {bgImg!!.updateBounds(0,0, // 这里可以调整中间的大按钮的上下位置。size.width.toInt(),size.height.toInt())bgImg.draw(drawContext.canvas.nativeCanvas)}}) {val navBackStackEntry by navController.currentBackStackEntryAsState()val currentDestination = navBackStackEntry?.destinationvar isSelected: Booleanitems.forEach { screenPage ->isSelected =currentDestination?.hierarchy?.any { it.route == screenPage.route } == trueCompositionLocalProvider(LocalRippleTheme provides NoRippleTheme) {BottomNavigationItem(selected = isSelected,selectedContentColor = Color(0xFF037FF5),unselectedContentColor = Color(0xFF31373D),onClick = {navController.navigate(screenPage.route) {//点击Item时,清空栈内到NavOptionsBuilder.popUpTo ID之间的所有Item// 避免栈内节点的持续增加,同时saveState用于界面状态的恢复popUpTo(navController.graph.findStartDestination().id) {saveState = true}// 避免多次点击Item时产生多个实列launchSingleTop = true// 当再次点击之前的Item时,恢复状态restoreState = true}},icon = {Image(painter = if (isSelected) {painterResource(screenPage.iconSelect)} else {painterResource(screenPage.iconUnselect)},null,modifier = if (!screenPage.isShowText) {Modifier.size(58.dp)} else {Modifier.size(25.dp)},contentScale = ContentScale.Crop)},alwaysShowLabel = screenPage.isShowText,label =if (!screenPage.isShowText) {null} else {{Text(text = stringResource(screenPage.resId),style = TextStyle(fontSize = 10.sp,fontWeight = FontWeight.Medium,color = if (isSelected) {Color.Yellow} else {Color.Black}))}},modifier = if (screenPage.isShowText) {Modifier.padding(top = 10.dp)} else {Modifier.padding(top = 0.dp)})}}}},backgroundColor = Color.LightGray) { paddingValues ->Log.d("walt-zhong", "paddingValues: $paddingValues")NavHost(navController,startDestination = ScreenPage.Home.route,// modifier = Modifier.padding(paddingValues) // 加了会导致底部多出一些padding导致影响透明背景的示) {composable(ScreenPage.Home.route) {HomePage()}composable(ScreenPage.Recommend.route) {RecPage()}composable(ScreenPage.Capture.route) {// CapturePage()}composable(ScreenPage.Find.route) {// FindPage()}composable(ScreenPage.Mine.route) {// MinePage()}}}
}object NoRippleTheme : RippleTheme {@Composableoverride fun defaultColor(): Color = Color.Unspecified@Composableoverride fun rippleAlpha(): RippleAlpha =RippleAlpha(0.0f, 0.0f, 0.0f, 0.0f)
}sealed class ScreenPage(val route: String,@StringRes val resId: Int = 0, // 如果没有文字标题,就不需要使用这个属性val iconSelect: Int,val iconUnselect: Int,var isShowText: Boolean = true
) {object Home : ScreenPage(route = "home",resId = R.string.str_main_title_home,iconSelect = R.drawable.ic_home_selected,iconUnselect = R.drawable.ic_home_unselected)object Recommend : ScreenPage(route = "recommend",resId = R.string.str_main_title_recommend,iconSelect = R.drawable.ic_recom_selected,iconUnselect = R.drawable.ic_recom_unselected)object Capture : ScreenPage(route = "add",iconSelect = R.drawable.ic_add_selected,iconUnselect = R.drawable.ic_add_unselected,isShowText = false)object Find : ScreenPage(route = "find",resId = R.string.str_main_title_find,iconSelect = R.drawable.ic_find_selected,iconUnselect = R.drawable.ic_find_unselected)object Mine : ScreenPage(route = "mine",resId = R.string.str_main_title_mine,iconSelect = R.drawable.ic_mine_selected,iconUnselect = R.drawable.ic_mine_unselected)
}

4.总结

本文主要介绍了一个特殊有趣的底部导航栏的实现方法,在大型项目的开发中,底部导航栏会被当成一个单独的模块维护,这就需要将底部导航栏抽取出来,本文只做一个抛砖引玉的作用,读者感兴趣可以试着抽取一下,我在项目中是抽取出来作为单独的模块的,发现的问题是抽取出来后 BottomNavigationItem的selectedContentColor 和unselectedContentColor 对于文字不生效了。最后我的解决方法是通过selected属性去动态修改对应的字体颜色和图片,在使用过程中读者有问题的话可以评论区一起交流

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/668853.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Ubuntu使用Docker部署Nginx并结合内网穿透实现公网远程访问

文章目录 1. 安装Docker2. 使用Docker拉取Nginx镜像3. 创建并启动Nginx容器4. 本地连接测试5. 公网远程访问本地Nginx5.1 内网穿透工具安装5.2 创建远程连接公网地址5.3 使用固定公网地址远程访问 在开发人员的工作中,公网远程访问内网是其必备的技术需求之一。对于…

基于YOLOv8的足球赛环境下足球目标检测系统(Python源码+Pyqt6界面+数据集)

博主简介 AI小怪兽,YOLO骨灰级玩家,1)YOLOv5、v7、v8优化创新,轻松涨点和模型轻量化;2)目标检测、语义分割、OCR、分类等技术孵化,赋能智能制造,工业项目落地经验丰富; …

五、医学影像云平台 - 医共体

原创不易,多谢关注!谢谢! 1. 医学大影像设备市场现状 目前影像设备,可以说低端产品同质化越来越严重,利润越来越薄,而高端超高端设备,整体销售额却在增长,利润空间也比低端的要高的…

【240121】桂林电子科技大学—调剂信息

桂林电子科技大学 学校层级:双非 调剂专业:081000 信息与通信工程 发布时间:2024.1.21 发布来源:网络发布 背景:欢迎广大08工学专业考生调剂进我的课题组,电子信息专业,也欢迎往届同学调剂…

SpringMVC-组件解析

一、引子 我们在上一篇文章Spring MVC-基本概念中,为读者解释了如何使用SpringMVC框架,将承接客户端请求的工作从原生的Servlet转移到我们熟知的Controller中。那么我们不禁会好奇,SpringMVC框架到底做了什么,是怎么把请求分发给…

sqlserver alwayson部署文档手册

1、ALWAYSON概述 详细介绍参照官网详细文档,我就不在这里赘述了: https://learn.microsoft.com/zh-cn/sql/database-engine/availability-groups/windows/overview-of-always-on-availability-groups-sql-server?viewsql-server-ver16 下图显示的是一个包含一个…

aspose-words基础功能演示

我们在Aspose.Words中使用术语“渲染”来描述将文档转换为文件格式或分页或具有页面概念的介质的过程。我们正在讨论将文档呈现为页面。下图显示了 Aspose.Words 中的渲染情况。 Aspose.Words 的渲染功能使您能够执行以下操作: 将文档或选定页面转换为 PDF、XPS、HTML、XAML、…

冀蒙辽三地共同推进北斗卫星导航定位基准站资源共享

冀蒙辽三地共同推进北斗卫星导航定位基准站资源共享 近期,冀蒙辽三地共同举办了“北斗卫星导航定位基准站资源共享推进会”,旨在推动北斗卫星导航定位系统的规模化应用,加强区域北斗卫星导航定位基准站网络的协同服务能力,为经济…

Java并发(二十三)----同步模式之保护性暂停

1、定义 即 Guarded Suspension,用在一个线程等待另一个线程的执行结果 要点 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject 如果有结果不断从一个线程到另一个线程那么可以使用消息队列 JDK 中,join 的实现…

微信小程序 简单优惠卷页面设计

index.wxml <view style"margin: 0.5rem;"><view class"points">我的积分&#xff1a;{{integralInfo}}</view></view><view><view wx:if"{{couponList.length>0}}" wx:for"{{couponList}}" wx:…

MySQL管理的常用工具(mysql,mysqlbinlog,mysqladmin,mysqlshow)

MySQL管理 系统数据库 数据库含义mysql存储MySQL服务器正常运行所需要的各种信息 &#xff08;时区、主从、用 户、权限等&#xff09;information_schema提供了访问数据库元数据的各种表和视图&#xff0c;包含数据库、表、字段类 型及访问权限等performance_schema为MySQL服…

SRS视频服务器使用记录

SRS是一个开源的&#xff08;MIT协议&#xff09;简单高效的实时视频服务器&#xff0c;支持RTMP、WebRTC、HLS、HTTP-FLV、SRT、MPEG-DASH和GB28181等协议。 SRS媒体服务器和FFmpeg、OBS、VLC、 WebRTC等客户端配合使用&#xff0c;提供流的接收和分发的能力&#xff0c;是一个…

【SpringBoot】SpringBoot的web开发

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;SpringBoot ⛺️稳重求进&#xff0c;晒太阳 Wbe开发 使用Springboot 1&#xff09;、创建SpringBoot应用&#xff0c;选中我们需要的模块&#xff1b; 2&#xff09;、SpringBoot已经默…

车载电子电器架构 —— IP地址获取策略

车载电子电器架构 —— IP地址获取策略 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明自…

【UE 材质】球形遮罩材质

效果 步骤 1. 新建一个材质&#xff0c;这里命名为“M_Mask” 打开“M_Mask”&#xff0c;混合模式设置为已遮罩&#xff0c;勾选双面显示 在材质图表中添加如下节点 此时我们将一个物体赋予材质“M_Mask”并放置在世界坐标原点&#xff0c;可以看到如下效果 2. 如果我们希望能…

【激光SLAM】里程计运动模型及标定

目录 里程计模型两轮差分底盘的运动学模型优点差分模型 三轮全向底盘的运动学模型优点全向模型 航迹推算(Dead Reckoning) 里程计标定线性最小二乘的基本原理最小二乘的直线拟合最小二乘在里程计标定中的应用方法 里程计模型 里程计相关介绍 两轮差分底盘的运动学模型 优点 …

无向图-树的重心-DFS求解

思路&#xff1a; 本题的本质是树的dfs&#xff0c; 每次dfs可以确定以u为重心的最大连通块的节点数&#xff0c;并且更新一下ans。 也就是说&#xff0c;dfs并不直接返回答案&#xff0c;而是在每次更新中迭代一次答案。 这样的套路会经常用到&#xff0c;在 树的dfs 题目中…

2024年MacBook上实用软件

Mac软件-mac软件下载-mac软件大全-MacZMacz下载是一个专业的Mac苹果电脑软件下载网站&#xff0c;提供专业的Mac软件、Mac游戏、精品插件以及各类海量素材下载&#xff0c;mac下载网站有Mac平台上常用好用的软件&#xff0c;有时下热门好玩的Mac游戏&#xff0c;还有各类PS、AE…

CentOS 7中搭建NFS文件共享服务器的完整步骤

CentOS 7中搭建NFS文件共享服务器的完整步骤 要求&#xff1a;实现镜像文件共享&#xff0c;并基于挂载的共享目录配置yum源。 系统环境&#xff1a; 服务器&#xff1a;172.20.26.167-CentOS7.6 客户端&#xff1a;172.20.26.198-CentOS7.6 1、在服务器和客户端上&#x…

从奥迪Quattro到碧然德:揭秘技术品牌成功打造与推广的秘诀

在当前全球化和信息化快速发展的背景下&#xff0c;技术品牌的打造不仅是企业竞争力提升的重要途径&#xff0c;也是企业实现长远发展的基石。通过深入剖析&#xff0c;我们认识到&#xff0c;技术品牌的建设并非一蹴而就的过程&#xff0c;而是需要企业准确把握市场趋势&#…