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.current

    Scaffold(
        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?.destination
                var isSelected: Boolean
                items.forEach { screenPage ->
                    isSelected =
                        currentDestination?.hierarchy?.any { it.route == screenPage.route } == true
                    CompositionLocalProvider(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 theme
                Surface(
                    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.current

    Scaffold(
        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?.destination
                var isSelected: Boolean
                items.forEach { screenPage ->
                    isSelected =
                        currentDestination?.hierarchy?.any { it.route == screenPage.route } == true
                    CompositionLocalProvider(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 {
    @Composable
    override fun defaultColor(): Color = Color.Unspecified

    @Composable
    override 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属性去动态修改对应的字体颜色和图片,在使用过程中读者有问题的话可以评论区一起交流

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

android开发---简单购物商城(JAVA) (一)

包括&#xff1a;商品展示&#xff0c;商品详情&#xff0c;购物车&#xff0c;删除&#xff0c;一键清除&#xff0c;返回 运用sqllist 另外因为一篇写不下 继续可看 源码二 下面是目录 运行样子 下面是源码 AndroidManifest.xml <?xml version"1.0" e…

路由重定向和别名

聚沙成塔每天进步一点点 本文内容 ⭐ 专栏简介1. 路由重定向实例场景&#xff1a;路由重定向的应用场景&#xff1a; 2. 路由别名实例场景&#xff1a;路由别名的应用场景&#xff1a; ⭐ 写在最后 ⭐ 专栏简介 Vue学习之旅的奇妙世界 欢迎大家来到 Vue 技能树参考资料专栏&…

(1)从 AGP 4.1.2 升级到 7.5.1 我遇到了什么问题

AGP 升级问题 &#xff08;1&#xff09;Could not get unknown property ‘project’ for settings&#xff0c;on project.buildscript 问题 Could not get unknown property ‘project’ for settings ‘AGP1’ of type org.gradle.initialization.DefaultSettings. agp4 …

数组与字符串深度巩固

经过再三思考觉得今天就写一篇关于数组与字符串相关的文章吧&#xff01;其中字符串主要通过练习来巩固知识亦或是获得新知识。好接下来将进行我们的学习时刻了。 首先我们来思考一个问题&#xff0c;你真的了解数组的数组名吗&#xff1f;数组名真的就单单一个名字而已吗&…

前置知识:方法递归

认识递归的形式 递归是一种算法&#xff0c;在程序设计语言中广泛应用。从形式上来说&#xff1a;方法调用自身的形式称为方法递归&#xff08;recursion&#xff09;。 递归的形式 直接递归&#xff1a;方法自己调用自己间接递归&#xff1a;方法调用其他方法&#xff0c;其…

深度解读NVMe计算存储协议-1

随着云计算、企业级应用以及物联网领域的飞速发展&#xff0c;当前的数据处理需求正以前所未有的规模增长&#xff0c;以满足存储行业不断变化的需求。这种增长导致网络带宽压力增大&#xff0c;并对主机计算资源&#xff08;如内存和CPU&#xff09;造成极大负担&#xff0c;进…

ASP.NET Core+Layui使用EF Core操作MySQL实战

一、创建ASP.NET Core Web应用程序 注意&#xff0c;本章节主要以ASP.NET Core 3.1版本作为博客的示例实例模板&#xff01;&#xff08;当然你也可以使用当前最新的.NET版本进行实操&#xff09;。 二、添加EF Core NuGet包 若要在项目中使用EF Core操作MySQL数据库&#xff…

【昕宝爸爸小模块】日志系列之什么是分布式日志系统

➡️博客首页 https://blog.csdn.net/Java_Yangxiaoyuan 欢迎优秀的你&#x1f44d;点赞、&#x1f5c2;️收藏、加❤️关注哦。 本文章CSDN首发&#xff0c;欢迎转载&#xff0c;要注明出处哦&#xff01; 先感谢优秀的你能认真的看完本文&…

坚持刷题 | 完全二叉树的节点个数

Hello&#xff0c;大家好&#xff0c;我是阿月&#xff01;坚持刷题&#xff0c;老年痴呆追不上我&#xff0c;今天刷&#xff1a;完全二叉树的节点个数 题目 222.完全二叉树的节点个数 代码实现 class TreeNode {int val;TreeNode left, right;public TreeNode(int val) …

编程实例分享,配件进销存进出库管理系统软件

编程实例分享&#xff0c;配件进销存进出库管理系统软件 一、前言 以下教程以 佳易王配件进出库管理系统软件V16.0为例说明 如上图&#xff0c;左侧为导航栏&#xff0c;分为 系统设置&#xff0c;用户信息设置&#xff0c;出入库开单&#xff0c;统计报表&#xff0c;财务管…

C++初阶 类和对象(补充)

目录 一、友元 1.1什么是友元&#xff1f; 1.2如何使用友元&#xff1f; 1.3使用友元 1.4使用友元注意事项 二、初始化列表 2.1什么是初始化列表? 2.2为什么要有初始化列表&#xff1f; 2.3使用初始化列表 2.4注意事项 一、友元 1.1什么是友元&#xff1f; 友元是一…

基于MobileNet(v1-v3)全系列不同参数量级模型开发构建果树图像病虫害识别分析系统,实验量化对比不同模型性能

最近正好项目中在做一些识别相关的内容&#xff0c;我也陆陆续续写了一些实验性质的博文用于对自己使用过的模型进行真实数据的评测对比分析&#xff0c;感兴趣的话可以自行移步阅读即可&#xff1a; 《移动端轻量级模型开发谁更胜一筹&#xff0c;efficientnet、mobilenetv2、…

高等数学:积分

本文主要参考&#xff1a; 【建议收藏】同济七版《高等数学》精讲视频 | 期末考试 | 考研零基础 | 高数小白_哔哩哔哩_bilibili 4.1.1.1 定积分引例_哔哩哔哩_bilibili 仅供本人学习使用。 积分是一个在多个领域都广泛使用的概念&#xff0c;特别是在数学和物理学中。 以下是积…

PyTorch识别验证码

## 一、生成测试集数据pip install captcha common.py import random import time captcha_array list("0123456789abcdefghijklmnopqrstuvwxyz") captcha_size 4from captcha.image import ImageCaptchaif __name__ __main__:for i in range(10):image ImageC…

leetcode209长度最小的子数组|滑动窗口算法详细讲解学习

滑动窗口是一种基于双指针的一种思想&#xff0c;两个指针指向的元素之间形成一个窗口。 分类&#xff1a;窗口有两类&#xff0c;一种是固定大小类的窗口&#xff0c;一类是大小动态变化的窗口。 简而言之&#xff0c;滑动窗口算法在一个特定大小的字符串或数组上进行操作&…

如何使用Cloudreve搭建私有云盘并发布公网访问无需购买域名服务器

文章目录 1、前言2、本地网站搭建2.1 环境使用2.2 支持组件选择2.3 网页安装2.4 测试和使用2.5 问题解决 3、本地网页发布3.1 cpolar云端设置3.2 cpolar本地设置 4、公网访问测试5、结语 1、前言 自云存储概念兴起已经有段时间了&#xff0c;各互联网大厂也纷纷加入战局&#…

飞轮储能系统的建模与MATLAB仿真(永磁同步电机作为飞轮驱动电机)

目录 1 主要内容 电网侧控制系统 电机侧模型 模型二 2 结果分析 3 下载链接 1 主要内容 该仿真为飞轮储能系统的建模&#xff0c;包括电网侧和电机侧两部分模型&#xff0c;仿真采用永磁同步电机作为飞轮驱动电机&#xff0c;通过矢量控制的方式对其发电和电动的工况进行控…

基于单片机温度控制系统的研究

摘 要&#xff1a;笔者基于单片机的温度控制系统&#xff0c;从单片机选择、传感器选择、系统框架设计等方面概述了单片机的温度控制系统内涵&#xff0c;分析了其运行原理&#xff0c;列举了单片机温度控制系统设计的实操方法&#xff0c;从硬件系统、软件系统、温度检测方法…

Python武器库开发-武器库篇之zip文件暴力破解(五十一)

Python武器库开发-武器库篇之zip文件暴力破解(五十一) Zip文件是一种常用的存档文件格式&#xff0c;用于将多个文件和文件夹压缩成一个单独的文件。它是一种广泛接受和支持的文件格式&#xff0c;几乎所有操作系统和计算机都能够处理Zip文件。Zip文件使用一种压缩算法来减小文…

sqli-labs靶场第一关详解

目录 sqlilabs靶场第一关 0. sql注入解释 0.1什么是sql注入 0.2sql注入的原理 0.3sql注入方法 0.3.1 数字型注入 0.3.2 字符型注入 1.注入第一步判断请求方式、类型 1.1打开我自己本地的靶场http://sql.com/Less-1/ &#xff08;上一期靶场搭建&#xff1a;http://t.…