CMP for OpenHarmony 标签组件开发实战
本文介绍了使用Compose Multiplatform在OpenHarmony上开发标签组件的实现方法。主要内容包括:1) 定义标签数据模型TagData和预定义颜色;2) 实现基础标签组件BasicTag,支持填充、边框和柔和三种样式;3) 扩展图标标签IconTag,可灵活配置图标位置。组件采用模块化设计,通过参数控制尺寸、颜色和交互行为,提供圆角、点击反馈等视觉效果,适用于分类、筛选等多种

项目开源地址:https://atomgit.com/nutpi/cmp_openharmony
前言
标签(Tag)是移动应用中非常常见的UI元素,用于分类、筛选、状态展示等场景。本文将详细介绍如何使用 Compose Multiplatform(CMP)在 OpenHarmony 平台上开发多种实用的标签组件,包括基础标签、可选择标签、可关闭标签、状态标签和数字徽章等。
一、数据模型定义
首先,我们需要定义标签相关的数据模型和配置项。
1.1 标签数据类
data class TagData(
val id: String,
val text: String,
val color: Color = Color(0xFF6200EE),
val isSelected: Boolean = false,
val isCloseable: Boolean = false
)
这个数据类定义了标签的基本属性。id 用于唯一标识每个标签,在列表操作时非常重要。text 是标签显示的文本内容。color 定义标签的主题色,默认使用紫色。isSelected 和 isCloseable 分别控制标签的选中状态和是否可关闭。
1.2 预定义颜色
object TagColors {
val Purple = Color(0xFF6200EE)
val Blue = Color(0xFF2196F3)
val Green = Color(0xFF4CAF50)
val Orange = Color(0xFFFF9800)
val Red = Color(0xFFF44336)
val Pink = Color(0xFFE91E63)
val Teal = Color(0xFF009688)
val Indigo = Color(0xFF3F51B5)
val Gray = Color(0xFF9E9E9E)
}
预定义颜色对象提供了一组精心挑选的颜色常量,这些颜色在视觉上协调统一,可以直接在标签组件中使用。使用 object 单例模式确保颜色值在整个应用中保持一致,同时也方便后续维护和修改。
1.3 标签尺寸枚举
enum class TagSize {
SMALL, // 小尺寸
MEDIUM, // 中等尺寸
LARGE // 大尺寸
}
通过枚举定义三种标准尺寸,让标签组件能够适应不同的使用场景。小尺寸适合紧凑的列表项,中等尺寸是默认选择,大尺寸则用于需要突出显示的场合。
1.4 标签样式枚举
enum class TagStyle {
FILLED, // 填充样式
OUTLINED, // 边框样式
SOFT // 柔和样式(浅色背景)
}
三种样式枚举覆盖了常见的标签视觉需求。FILLED 填充样式最为醒目,适合重要标签;OUTLINED 边框样式较为轻量,不会喧宾夺主;SOFT 柔和样式使用浅色背景,视觉上最为柔和,适合大量标签并存的场景。
二、基础标签组件
2.1 BasicTag 组件
@Composable
fun BasicTag(
text: String,
modifier: Modifier = Modifier,
color: Color = TagColors.Purple,
style: TagStyle = TagStyle.FILLED,
size: TagSize = TagSize.MEDIUM,
onClick: (() -> Unit)? = null
) {
val (paddingH, paddingV, fontSize) = when (size) {
TagSize.SMALL -> Triple(8.dp, 2.dp, 10.sp)
TagSize.MEDIUM -> Triple(12.dp, 4.dp, 12.sp)
TagSize.LARGE -> Triple(16.dp, 6.dp, 14.sp)
}
组件函数签名设计遵循 Compose 最佳实践,将必需参数 text 放在最前面,可选参数使用默认值。使用 when 表达式根据尺寸计算对应的内边距和字体大小,Triple 数据类让我们能够优雅地一次性解构三个值。
val (bgColor, textColor, borderColor) = when (style) {
TagStyle.FILLED -> Triple(color, Color.White, Color.Transparent)
TagStyle.OUTLINED -> Triple(Color.Transparent, color, color)
TagStyle.SOFT -> Triple(color.copy(alpha = 0.15f), color, Color.Transparent)
}
根据样式类型计算背景色、文字色和边框色。填充样式使用主题色作为背景、白色文字;边框样式背景透明、使用主题色作为文字和边框;柔和样式使用 15% 透明度的主题色作为背景,这个透明度值经过测试,在视觉上既能体现颜色又不会过于强烈。
Box(
modifier = modifier
.clip(RoundedCornerShape(16.dp))
.background(bgColor)
.then(
if (borderColor != Color.Transparent) {
Modifier.border(1.dp, borderColor, RoundedCornerShape(16.dp))
} else Modifier
)
.then(
if (onClick != null) {
Modifier.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { onClick() }
} else Modifier
)
.padding(horizontal = paddingH, vertical = paddingV),
contentAlignment = Alignment.Center
) {
Text(
text = text,
fontSize = fontSize,
color = textColor,
fontWeight = FontWeight.Medium
)
}
}
使用 Box 作为容器,通过 clip 裁剪实现圆角效果。then 函数用于条件性地添加修饰符,只有当边框色不透明时才添加边框,只有当 onClick 不为空时才添加点击事件。设置 indication = null 可以移除默认的点击波纹效果,让标签看起来更加简洁。
2.2 带图标的标签
@Composable
fun IconTag(
text: String,
icon: String,
modifier: Modifier = Modifier,
color: Color = TagColors.Purple,
style: TagStyle = TagStyle.FILLED,
size: TagSize = TagSize.MEDIUM,
iconPosition: IconPosition = IconPosition.START,
onClick: (() -> Unit)? = null
) {
IconTag 在基础标签的基础上增加了图标支持。icon 参数使用字符串类型,可以传入 emoji 表情或文字图标。iconPosition 参数控制图标显示在文字的前面还是后面,提供了灵活的布局选项。
Row(
modifier = modifier
.clip(RoundedCornerShape(16.dp))
.background(bgColor)
// ... 其他修饰符
.padding(horizontal = paddingH, vertical = paddingV),
verticalAlignment = Alignment.CenterVertically
) {
if (iconPosition == IconPosition.START) {
Text(text = icon, fontSize = fontSize, color = textColor)
Spacer(modifier = Modifier.width(4.dp))
}
Text(
text = text,
fontSize = fontSize,
color = textColor,
fontWeight = FontWeight.Medium
)
if (iconPosition == IconPosition.END) {
Spacer(modifier = Modifier.width(4.dp))
Text(text = icon, fontSize = fontSize, color = textColor)
}
}
}
enum class IconPosition { START, END }
使用 Row 布局来水平排列图标和文字。通过条件判断 iconPosition 的值来决定图标的位置,Spacer 组件在图标和文字之间添加 4dp 的间距,保证视觉上的舒适感。枚举 IconPosition 定义了两个位置选项,语义清晰。
三、可选择标签组件
3.1 SelectableTag 组件
@Composable
fun SelectableTag(
text: String,
isSelected: Boolean,
onSelect: (Boolean) -> Unit,
modifier: Modifier = Modifier,
selectedColor: Color = TagColors.Purple,
unselectedColor: Color = Color.Gray,
size: TagSize = TagSize.MEDIUM
) {
可选择标签是交互性最强的标签类型,常用于筛选器、标签选择等场景。isSelected 参数由外部控制,遵循单向数据流原则;onSelect 回调将新的选中状态传递给父组件。
val bgColor by animateColorAsState(
if (isSelected) selectedColor else Color.Transparent
)
val textColor by animateColorAsState(
if (isSelected) Color.White else unselectedColor
)
val borderColor by animateColorAsState(
if (isSelected) selectedColor else unselectedColor.copy(alpha = 0.5f)
)
val scale by animateFloatAsState(
targetValue = if (isSelected) 1.05f else 1f,
animationSpec = spring()
)
使用 animateColorAsState 和 animateFloatAsState 为颜色和缩放添加平滑的过渡动画。当选中状态改变时,背景色、文字色、边框色都会有渐变效果,同时标签会有轻微的放大动画(1.05倍),使用 spring() 弹簧动画让效果更加自然生动。
Box(
modifier = modifier
.scale(scale)
.clip(RoundedCornerShape(16.dp))
.background(bgColor)
.border(1.dp, borderColor, RoundedCornerShape(16.dp))
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { onSelect(!isSelected) }
.padding(horizontal = paddingH, vertical = paddingV),
contentAlignment = Alignment.Center
) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (isSelected) {
Text(text = "✓", fontSize = fontSize, color = textColor)
Spacer(modifier = Modifier.width(4.dp))
}
Text(
text = text,
fontSize = fontSize,
color = textColor,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
)
}
}
}
scale 修饰符应用缩放动画效果。点击时调用 onSelect(!isSelected) 切换选中状态。选中时会在文字前显示一个勾选符号 “✓”,并将字体加粗,这些视觉反馈让用户能够清楚地识别当前选中的标签。
3.2 可关闭标签
@Composable
fun CloseableTag(
text: String,
onClose: () -> Unit,
modifier: Modifier = Modifier,
color: Color = TagColors.Purple,
style: TagStyle = TagStyle.SOFT,
size: TagSize = TagSize.MEDIUM
) {
可关闭标签常用于已选择的筛选条件、搜索历史等场景,用户可以点击关闭按钮移除标签。默认使用 SOFT 柔和样式,因为这类标签通常会有多个同时显示。
Row(
modifier = modifier
.clip(RoundedCornerShape(16.dp))
.background(bgColor)
.then(
if (style == TagStyle.OUTLINED) {
Modifier.border(1.dp, color, RoundedCornerShape(16.dp))
} else Modifier
)
.padding(start = paddingH, end = 6.dp, top = paddingV, bottom = paddingV),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = text,
fontSize = fontSize,
color = textColor,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.width(4.dp))
Box(
modifier = Modifier
.size(16.dp)
.clip(RoundedCornerShape(8.dp))
.background(textColor.copy(alpha = 0.2f))
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { onClose() },
contentAlignment = Alignment.Center
) {
Text(
text = "✕",
fontSize = 10.sp,
color = textColor
)
}
}
}
注意右侧内边距设置为 6dp 而不是 paddingH,这是为了让关闭按钮更靠近边缘,视觉上更加平衡。关闭按钮使用一个 16dp 的圆形 Box,背景色为文字色的 20% 透明度,既能看清又不会太突兀。点击关闭按钮时调用 onClose() 回调,由父组件处理标签的移除逻辑。
四、状态标签组件
4.1 StatusTag 组件
@Composable
fun StatusTag(
text: String,
status: TagStatus,
modifier: Modifier = Modifier,
size: TagSize = TagSize.MEDIUM,
showDot: Boolean = true
) {
val color = status.color
状态标签用于显示各种状态信息,如成功、错误、警告等。status 参数使用枚举类型,每种状态都有对应的颜色。showDot 参数控制是否显示状态指示圆点。
Row(
modifier = modifier
.clip(RoundedCornerShape(12.dp))
.background(color.copy(alpha = 0.15f))
.padding(horizontal = paddingH, vertical = paddingV),
verticalAlignment = Alignment.CenterVertically
) {
if (showDot) {
if (status == TagStatus.PROCESSING) {
PulsingDot(color = color)
} else {
Box(
modifier = Modifier
.size(6.dp)
.background(color, CircleShape)
)
}
Spacer(modifier = Modifier.width(6.dp))
}
Text(
text = text,
fontSize = fontSize,
color = color,
fontWeight = FontWeight.Medium
)
}
}
状态标签使用 12dp 的圆角,比普通标签稍小,视觉上更加精致。背景使用 15% 透明度的状态色。当状态为 PROCESSING(处理中)时,显示一个带脉冲动画的圆点,其他状态显示静态圆点。这个细节让"处理中"状态更加生动,用户能够直观感受到正在进行的操作。
4.2 脉冲动画圆点
@Composable
private fun PulsingDot(color: Color) {
val infiniteTransition = rememberInfiniteTransition()
val scale by infiniteTransition.animateFloat(
initialValue = 0.8f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(800),
repeatMode = RepeatMode.Reverse
)
)
val alpha by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 0.5f,
animationSpec = infiniteRepeatable(
animation = tween(800),
repeatMode = RepeatMode.Reverse
)
)
Box(
modifier = Modifier
.size(6.dp)
.scale(scale)
.background(color.copy(alpha = alpha), CircleShape)
)
}
脉冲动画使用 rememberInfiniteTransition 创建无限循环的动画。同时对缩放(0.8 到 1.2)和透明度(1 到 0.5)进行动画处理,800ms 的周期配合 RepeatMode.Reverse 反向重复,产生呼吸般的脉冲效果。这种动画既能吸引注意力,又不会过于干扰用户。
4.3 状态枚举
enum class TagStatus(val color: Color) {
SUCCESS(Color(0xFF4CAF50)),
ERROR(Color(0xFFF44336)),
WARNING(Color(0xFFFF9800)),
INFO(Color(0xFF2196F3)),
PROCESSING(Color(0xFF6200EE)),
DEFAULT(Color(0xFF9E9E9E))
}
状态枚举将状态类型和对应颜色绑定在一起,这是 Kotlin 枚举的强大特性。绿色表示成功、红色表示错误、橙色表示警告、蓝色表示信息、紫色表示处理中、灰色表示默认状态,这些颜色选择符合用户的直觉认知。
五、数字徽章组件
5.1 NumberBadge 组件
@Composable
fun NumberBadge(
count: Int,
modifier: Modifier = Modifier,
color: Color = Color.Red,
maxCount: Int = 99
) {
if (count <= 0) return
val text = if (count > maxCount) "$maxCount+" else count.toString()
Box(
modifier = modifier
.background(color, RoundedCornerShape(10.dp))
.padding(horizontal = 6.dp, vertical = 2.dp),
contentAlignment = Alignment.Center
) {
Text(
text = text,
fontSize = 10.sp,
color = Color.White,
fontWeight = FontWeight.Bold
)
}
}
数字徽章用于显示未读消息数、通知数量等。当 count 小于等于 0 时直接返回不显示任何内容。maxCount 参数设置最大显示数字,超过时显示 “99+”,这是移动应用的常见做法,避免数字过长影响布局。使用 10dp 的圆角和紧凑的内边距,让徽章看起来小巧精致。
5.2 圆点徽章
@Composable
fun DotBadge(
modifier: Modifier = Modifier,
color: Color = Color.Red,
size: Dp = 8.dp
) {
Box(
modifier = modifier
.size(size)
.background(color, CircleShape)
)
}
圆点徽章是最简单的徽章形式,只显示一个小圆点,用于表示"有新内容"但不显示具体数量。默认 8dp 的尺寸在大多数场景下都很合适,size 参数允许根据需要调整大小。使用 CircleShape 确保无论什么尺寸都是完美的圆形。
六、使用示例
6.1 基础标签使用
// 不同样式
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
BasicTag(text = "填充样式", style = TagStyle.FILLED)
BasicTag(text = "边框样式", style = TagStyle.OUTLINED)
BasicTag(text = "柔和样式", style = TagStyle.SOFT)
}
// 不同颜色
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
BasicTag(text = "紫色", color = TagColors.Purple)
BasicTag(text = "蓝色", color = TagColors.Blue)
BasicTag(text = "绿色", color = TagColors.Green)
}
基础标签的使用非常简单,只需传入文本即可使用默认样式。通过 style 和 color 参数可以轻松定制外观。Arrangement.spacedBy(8.dp) 让标签之间保持统一的间距。
6.2 带图标标签使用
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
IconTag(text = "热门", icon = "🔥", color = TagColors.Red)
IconTag(text = "新品", icon = "✨", color = TagColors.Orange)
IconTag(
text = "收藏",
icon = "❤️",
iconPosition = IconPosition.END,
style = TagStyle.SOFT
)
}
带图标的标签可以使用 emoji 作为图标,简单直观。通过 iconPosition 参数可以将图标放在文字后面,适合"收藏"、"分享"等操作类标签。
6.3 可选择标签使用
var selectedTag by remember { mutableStateOf<String?>(null) }
val tags = listOf("Kotlin", "Swift", "Java", "TypeScript")
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
tags.forEach { tag ->
SelectableTag(
text = tag,
isSelected = tag == selectedTag,
onSelect = { if (it) selectedTag = tag }
)
}
}
单选模式下,使用一个可空的 String 状态来记录当前选中的标签。在 onSelect 回调中,只有当 it 为 true(即选中操作)时才更新状态,这样点击已选中的标签不会取消选择。
6.4 多选标签使用
var selectedTags by remember { mutableStateOf(setOf<String>()) }
val tags = listOf("Android", "iOS", "HarmonyOS", "Web")
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
tags.forEach { tag ->
SelectableTag(
text = tag,
isSelected = tag in selectedTags,
onSelect = { selected ->
selectedTags = if (selected) {
if (selectedTags.size < 3) selectedTags + tag else selectedTags
} else {
selectedTags - tag
}
},
selectedColor = TagColors.Blue
)
}
}
多选模式使用 Set<String> 来存储选中的标签,Set 的特性保证不会有重复项。这里还实现了最多选择 3 个的限制,当已选择 3 个时,再点击其他标签不会生效。使用 + 和 - 操作符可以方便地添加或移除集合元素。
6.5 状态标签使用
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
StatusTag(text = "成功", status = TagStatus.SUCCESS)
StatusTag(text = "错误", status = TagStatus.ERROR)
StatusTag(text = "处理中", status = TagStatus.PROCESSING)
}
状态标签的使用最为简单,只需指定文本和状态类型,颜色会自动匹配。PROCESSING 状态会自动显示脉冲动画效果。
七、最佳实践
7.1 颜色一致性
建议在整个应用中使用 TagColors 中预定义的颜色,保持视觉一致性。如果需要自定义颜色,也建议统一定义在一个地方。
7.2 尺寸选择
SMALL:适合列表项、紧凑布局MEDIUM:默认选择,适合大多数场景LARGE:适合需要突出显示的场合
7.3 样式搭配
- 重要标签使用
FILLED样式 - 次要标签使用
OUTLINED或SOFT样式 - 大量标签并存时优先使用
SOFT样式
7.4 动画性能
可选择标签的动画效果虽然美观,但在大量标签同时存在时可能影响性能。如果遇到性能问题,可以考虑简化动画或使用 LazyRow 进行懒加载。
总结
本文详细介绍了使用 CMP 在 OpenHarmony 平台上开发标签组件的完整过程。通过合理的数据模型设计、灵活的参数配置和流畅的动画效果,我们实现了一套功能完善、易于使用的标签组件库。这些组件可以直接应用到实际项目中,也可以根据具体需求进行扩展和定制。
CMP 的声明式 UI 编程模式让组件开发变得简洁高效,同时保持了良好的可维护性和可扩展性。希望本文能够帮助你在 OpenHarmony 应用开发中更好地使用标签组件。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)