You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Jetpack Compose底部导航栏选中图标上方添加圆点并自定义颜色

解决方案

一、添加选中项顶部外部圆点(无图片实现)

要实现导航栏外部的顶部圆点,可通过Box布局嵌套实现:

  1. 给整个BottomBar外层套一个Box,让导航栏和圆点处于同一层级;
  2. 单独封装导航项组件,选中时显示顶部圆点,通过offset将圆点定位到导航栏外部。

二、图标自定义颜色优化

通过统一的状态管理切换图标tint,同时支持传入自定义颜色参数,实现更灵活的颜色控制。

重构后的完整代码

@Composable
fun BottomBar(modifier: Modifier = Modifier) {
    var navNum by remember { mutableStateOf(0) }
    // 统一管理导航项数据
    val navItems = listOf(
        NavItem(R.drawable.home_filled, R.drawable.home_light, "home", 0),
        NavItem(R.drawable.calendar_filled, R.drawable.calendar_light, "calendar", 1),
        NavItem(R.drawable.message_filled, R.drawable.message_light, "message", 2),
        NavItem(R.drawable.user_filled, R.drawable.user_light, "user", 3)
    )

    Box(modifier = modifier.fillMaxWidth()) {
        // 底部导航栏主体
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .clip(RoundedCornerShape(topStart = 30.dp, topEnd = 30.dp))
                .background(CardColor)
                .padding(vertical = 15.dp, horizontal = 15.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Row(verticalAlignment = Alignment.CenterVertically) {
                navItems.take(2).forEach { item ->
                    BottomNavItem(
                        isSelected = navNum == item.index,
                        selectedIcon = item.selectedIcon,
                        unselectedIcon = item.unselectedIcon,
                        contentDescription = item.desc,
                        onClick = { navNum = item.index }
                    )
                    if (item.index != 1) Spacer(modifier = Modifier.width(8.dp))
                }
            }

            Row(verticalAlignment = Alignment.CenterVertically) {
                navItems.takeLast(2).forEach { item ->
                    BottomNavItem(
                        isSelected = navNum == item.index,
                        selectedIcon = item.selectedIcon,
                        unselectedIcon = item.unselectedIcon,
                        contentDescription = item.desc,
                        onClick = { navNum = item.index }
                    )
                    if (item.index != 3) Spacer(modifier = Modifier.width(8.dp))
                }
            }
        }

        // 顶部选中圆点
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 15.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.Top
        ) {
            Row(verticalAlignment = Alignment.CenterVertically) {
                navItems.take(2).forEachIndexed { index, item ->
                    if (navNum == item.index) {
                        Box(
                            modifier = Modifier
                                .size(8.dp)
                                .background(SecondaryColor, CircleShape)
                                .offset(y = (-20).dp) // 上移至导航栏外部
                        )
                    }
                    if (index != 1) Spacer(modifier = Modifier.width(33.dp)) // 匹配图标间距
                }
            }

            Row(verticalAlignment = Alignment.CenterVertically) {
                navItems.takeLast(2).forEachIndexed { index, item ->
                    if (navNum == item.index) {
                        Box(
                            modifier = Modifier
                                .size(8.dp)
                                .background(SecondaryColor, CircleShape)
                                .offset(y = (-20).dp)
                        )
                    }
                    if (index != 1) Spacer(modifier = Modifier.width(33.dp))
                }
            }
        }
    }
}

// 导航项数据类,统一管理图标、索引等信息
data class NavItem(
    val selectedIcon: Int,
    val unselectedIcon: Int,
    val desc: String,
    val index: Int
)

@Composable
fun BottomNavItem(
    isSelected: Boolean,
    selectedIcon: Int,
    unselectedIcon: Int,
    contentDescription: String,
    onClick: () -> Unit,
    selectedColor: Color = SecondaryColor,
    unselectedColor: Color = ThinTextColor
) {
    IconButton(onClick = onClick) {
        Icon(
            painter = painterResource(
                id = if (isSelected) selectedIcon else unselectedIcon
            ),
            contentDescription = contentDescription,
            tint = if (isSelected) selectedColor else unselectedColor,
            modifier = Modifier.size(25.dp)
        )
    }
}

关键说明

  1. 顶部圆点实现:
    • 用独立Row布局放置圆点,通过offset(y = (-20).dp)将圆点上移到导航栏背景外;
    • 借助Box+CircleShape绘制圆形,无需图片资源,颜色直接复用现有配色。
  2. 图标颜色自定义:
    • BottomNavItem组件新增selectedColor和unselectedColor参数,可直接传入自定义颜色;
    • 利用tint属性动态切换颜色,若使用矢量图标,甚至可只用一套图标资源通过tint切换状态。
  3. 代码优化:
    • 提取NavItem数据类统一管理导航信息,消除重复的if-else逻辑;
    • 封装BottomNavItem组件,简化主布局代码,提升可维护性。

内容的提问来源于stack exchange,提问作者Baba Yaga

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 15:57:32