这一节主要了解一下Compose中的BadgedBox,在Jetpack Compose中,BadgedBox是一个用于在任意可组合项右上角叠加徽章(Badge)的布局容器。它属于Material Design组件库的一部分,常用于在图标、头像、按钮等 UI 元素上显示通知数量、状态标记或提示信息。
API
Kotlin
@Composable
fun BadgedBox(
badge: @Composable () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
)
badge:徽章的具体内容(可自定义布局、样式)
content:被徽章标记的主组件
场景:
1 消息未读数
2 购物车商品数量
3 新功能提示
4 状态标识
栗子:
Kotlin
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@Composable
fun TabBadgeDemo() {
var selectedTab by remember { mutableIntStateOf(0) }
val tabItems = listOf(
"首页" to Icons.Outlined.Home,
"通知" to Icons.Outlined.Notifications,
"我的" to Icons.Outlined.Person
)
Scaffold(
bottomBar = {
NavigationBar {
tabItems.forEachIndexed { index, (title, icon) ->
NavigationBarItem(
icon = {
if (index == 1) {
BadgedBox(
badge = { Badge() },
content = { Icon(icon, contentDescription = title) }
)
} else {
Icon(icon, contentDescription = title)
}
},
label = { Text(title) },
selected = selectedTab == index,
onClick = { selectedTab = index }
)
}
}
}
) { padding ->
Box(modifier = Modifier.padding(padding)) {
Text(
text = "当前页面:${tabItems[selectedTab].first}",
style = MaterialTheme.typography.bodyLarge
)
}
}
}
Kotlin
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
data class ChatSession(
val id: Int,
val name: String,
val lastMessage: String,
val unreadCount: Int,
val isMuted: Boolean,
val isPinned: Boolean,
val timestamp: String
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BadgedBoxDemo() {
val chatSessions = remember {
mutableStateListOf(
ChatSession(1, "项目组", "大家记得参加明天的评审会", 3, false, true, "10:30"),
ChatSession(2, "张三", "方案已更新,请看附件", 1, false, false, "09:15"),
ChatSession(3, "家人群", "周末聚餐地点定好了吗?", 5, true, false, "昨天"),
ChatSession(4, "客服中心", "您的订单已发货", 0, false, false, "周三"),
ChatSession(5, "老板", "周一提交周报", 2, false, true, "上周")
)
}
val totalUnread by remember(chatSessions) {
derivedStateOf { chatSessions.sumOf { it.unreadCount } }
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("消息") },
actions = {
BadgedBox(
badge = {
Badge(
containerColor = MaterialTheme.colorScheme.error,
contentColor = Color.White
) {
Text(
text = if (totalUnread > 99) "99+" else totalUnread.toString(),
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)
}
},
content = {
BadgedBox(
badge = {
if (chatSessions.any { it.isMuted && it.unreadCount > 0 }) {
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(Color.Gray)
.offset(x = 8.dp, y = -8.dp)
)
}
},
content = {
IconButton(onClick = { /* 全局设置 */ }) {
Icon(Icons.Outlined.Settings, contentDescription = "设置")
}
}
)
}
)
}
)
},
floatingActionButton = {
BadgedBox(
badge = {
if (chatSessions.any { it.isPinned && it.unreadCount > 0 }) {
Badge(
containerColor = MaterialTheme.colorScheme.primary,
) {
Icon(
Icons.Outlined.Star,
contentDescription = "重要消息",
modifier = Modifier.size(12.dp)
)
}
}
},
content = {
FloatingActionButton(onClick = { /* 新建消息 */ }) {
Icon(Icons.Outlined.Add, contentDescription = "新建")
}
}
)
}
) { padding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(horizontal = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(chatSessions) { session ->
Card(
modifier = Modifier
.fillMaxWidth()
.clickable {
chatSessions[chatSessions.indexOf(session)] =
session.copy(unreadCount = 0)
},
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(2.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center
) {
Text(
text = session.name.first().toString(),
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.width(12.dp))
Column(
modifier = Modifier.weight(1f)
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = session.name,
fontWeight = if (session.unreadCount > 0) FontWeight.Bold else FontWeight.Normal
)
Text(
text = session.timestamp,
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Text(
text = session.lastMessage,
fontSize = 14.sp,
color = if (session.unreadCount > 0)
MaterialTheme.colorScheme.onSurface
else
MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
}
Spacer(modifier = Modifier.width(8.dp))
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.Top
) {
if (session.unreadCount > 0) {
Badge(
containerColor = if (session.isPinned)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.error,
modifier = Modifier.offset(y = (-4).dp)
) {
Text(
text = if (session.unreadCount > 9) "9+" else session.unreadCount.toString(),
fontSize = 12.sp
)
}
}
when {
session.isPinned -> {
Icon(
Icons.Outlined.PushPin,
contentDescription = "置顶",
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.primary
)
}
session.isMuted -> {
Icon(
Icons.Outlined.VolumeOff,
contentDescription = "静音",
modifier = Modifier.size(16.dp),
tint = Color.Gray
)
}
}
}
}
}
}
}
}
}
注意:
1 不要滥用:徽章应只用于重要状态提示,避免界面杂乱
2 尺寸控制:徽章不宜过大
3 颜色对比:确保徽章文字与背景有足够对比度