ArkUI组件库完全指南:从基础组件到自定义装饰器
在鸿蒙NEXT(HarmonyOS NEXT)的应用开发中,ArkUI 是构建用户界面的核心框架。它采用声明式UI范式,让开发者可以用简洁直观的代码描述界面结构和交互逻辑。与传统的命令式UI相比,ArkUI的声明式写法不仅大幅减少了代码量,还让UI状态管理变得更加清晰可控。对于从Android或iOS转过来的开发者,ArkUI的学习曲线并不陡峭。它的设计理念融合了Flutter的声明式语法和Rea
系列文章:鸿蒙NEXT开发实战系列 -- 第2篇(共5篇) 适合人群:有基础ArkTS语法知识,想系统掌握ArkUI组件库的开发者 开发环境:DevEco Studio 5.0.5+ | HarmonyOS NEXT (API 14) 阅读时长:约40分钟
上一篇:鸿蒙NEXT开发从零到一 | 下一篇:状态管理一文通
目录
一、引言:为什么ArkUI是鸿蒙开发的核心?
在鸿蒙NEXT(HarmonyOS NEXT)的应用开发中,ArkUI 是构建用户界面的核心框架。它采用声明式UI范式,让开发者可以用简洁直观的代码描述界面结构和交互逻辑。与传统的命令式UI相比,ArkUI的声明式写法不仅大幅减少了代码量,还让UI状态管理变得更加清晰可控。
对于从Android或iOS转过来的开发者,ArkUI的学习曲线并不陡峭。它的设计理念融合了Flutter的声明式语法和React的状态驱动思想,同时针对鸿蒙的分布式能力做了深度优化。掌握ArkUI组件库,就等于拿到了鸿蒙应用开发的"入场券"。
本文将从基础组件讲起,逐步深入到布局系统、自定义组件、样式主题和动画效果,配合完整可运行的实战代码和踩坑记录,帮助你系统掌握ArkUI的核心能力。无论你是鸿蒙新手还是有经验的开发者,这篇文章都值得收藏备用。
二、常用基础组件详解
2.1 Text 文本组件
Text 是最基础的组件,用于展示文字内容。它支持富文本、文本样式定制和文本溢出处理。
// TextComponent.ets
@Entry
@Component
struct TextExample {
build() {
Column({ space: 16 }) {
// 基础文本
Text('Hello HarmonyOS NEXT!')
// 带样式的文本
Text('这是一段带样式的文本')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6B35')
.fontFamily('HarmonyOS Sans')
// 富文本(通过Span拼接不同样式)
Text() {
Span('鸿蒙NEXT ')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#007DFF')
Span('是华为推出的')
.fontSize(16)
.fontColor('#333333')
Span('全场景分布式操作系统')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6B35')
}
.textArea({ overflow: TextOverflow.Ellipsis, maxLines: 2 })
// 文本溢出处理
Text('这是一段很长很长的文本,用于演示文本溢出处理效果,当文本超出指定行数时会显示省略号')
.fontSize(14)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('90%')
}
.width('100%')
.padding(16)
}
}
关键属性说明:
|
属性 |
说明 |
示例值 |
|---|---|---|
|
|
字体大小 |
|
|
|
字体粗细 |
|
|
|
字体颜色 |
|
|
|
字体族 |
|
|
|
最大行数 |
|
|
|
溢出处理 |
|
|
|
文本装饰 |
|
|
|
字间距 |
|
2.2 Image 图片组件
Image 组件用于展示图片,支持本地资源、网络图片和Base64数据。
// ImageExample.ets
@Entry
@Component
struct ImageExample {
build() {
Column({ space: 16 }) {
// 本地资源图片
Image($r('app.media.icon'))
.width(100)
.height(100)
.borderRadius(12)
// 网络图片
Image('https://developer.huawei.com/images/icon/harmonyos.png')
.width(200)
.height(120)
.objectFit(ImageFit.Cover) // 填充模式
.borderRadius(8)
.shadow({ // 阴影效果
radius: 10,
color: 'rgba(0,0,0,0.3)',
offsetX: 2,
offsetY: 2
})
// SVG矢量图片
Image($r('app.media.ic_arrow_right'))
.width(24)
.height(24)
.fillColor('#007DFF') // SVG着色
// 带占位图和错误图的网络图片
Image('https://example.com/image.png')
.width(200)
.height(150)
.alt($r('app.media.placeholder')) // 加载失败显示
.objectFit(ImageFit.Contain)
.onComplete(() => {
console.info('图片加载成功')
})
.onError(() => {
console.error('图片加载失败')
})
}
.width('100%')
.padding(16)
}
}
objectFit 填充模式对比:
|
模式 |
说明 |
适用场景 |
|---|---|---|
|
|
等比缩放,填满容器,可能裁切 |
头图、封面 |
|
|
等比缩放,完整显示,可能留白 |
详情图 |
|
|
拉伸填满,可能变形 |
不推荐 |
|
|
保持原始尺寸 |
小图标 |
2.3 Button 按钮组件
Button 组件支持多种样式和状态,是交互的核心组件。
// ButtonExample.ets
@Entry
@Component
struct ButtonExample {
@State isLoading: boolean = false
build() {
Column({ space: 20 }) {
// 基础按钮
Button('点击我')
.onClick(() => {
console.info('按钮被点击')
})
// 主要按钮(强调色)
Button('主要操作')
.type(ButtonType.Capsule) // 胶囊形状
.stateEffect(true) // 点击态效果
.backgroundColor('#007DFF')
.fontColor(Color.White)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width('90%')
.height(48)
.borderRadius(24)
// 次要按钮(描边样式)
Button('次要操作')
.type(ButtonType.Capsule)
.backgroundColor(Color.Transparent)
.fontColor('#007DFF')
.border({
width: 1,
color: '#007DFF',
style: BorderStyle.Solid
})
.width('90%')
.height(48)
// 危险操作按钮
Button('删除')
.type(ButtonType.Capsule)
.backgroundColor('#FF4D4F')
.fontColor(Color.White)
.width('90%')
.height(48)
// 带图标的按钮
Button({ type: ButtonType.Capsule, stateEffect: true }) {
Row({ space: 8 }) {
Image($r('app.media.ic_add'))
.width(18)
.height(18)
.fillColor(Color.White)
Text('新建')
.fontSize(16)
.fontColor(Color.White)
}
}
.backgroundColor('#007DFF')
.width('90%')
.height(48)
// 加载状态按钮
Button(this.isLoading ? '加载中...' : '提交订单')
.type(ButtonType.Capsule)
.backgroundColor(this.isLoading ? '#CCCCCC' : '#007DFF')
.fontColor(Color.White)
.width('90%')
.height(48)
.enabled(!this.isLoading)
.onClick(() => {
this.isLoading = true
// 模拟异步操作
setTimeout(() => {
this.isLoading = false
}, 2000)
})
}
.width('100%')
.padding(16)
}
}
2.4 TextInput 输入框组件
// TextInputExample.ets
@Entry
@Component
struct TextInputExample {
@State username: string = ''
@State password: string = ''
@State searchKey: string = ''
build() {
Column({ space: 16 }) {
// 普通输入框
TextInput({ placeholder: '请输入用户名', text: this.username })
.type(InputType.Normal)
.maxLength(20)
.onChange((value: string) => {
this.username = value
})
.width('90%')
.height(48)
.padding({ left: 16, right: 16 })
.borderRadius(8)
.backgroundColor('#F5F5F5')
// 密码输入框
TextInput({ placeholder: '请输入密码', text: this.password })
.type(InputType.Password)
.maxLength(20)
.onChange((value: string) => {
this.password = value
})
.width('90%')
.height(48)
.padding({ left: 16, right: 16 })
.borderRadius(8)
.backgroundColor('#F5F5F5')
// 带搜索图标的输入框
Row() {
Image($r('app.media.ic_search'))
.width(20)
.height(20)
.margin({ left: 12 })
.fillColor('#999999')
TextInput({ placeholder: '搜索内容', text: this.searchKey })
.type(InputType.Normal)
.onChange((value: string) => {
this.searchKey = value
})
.layoutWeight(1)
.height(40)
.backgroundColor(Color.Transparent)
.padding({ left: 8, right: 12 })
}
.width('90%')
.height(48)
.borderRadius(24)
.backgroundColor('#F5F5F5')
.alignItems(VerticalAlign.Center)
// 多行文本输入
TextInput({ placeholder: '请输入详细描述...' })
.type(InputType.Normal)
.width('90%')
.height(120)
.padding(16)
.borderRadius(8)
.backgroundColor('#F5F5F5')
.textAlign(TextAlign.Start)
}
.width('100%')
.padding(16)
}
}
2.5 Toggle 开关组件
// ToggleExample.ets
@Entry
@Component
struct ToggleExample {
@State isOn: boolean = true
@State isNotify: boolean = false
build() {
Row({ space: 16 }) {
// 开关样式
Toggle({ type: ToggleType.Switch, isOn: this.isOn })
.selectedColor('#007DFF')
.switchPointColor(Color.White)
.onChange((isOn: boolean) => {
this.isOn = isOn
console.info(`开关状态: ${isOn ? '开启' : '关闭'}`)
})
Text(this.isOn ? '已开启' : '已关闭')
.fontSize(16)
.fontColor(this.isOn ? '#007DFF' : '#999999')
}
.width('100%')
.padding(16)
}
}
2.6 Progress 进度条组件
// ProgressExample.ets
@Entry
@Component
struct ProgressExample {
@State progressValue: number = 40
build() {
Column({ space: 20 }) {
// 线性进度条
Progress({ value: this.progressValue, total: 100, type: ProgressType.Linear })
.width('90%')
.height(8)
.color('#007DFF')
.backgroundColor('#E8E8E8')
// 模糊进度条(不确定进度)
Progress({ value: 0, total: 100, type: ProgressType.ScaleRing })
.width(80)
.height(80)
.color('#007DFF')
// 环形进度条
Progress({ value: this.progressValue, total: 100, type: ProgressType.Ring })
.width(100)
.height(100)
.color('#007DFF')
.backgroundColor('#E8E8E8')
Text(`当前进度: ${this.progressValue}%`)
.fontSize(16)
Button('增加进度')
.onClick(() => {
if (this.progressValue < 100) {
this.progressValue += 10
}
})
}
.width('100%')
.padding(16)
}
}
2.7 List 列表组件
List 是高性能滚动容器,适用于长列表场景。
// ListExample.ets
interface ListItemData {
id: number
title: string
description: string
icon: Resource
}
@Entry
@Component
struct ListExample {
private items: ListItemData[] = Array.from({ length: 20 }, (_, i) => ({
id: i,
title: `列表项 ${i + 1}`,
description: `这是第 ${i + 1} 个列表项的描述信息`,
icon: $r('app.media.ic_list_item')
}))
build() {
List({ space: 8 }) {
ForEach(this.items, (item: ListItemData) => {
ListItem() {
Row({ space: 12 }) {
Image(item.icon)
.width(48)
.height(48)
.borderRadius(8)
Column({ space: 4 }) {
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
Text(item.description)
.fontSize(14)
.fontColor('#999999')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Image($r('app.media.ic_arrow_right'))
.width(16)
.height(16)
.fillColor('#CCCCCC')
}
.width('100%')
.padding(12)
.backgroundColor(Color.White)
.borderRadius(12)
}
}, (item: ListItemData) => item.id.toString())
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.divider({ // 分割线
strokeWidth: 0.5,
color: '#E8E8E8',
startMargin: 60,
endMargin: 16
})
}
}
三、布局系统详解
ArkUI提供了多种布局容器,掌握它们的特性和使用场景是构建复杂界面的关键。
3.1 Column 线性布局(垂直)
Column 将子组件从上到下依次排列。
// ColumnExample.ets
@Entry
@Component
struct ColumnExample {
build() {
Column({ space: 12 }) {
// space 控制子组件间距
Text('Header')
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text('Content Area')
.fontSize(16)
Text('Footer')
.fontSize(14)
.fontColor('#999999')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.SpaceBetween) // 两端对齐
.alignItems(HorizontalAlign.Center) // 水平居中
.padding(16)
.backgroundColor('#FFFFFF')
}
}
justifyContent 取值说明:
|
值 |
说明 |
效果 |
|---|---|---|
|
|
靠起始端 |
子组件顶部对齐 |
|
|
居中 |
子组件垂直居中 |
|
|
靠末尾端 |
子组件底部对齐 |
|
|
两端对齐 |
首尾贴边,中间均分 |
|
|
等间距 |
每个子组件两侧等间距 |
|
|
均匀分布 |
所有间距相等 |
3.2 Row 线性布局(水平)
Row 将子组件从左到右依次排列。
// RowExample.ets
@Entry
@Component
struct RowExample {
build() {
Column({ space: 16 }) {
// 基础水平布局
Row({ space: 12 }) {
Text('标签1')
.fontSize(14)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor('#E8F4FD')
.borderRadius(16)
.fontColor('#007DFF')
Text('标签2')
.fontSize(14)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor('#FFF1E8')
.borderRadius(16)
.fontColor('#FF6B35')
Text('标签3')
.fontSize(14)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor('#E8F8E8')
.borderRadius(16)
.fontColor('#52C41A')
}
// 卡片布局示例
Row({ space: 12 }) {
Image($r('app.media.avatar'))
.width(60)
.height(60)
.borderRadius(30)
Column({ space: 4 }) {
Text('张三')
.fontSize(18)
.fontWeight(FontWeight.Bold)
Text('鸿蒙开发工程师')
.fontSize(14)
.fontColor('#666666')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1) // 占据剩余空间
Button('关注')
.type(ButtonType.Capsule)
.fontSize(14)
.height(32)
.backgroundColor('#007DFF')
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
}
.width('100%')
.padding(16)
}
}
3.3 Stack 层叠布局
Stack 将子组件层叠显示,后添加的组件在上层,适合实现叠加效果。
// StackExample.ets
@Entry
@Component
struct StackExample {
build() {
Column({ space: 16 }) {
// 图片上叠加文字
Stack({ alignContent: Alignment.BottomStart }) {
Image($r('app.media.banner'))
.width('100%')
.height(200)
.borderRadius(12)
.objectFit(ImageFit.Cover)
// 渐变遮罩 + 文字
Column() {
Text('鸿蒙NEXT应用开发实战')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('2026年最新教程')
.fontSize(14)
.fontColor('rgba(255,255,255,0.8)')
}
.width('100%')
.padding(16)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], ['rgba(0,0,0,0.7)', 1]]
})
}
.width('100%')
.height(200)
// 角标示例
Stack({ alignContent: Alignment.TopEnd }) {
Image($r('app.media.ic_message'))
.width(40)
.height(40)
Text('99+')
.fontSize(10)
.fontColor(Color.White)
.backgroundColor('#FF4D4F')
.borderRadius(10)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.translate({ x: 8, y: -8 }) // 微调位置
}
.width(56)
.height(56)
}
.width('100%')
.padding(16)
}
}
3.4 Flex 弹性布局
Flex 是最灵活的布局容器,支持主轴方向、换行、对齐等丰富配置。
// FlexExample.ets
@Entry
@Component
struct FlexExample {
build() {
Column({ space: 20 }) {
Text('Flex Wrap 示例(标签流式布局)')
.fontSize(16)
.fontWeight(FontWeight.Medium)
// 流式标签布局
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start, space: { main: LengthMetrics.vp(8), cross: LengthMetrics.vp(8) } }) {
ForEach(['鸿蒙', 'ArkUI', 'HarmonyOS', 'NEXT', '声明式UI', '分布式', '原子化服务', '元服务'], (tag: string) => {
Text(tag)
.fontSize(14)
.fontColor('#007DFF')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.border({
width: 1,
color: '#007DFF',
style: BorderStyle.Solid
})
.borderRadius(20)
})
}
.width('100%')
Text('Flex Direction 示例(水平分布)')
.fontSize(16)
.fontWeight(FontWeight.Medium)
// 均匀分布的按钮组
Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceEvenly }) {
Button('取消')
.type(ButtonType.Capsule)
.backgroundColor('#F5F5F5')
.fontColor('#666666')
.layoutWeight(1)
.margin({ right: 8 })
Button('确认')
.type(ButtonType.Capsule)
.backgroundColor('#007DFF')
.fontColor(Color.White)
.layoutWeight(1)
.margin({ left: 8 })
}
.width('100%')
}
.width('100%')
.padding(16)
}
}
3.5 Grid 网格布局
Grid 适用于九宫格、商品列表等网格排列场景。
// GridExample.ets
@Entry
@Component
struct GridExample {
private gridItems: string[] = [
'美食', '电影', '酒店', 'KTV',
'外卖', '打车', '充值', '更多'
]
build() {
Column() {
Text('服务入口')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
Grid() {
ForEach(this.gridItems, (item: string, index: number) => {
GridItem() {
Column({ space: 8 }) {
Image($r(`app.media.ic_grid_${index}`))
.width(40)
.height(40)
Text(item)
.fontSize(12)
.fontColor('#333333')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr') // 4列等宽
.rowsTemplate('1fr 1fr') // 2行等高
.columnsGap(8)
.rowsGap(8)
.width('100%')
.height(200)
.backgroundColor(Color.White)
.borderRadius(12)
.padding(16)
}
.width('100%')
.padding(16)
}
}
3.6 RelativeContainer 相对布局
RelativeContainer 允许子组件通过相对关系定位,适合复杂界面。
// RelativeContainerExample.ets
@Entry
@Component
struct RelativeContainerExample {
build() {
RelativeContainer() {
// 居中的标题
Text('相对布局示例')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.id('title')
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
// 左侧返回按钮
Image($r('app.media.ic_back'))
.width(24)
.height(24)
.id('backBtn')
.alignRules({
left: { anchor: '__container__', align: HorizontalAlign.Start },
center: { anchor: 'title', align: VerticalAlign.Center }
})
.margin({ left: 16 })
// 右侧操作按钮
Image($r('app.media.ic_more'))
.width(24)
.height(24)
.id('moreBtn')
.alignRules({
right: { anchor: '__container__', align: HorizontalAlign.End },
center: { anchor: 'title', align: VerticalAlign.Center }
})
.margin({ right: 16 })
}
.width('100%')
.height(56)
.backgroundColor(Color.White)
}
}
四、自定义组件开发教程
4.1 组件基础结构
ArkUI的自定义组件通过 @Component 装饰器定义,支持参数传递和生命周期管理。
// CustomCard.ets
@Component
export struct CustomCard {
// 组件参数(外部传入)
@Prop title: string = ''
@Prop content: string = ''
@Prop icon: Resource = $r('app.media.ic_default')
// 内部状态
@State isExpanded: boolean = false
// 事件回调
onCardClick?: () => void
build() {
Column({ space: 8 }) {
Row({ space: 12 }) {
Image(this.icon)
.width(32)
.height(32)
Text(this.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.layoutWeight(1)
Image(this.isExpanded ? $r('app.media.ic_up') : $r('app.media.ic_down'))
.width(16)
.height(16)
}
.width('100%')
if (this.isExpanded) {
Text(this.content)
.fontSize(14)
.fontColor('#666666')
.width('100%')
.transition({
type: TransitionType.Insert,
opacity: 0,
translate: { y: -20 }
})
}
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 4, color: 'rgba(0,0,0,0.08)', offsetY: 2 })
.onClick(() => {
this.isExpanded = !this.isExpanded
this.onCardClick?.()
})
}
}
// 使用示例
@Entry
@Component
struct CustomCardDemo {
build() {
Column({ space: 12 }) {
CustomCard({
title: '鸿蒙NEXT特性',
content: 'ArkUI声明式开发范式,让UI开发更简洁高效。支持一次开发多端部署,覆盖手机、平板、智慧屏等设备。',
icon: $r('app.media.ic_feature')
})
CustomCard({
title: '开发工具',
content: 'DevEco Studio提供完整的开发、调试、测试环境,支持模拟器和真机调试。',
icon: $r('app.media.ic_tool')
})
}
.width('100%')
.padding(16)
.backgroundColor('#F5F5F5')
}
}
4.2 @Builder 自定义构建函数
@Builder 用于定义可复用的UI片段,避免代码重复。
// BuilderExample.ets
@Entry
@Component
struct BuilderExample {
@State activeTab: number = 0
// 底部导航栏 Builder
@Builder
TabBarBuilder() {
Row({ space: 0 }) {
this.TabItemBuilder('首页', $r('app.media.ic_home'), 0)
this.TabItemBuilder('发现', $r('app.media.ic_discover'), 1)
this.TabItemBuilder('消息', $r('app.media.ic_message'), 2)
this.TabItemBuilder('我的', $r('app.media.ic_profile'), 3)
}
.width('100%')
.height(56)
.backgroundColor(Color.White)
.border({ width: { top: 0.5 }, color: '#E8E8E8' })
}
// 单个Tab项 Builder
@Builder
TabItemBuilder(label: string, icon: Resource, index: number) {
Column({ space: 4 }) {
Image(icon)
.width(24)
.height(24)
.fillColor(this.activeTab === index ? '#007DFF' : '#999999')
Text(label)
.fontSize(10)
.fontColor(this.activeTab === index ? '#007DFF' : '#999999')
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
.height('100%')
.onClick(() => {
this.activeTab = index
})
}
build() {
Column() {
// 内容区域
Column() {
Text(`当前页面: ${this.activeTab}`)
.fontSize(20)
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
// 底部导航
this.TabBarBuilder()
}
.width('100%')
.height('100%')
}
}
4.3 @Extend 扩展原生组件
@Extend 用于扩展原生组件的样式和能力。
// ExtendExample.ets
// 扩展 Text 组件 - 标题样式
@Extend(Text)
function titleStyle() {
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
.lineHeight(32)
}
// 扩展 Text 组件 - 副标题样式
@Extend(Text)
function subtitleStyle() {
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#666666')
.lineHeight(24)
}
// 扩展 Text 组件 - 辅助文字样式
@Extend(Text)
function captionStyle() {
.fontSize(12)
.fontColor('#999999')
.lineHeight(18)
}
// 扩展 Image 组件 - 圆形头像
@Extend(Image)
function avatarStyle(size: number = 48) {
.width(size)
.height(size)
.borderRadius(size / 2)
.clip(true)
}
// 扩展 Button 组件 - 主要按钮
@Extend(Button)
function primaryButtonStyle() {
.type(ButtonType.Capsule)
.backgroundColor('#007DFF')
.fontColor(Color.White)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.height(48)
.width('100%')
}
// 使用示例
@Entry
@Component
struct ExtendDemo {
build() {
Column({ space: 16 }) {
Text('文章标题') // 应用 titleStyle
.titleStyle()
Text('文章副标题') // 应用 subtitleStyle
.subtitleStyle()
Image($r('app.media.avatar'))
.avatarStyle(64) // 应用 avatarStyle,传入自定义尺寸
Text('发布时间:2026-05-07') // 应用 captionStyle
.captionStyle()
Button('立即体验') // 应用 primaryButtonStyle
.primaryButtonStyle()
}
.width('100%')
.padding(16)
}
}
4.4 @Styles 通用样式复用
@Styles 用于提取通用的样式属性。
// StylesExample.ets
@Styles
function cardStyle() {
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 4, color: 'rgba(0,0,0,0.06)', offsetY: 2 })
}
@Styles
function sectionTitleStyle() {
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
.margin({ bottom: 12 })
}
@Entry
@Component
struct StylesDemo {
build() {
Column({ space: 16 }) {
Column() {
Text('订单信息')
.sectionTitleStyle()
Text('订单号: HM20260507001')
.fontSize(14)
.fontColor('#666666')
}
.cardStyle()
Column() {
Text('收货地址')
.sectionTitleStyle()
Text('广东省深圳市龙岗区华为基地')
.fontSize(14)
.fontColor('#666666')
}
.cardStyle()
}
.width('100%')
.padding(16)
.backgroundColor('#F5F5F5')
}
}
4.5 @CustomDecoration 自定义装饰器(实战案例)
接下来我们通过一个完整的实战案例,综合运用上述知识点,开发一个商品卡片组件。
// ProductCard.ets
// 自定义商品卡片组件
// 样式复用
@Styles
function cardShadow() {
.shadow({ radius: 8, color: 'rgba(0,0,0,0.08)', offsetY: 4 })
}
// 扩展文本样式
@Extend(Text)
function priceStyle() {
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FF4D4F')
}
@Extend(Text)
function originalPriceStyle() {
.fontSize(12)
.fontColor('#999999')
.decoration({ type: TextDecorationType.LineThrough })
}
// 评分星星 Builder
@Component
struct StarRating {
@Prop rating: number = 0
@Builder
StarIcon(filled: boolean) {
Text(filled ? '★' : '☆')
.fontSize(14)
.fontColor(filled ? '#FFD700' : '#CCCCCC')
}
build() {
Row({ space: 2 }) {
ForEach([1, 2, 3, 4, 5], (star: number) => {
this.StarIcon(star <= this.rating)
})
Text(`(${this.rating}.0)`)
.fontSize(12)
.fontColor('#999999')
.margin({ left: 4 })
}
}
}
// 商品卡片组件
@Component
export struct ProductCard {
@Prop productName: string = ''
@Prop productImage: Resource = $r('app.media.ic_product')
@Prop currentPrice: number = 0
@Prop originalPrice: number = 0
@Prop salesCount: number = 0
@Prop rating: number = 5
// 事件回调
onProductClick?: () => void
onAddCart?: () => void
// 计算折扣
private get discount(): string {
if (this.originalPrice > 0 && this.currentPrice > 0) {
return (this.currentPrice / this.originalPrice * 10).toFixed(1)
}
return '10.0'
}
build() {
Column({ space: 8 }) {
// 商品图片
Stack({ alignContent: Alignment.TopLeft }) {
Image(this.productImage)
.width('100%')
.height(180)
.objectFit(ImageFit.Cover)
.borderRadius({ topLeft: 12, topRight: 12 })
// 折扣标签
if (parseFloat(this.discount) < 10) {
Text(`${this.discount}折`)
.fontSize(10)
.fontColor(Color.White)
.backgroundColor('#FF4D4F')
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius({ topLeft: 12, bottomRight: 8 })
}
}
.width('100%')
// 商品信息
Column({ space: 4 }) {
Text(this.productName)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.lineHeight(20)
// 评分
StarRating({ rating: this.rating })
// 价格区域
Row({ space: 8 }) {
Text(`¥${this.currentPrice}`)
.priceStyle()
Text(`¥${this.originalPrice}`)
.originalPriceStyle()
Blank()
Text(`已售${this.salesCount > 10000 ? (this.salesCount / 10000).toFixed(1) + '万' : this.salesCount}件`)
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
// 加入购物车按钮
Button('加入购物车')
.type(ButtonType.Capsule)
.fontSize(12)
.height(28)
.backgroundColor('#FF6B35')
.fontColor(Color.White)
.width('100%')
.margin({ top: 4 })
.onClick(() => {
this.onAddCart?.()
})
}
.padding({ left: 8, right: 8, bottom: 8 })
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(12)
.cardShadow()
.onClick(() => {
this.onProductClick?.()
})
}
}
// 使用示例
@Entry
@Component
struct ProductCardDemo {
build() {
Column({ space: 12 }) {
Text('热门商品')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.padding({ left: 16, right: 16 })
Row({ space: 12 }) {
ProductCard({
productName: '鸿蒙NEXT开发实战指南(2026版)',
productImage: $r('app.media.product_book'),
currentPrice: 79.9,
originalPrice: 129.0,
salesCount: 15800,
rating: 5,
onProductClick: () => {
console.info('点击了商品1')
},
onAddCart: () => {
console.info('商品1加入购物车')
}
})
.layoutWeight(1)
ProductCard({
productName: 'HarmonyOS智能手表运动版',
productImage: $r('app.media.product_watch'),
currentPrice: 1299,
originalPrice: 1599,
salesCount: 8600,
rating: 4,
onProductClick: () => {
console.info('点击了商品2')
},
onAddCart: () => {
console.info('商品2加入购物车')
}
})
.layoutWeight(1)
}
.padding({ left: 16, right: 16 })
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
五、样式与主题系统
5.1 内联样式
最直接的样式设置方式,适合一次性使用的样式。
// InlineStyleExample.ets
@Entry
@Component
struct InlineStyleExample {
build() {
Column({ space: 16 }) {
// 完整的内联样式示例
Text('内联样式示例')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
.textAlign(TextAlign.Center)
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({ radius: 4, color: 'rgba(0,0,0,0.08)', offsetY: 2 })
// 链式调用顺序很重要
Text('注意链式调用顺序')
.fontSize(16)
.width('100%')
.height(80)
.backgroundColor('#E8F4FD')
.borderRadius(8)
.textAlign(TextAlign.Center)
}
.width('100%')
.padding(16)
}
}
5.2 全局样式复用
通过 @Styles 和资源文件管理全局样式。
// styles/CommonStyles.ets
// 全局通用样式定义
@Styles
function statusBarHeight() {
.padding({ top: 'statusBarHeight' })
}
@Styles
function pageBackground() {
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Styles
function whiteCard() {
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ bottom: 12 })
}
@Styles
function primaryText() {
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#1A1A1A')
}
@Styles
function secondaryText() {
.fontSize(14)
.fontColor('#666666')
}
@Styles
function captionText() {
.fontSize(12)
.fontColor('#999999')
}
// 使用示例
@Entry
@Component
struct StyleReuseDemo {
build() {
Column() {
Column() {
Text('主标题')
.primaryText()
Text('副标题描述')
.secondaryText()
Text('辅助说明文字')
.captionText()
}
.whiteCard()
Column() {
Text('另一个卡片')
.primaryText()
Text('卡片内容')
.secondaryText()
}
.whiteCard()
}
.pageBackground()
.statusBarHeight()
.padding(16)
}
}
5.3 深色模式适配
鸿蒙NEXT支持系统级深色模式,需要做好适配。
// DarkModeExample.ets
@Entry
@Component
struct DarkModeExample {
@StorageProp('currentColorMode') colorMode: number = 0 // 0: 浅色, 1: 深色
// 根据模式获取颜色
private getBgColor(): ResourceColor {
return this.colorMode === 1 ? '#1A1A1A' : '#FFFFFF'
}
private getTextColor(): ResourceColor {
return this.colorMode === 1 ? '#E8E8E8' : '#1A1A1A'
}
private getSubTextColor(): ResourceColor {
return this.colorMode === 1 ? '#AAAAAA' : '#666666'
}
private getCardBgColor(): ResourceColor {
return this.colorMode === 1 ? '#2A2A2A' : '#FFFFFF'
}
build() {
Column({ space: 12 }) {
// 使用 $r 获取系统资源颜色(推荐方式)
Text('系统资源颜色适配')
.fontSize(20)
.fontColor($r('sys.color.font_primary'))
.width('100%')
.padding(16)
.backgroundColor($r('sys.color.background_primary'))
.borderRadius(12)
// 手动适配深色模式
Text('手动适配深色模式')
.fontSize(16)
.fontColor(this.getTextColor())
.width('100%')
.padding(16)
.backgroundColor(this.getCardBgColor())
.borderRadius(12)
Text('使用条件渲染适配不同主题')
.fontSize(14)
.fontColor(this.getSubTextColor())
.padding({ left: 16 })
}
.width('100%')
.height('100%')
.backgroundColor(this.getBgColor())
.padding(16)
}
}
最佳实践:
-
优先使用
$r('sys.color.xxx')系统资源颜色 -
避免硬编码颜色值,使用主题变量
-
图片资源提供深色模式版本(
$r('app.media.ic_icon_dark'))
六、动画效果实现
6.1 属性动画
通过 animation 属性为状态变化添加动画效果。
// AnimationExample.ets
@Entry
@Component
struct AnimationExample {
@State isExpanded: boolean = false
@State rotateAngle: number = 0
@State opacityValue: number = 1
@State scaleValue: number = 1
build() {
Column({ space: 24 }) {
// 1. 尺寸动画
Column() {
Text('点击展开/收起')
.fontSize(16)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
}
.width(this.isExpanded ? '100%' : '50%')
.height(this.isExpanded ? 200 : 80)
.backgroundColor('#007DFF')
.borderRadius(this.isExpanded ? 16 : 8)
.animation({
duration: 300,
curve: Curve.EaseInOut
})
.onClick(() => {
this.isExpanded = !this.isExpanded
})
// 2. 旋转动画
Image($r('app.media.ic_refresh'))
.width(48)
.height(48)
.rotate({ angle: this.rotateAngle })
.animation({
duration: 1000,
curve: Curve.Linear
})
.onClick(() => {
this.rotateAngle += 360
})
// 3. 透明度动画
Text('淡入淡出效果')
.fontSize(20)
.opacity(this.opacityValue)
.animation({
duration: 500,
curve: Curve.EaseIn
})
.onClick(() => {
this.opacityValue = this.opacityValue === 1 ? 0 : 1
})
// 4. 缩放动画
Text('缩放动画')
.fontSize(18)
.scale({ x: this.scaleValue, y: this.scaleValue })
.animation({
duration: 300,
curve: Curve.FastOutSlowIn
})
.onClick(() => {
this.scaleValue = this.scaleValue === 1 ? 1.2 : 1
})
}
.width('100%')
.padding(24)
.alignItems(HorizontalAlign.Center)
}
}
动画曲线说明:
|
曲线 |
说明 |
适用场景 |
|---|---|---|
|
|
匀速 |
旋转、进度条 |
|
|
先慢后快 |
淡出、收起 |
|
|
先快后慢 |
淡入、展开 |
|
|
两头慢中间快 |
通用过渡 |
|
|
Material Design标准曲线 |
大多数动画 |
|
|
弹簧效果 |
有弹性的交互 |
6.2 显式动画
使用 animateTo 函数精确控制动画触发。
// ExplicitAnimationExample.ets
@Entry
@Component
struct ExplicitAnimationExample {
@State boxWidth: number = 100
@State boxHeight: number = 100
@State boxColor: string = '#007DFF'
@State borderRadius: number = 0
build() {
Column({ space: 32 }) {
// 动画目标元素
Column() {
Text('动画')
.fontSize(16)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
}
.width(this.boxWidth)
.height(this.boxHeight)
.backgroundColor(this.boxColor)
.borderRadius(this.borderRadius)
// 控制按钮
Column({ space: 12 }) {
Button('变大')
.width('80%')
.onClick(() => {
animateTo({
duration: 500,
curve: Curve.FastOutSlowIn,
onFinish: () => {
console.info('动画完成')
}
}, () => {
this.boxWidth = 200
this.boxHeight = 200
this.boxColor = '#FF6B35'
this.borderRadius = 100
})
})
Button('还原')
.width('80%')
.onClick(() => {
animateTo({
duration: 300,
curve: Curve.EaseInOut
}, () => {
this.boxWidth = 100
this.boxHeight = 100
this.boxColor = '#007DFF'
this.borderRadius = 0
})
})
}
}
.width('100%')
.padding(32)
.alignItems(HorizontalAlign.Center)
}
}
6.3 转场动画
组件插入/删除时的过渡动画。
// TransitionExample.ets
@Entry
@Component
struct TransitionExample {
@State showElement: boolean = true
build() {
Column({ space: 24 }) {
Button(this.showElement ? '隐藏元素' : '显示元素')
.onClick(() => {
this.showElement = !this.showElement
})
if (this.showElement) {
// 内置过渡效果
Text('内置淡入过渡')
.fontSize(18)
.padding(24)
.backgroundColor('#E8F4FD')
.borderRadius(12)
.transition(TransitionEffect.OPACITY) // 淡入淡出
// 自定义过渡效果
Column() {
Text('自定义滑入过渡')
.fontSize(18)
.fontColor(Color.White)
}
.padding(24)
.backgroundColor('#007DFF')
.borderRadius(12)
.transition(
TransitionEffect.asymmetric({
appear: TransitionEffect.move({ x: -200 }).animation({ duration: 500 }),
disappear: TransitionEffect.move({ x: 200 }).animation({ duration: 500 })
})
)
// 组合过渡效果
Text('组合动画过渡')
.fontSize(18)
.padding(24)
.backgroundColor('#52C41A')
.borderRadius(12)
.fontColor(Color.White)
.transition(
TransitionEffect.combine(
TransitionEffect.OPACITY.animation({ duration: 300 }),
TransitionEffect.scale({ x: 0.5, y: 0.5 }).animation({ duration: 400 }),
TransitionEffect.rotate({ angle: 15 }).animation({ duration: 500 })
)
)
}
}
.width('100%')
.padding(24)
.alignItems(HorizontalAlign.Center)
}
}
6.4 路径动画与粒子动画
鸿蒙NEXT还支持更高级的动画效果。
// PathAnimationExample.ets
@Entry
@Component
struct PathAnimationExample {
@State pathProgress: number = 0
build() {
Column({ space: 32 }) {
// 沿路径动画
Stack() {
// 路径轨迹(可视化)
Path()
.width(300)
.height(200)
.commands('M 20 100 C 80 20, 160 180, 280 100')
.stroke('#E8E8E8')
.strokeWidth(2)
.fill('none')
// 运动的元素
Circle({ width: 20, height: 20 })
.fill('#007DFF')
.offset({
x: this.pathProgress * 260 + 10,
y: Math.sin(this.pathProgress * Math.PI) * 80 + 90
})
.animation({
duration: 2000,
curve: Curve.Linear,
iterations: -1 // 无限循环
})
}
.width(300)
.height(200)
Button('开始路径动画')
.onClick(() => {
this.pathProgress = 1
})
// 加载动画示例
Row({ space: 4 }) {
ForEach([0, 1, 2], (index: number) => {
Circle({ width: 12, height: 12 })
.fill('#007DFF')
.opacity(0.3)
.scale({ x: 0.8, y: 0.8 })
.animation({
duration: 600,
delay: index * 200,
iterations: -1,
curve: Curve.EaseInOut
})
})
}
}
.width('100%')
.padding(32)
.alignItems(HorizontalAlign.Center)
}
}
七、踩坑记录与最佳实践
踩坑1:ForEach的keyGenerator缺失导致列表渲染异常
问题描述:使用 ForEach 渲染列表时,如果不提供第三个参数(keyGenerator),当数据发生变化时可能导致列表项复用错误。
错误写法:
ForEach(this.list, (item: string) => {
ListItem() {
Text(item)
}
// 缺少 keyGenerator
})
正确写法:
ForEach(this.list, (item: string, index: number) => {
ListItem() {
Text(item)
}
}, (item: string, index: number) => `${item}_${index}`) // 提供唯一key
踩坑2:animation属性位置影响动画效果
问题描述:animation 属性必须放在要动画的属性之后,否则动画不会生效。
错误写法:
Text('Hello')
.animation({ duration: 300 }) // 放在前面,不生效
.fontSize(this.isLarge ? 24 : 16)
正确写法:
Text('Hello')
.fontSize(this.isLarge ? 24 : 16) // 要动画的属性在前
.animation({ duration: 300 }) // animation在后
踩坑3:组件id在RelativeContainer中必须唯一
问题描述:在 RelativeContainer 中,子组件的 id 必须唯一,否则布局会混乱。
解决方案:使用常量定义id,避免字符串重复。
// 定义id常量
private static readonly ID_HEADER = 'header'
private static readonly ID_CONTENT = 'content'
private static readonly ID_FOOTER = 'footer'
RelativeContainer() {
Text('Header')
.id(RelativeContainerExample.ID_HEADER)
.alignRules({ top: { anchor: '__container__' } })
Text('Content')
.id(RelativeContainerExample.ID_CONTENT)
.alignRules({ top: { anchor: RelativeContainerExample.ID_HEADER } })
}
踩坑4:网络图片需要配置网络权限
问题描述:直接加载网络图片不显示,没有报错。
解决方案:在 module.json5 中配置网络权限。
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
踩坑5:状态变量初始化时机
问题描述:在组件外部定义的变量无法触发UI刷新。
错误写法:
let count = 0 // 普通变量,不触发UI刷新
Button('点击')
.onClick(() => {
count++ // UI不会更新
})
正确写法:
@State count: number = 0 // 状态变量,触发UI刷新
Button('点击')
.onClick(() => {
this.count++ // UI会自动更新
})
踩坑6:Row/Column的layoutWeight使用注意事项
问题描述:layoutWeight 会让组件占据剩余空间,但如果父容器没有固定尺寸,可能不生效。
解决方案:确保父容器有明确的尺寸约束。
// 确保Row有宽度约束
Row() {
Text('左侧')
.width(80)
Text('右侧占满')
.layoutWeight(1) // 占据剩余空间
}
.width('100%') // 必须设置宽度
最佳实践总结
|
实践 |
说明 |
|---|---|
|
组件粒度适中 |
单个组件不超过200行,过长则拆分 |
|
状态最小化 |
只用必要的 |
|
样式复用 |
使用 |
|
列表优化 |
|
|
图片优化 |
使用合适的 |
|
动画流畅 |
动画时长控制在300ms以内,使用合适的曲线 |
八、总结与下期预告
本文总结
本文系统讲解了鸿蒙NEXT ArkUI组件库的核心知识:
-
基础组件:掌握了
Text、Image、Button、TextInput、Toggle、Progress、List等常用组件的核心属性和使用方式 -
布局系统:理解了
Column、Row、Stack、Flex、Grid、RelativeContainer六大布局容器的特性和适用场景 -
自定义组件:学会了
@Component、@Builder、@Extend、@Styles的用法,能够开发可复用的业务组件 -
样式主题:掌握了内联样式、全局样式复用和深色模式适配方案
-
动画效果:实现了属性动画、显式动画、转场动画和路径动画
-
踩坑经验:总结了6个常见坑点和最佳实践
关键知识点速查表
|
知识点 |
核心要点 |
|---|---|
|
Text |
|
|
Image |
|
|
Button |
|
|
Column/Row |
|
|
Flex |
|
|
@Builder |
可复用的UI片段,避免代码重复 |
|
@Extend |
扩展原生组件样式 |
|
@Styles |
通用样式属性提取 |
|
animation |
放在目标属性之后,配置 |
下期预告
第3篇:状态管理一文通:@State、@Prop、@Link、@Provide/Consume全解析
状态管理是鸿蒙应用开发的核心难点。下期我们将深入讲解:
@State:组件内状态管理的最佳实践
@Prop:父子组件单向数据传递的正确姿势
@Link:父子组件双向数据绑定的实现原理
@Provide/@Consume:跨组件层级数据共享的高级用法状态管理常见陷阱与性能优化
关注我,第一时间获取更新!
鸿蒙NEXT开发实战系列 -- 全部文章:
ArkUI组件库完全指南(本文)
本文基于 HarmonyOS NEXT (API 14) 编写,代码示例均已在 DevEco Studio 5.0.5+ 环境下验证通过。如有问题欢迎评论区交流。
标签:鸿蒙NEXT | HarmonyOS NEXT | ArkUI | ArkTS | DevEco Studio | 组件库 | 布局系统 | @Builder | @Extend | @Styles | 自定义组件 | 声明式UI
更多推荐


所有评论(0)