跳到主要内容

第三章:ArkUI 组件与布局

本章配套代码:code/Chapter3(组件与布局综合展示 + 手势交互案例) 前置要求:已完成第二章 ArkTS 基础语法

3.1 学习目标

  • 掌握常用基础组件:Text、Button、TextInput、Toggle、Image
  • 理解容器组件:Column、Row、Stack、Grid、Flex
  • 掌握布局核心技巧:layoutWeightjustifyContentalignItems
  • 学会组件样式设置(尺寸、间距、圆角、阴影)
  • 掌握 ArkUI 手势系统:常用手势(点击/长按/拖动/旋转/捏合/滑动)与组合手势(GestureGroup)

3.2 ArkUI 概述

ArkUI 是 HarmonyOS 的声明式 UI 框架。核心思想:UI 由数据驱动,状态变化自动刷新界面

┌─────────────────────────────────────────────┐
│ ArkUI 声明式范式 │
│ │
│ 状态(@Local 等)──> build()──> UI组件树 │
│ │ │ │
│ └──────── 事件回调 ◄───┘ │
│ (onClick/onChange...) │
└─────────────────────────────────────────────┘

三大核心概念:

概念说明示例
组件UI 的基本单元TextButtonColumn
属性配置组件外观行为.fontSize().backgroundColor()
事件响应用户交互.onClick().onChange()

Text 文本

Text(value: string | Resource) 文本组件,用于展示文字内容。

Text('人生若只如初见')
.fontSize(20) // 字号
.fontWeight(FontWeight.Bold) // 字重
.fontColor('#333333') // 颜色
.textAlign(TextAlign.Center) // 对齐
.maxLines(2) // 最大行数
.textOverflow({ overflow: TextOverflow.Ellipsis }) // 超长省略
属性说明
fontSize字体大小(vp)
fontWeight字体粗细(Regular/Bold/...)
fontColor字体颜色
textAlign文本对齐
maxLines最大行数
textOverflow溢出处理(Ellipsis 省略号)

多段文本 Span 子组件:

Text() {
Span('重点:').fontColor('#ff6b6b').fontWeight(FontWeight.Bold)
Span('普通内容').fontColor('#333333')
}
.fontSize(16)

Button 按钮

Button(label: ResourceStr, options?: ButtonOptions) 按钮组件,用于响应用户点击。

Button('点击')
.type(ButtonType.Capsule) // 胶囊样式
.width(120)
.height(40)
.fontSize(16)
.backgroundColor('#007DFF')
.onClick(() => {
console.log('clicked')
})

ButtonType 类型:

类型外观说明
ButtonType.Capsule胶囊形圆角最大,不支持 borderRadius
ButtonType.Circle圆形不支持 borderRadius
ButtonType.Normal直角矩形支持 borderRadius 自定义圆角

💡 可内嵌子组件:Button() { Row() { Image(...); Text(...) } },实现"图标+文字"按钮。


TextInput 输入框

TextInput(value?: { placeholder?: ResourceStr, text?: ResourceStr, controller?: TextInputController }) 单行文本输入框。

TextInput({ placeholder: '请输入内容', text: this.inputText })
.width(200)
.height(44)
.type(InputType.Normal) // 输入类型:Normal/Password/Number...
.maxLength(20)
.onChange((value: string) => {
this.inputText = value
})
属性说明
placeholder占位提示文字
text初始内容
type输入类型(密码/数字/邮箱等)
maxLength最大长度
onChange内容变化回调

InputType 常用类型: Normal(普通)、Password(密码)、Number(数字)、Email(邮箱)。

小案例:密码可见性切换

@Local showPassword: boolean = false

TextInput({ placeholder: '请输入密码' })
.type(this.showPassword ? InputType.Normal : InputType.Password)
.onChange((v: string) => { /* 处理密码 */ })

Button(this.showPassword ? '隐藏' : '显示')
.onClick(() => { this.showPassword = !this.showPassword })

Toggle 开关

Toggle(options: { type: ToggleType, isOn?: boolean }) 开关/复选/状态按钮组件。

Toggle({ type: ToggleType.Switch, isOn: this.isOn })
.onChange((isOn: boolean) => {
this.isOn = isOn
})

ToggleType: Switch(开关)、Checkbox(复选)、Radio(单选)、Button(状态按钮)。

小案例:蓝牙开关

@Local bluetoothOn: boolean = false

Row() {
Text('Bluetooth')
Toggle({ type: ToggleType.Switch, isOn: this.bluetoothOn })
.onChange((isOn: boolean) => { this.bluetoothOn = isOn })
}

Image 图片

Image(src: PixelMap | ResourceStr | DrawableDescriptor) 图片组件,支持本地、资源、网络、字节流。

Image($r('app.media.logo'))   // 资源目录图片
.width(100)
.height(100)
.objectFit(ImageFit.Cover) // 填充方式
.borderRadius(10)

Image($rawfile('avatar.png')) // rawfile 目录图片
Image('https://example.com/x.png') // 网络图片(需网络权限)

图片来源:

方式语法说明
资源目录$r('app.media.xxx')resources/base/media
rawfile$rawfile('xxx.png')resources/rawfile
网络URL 字符串需 INTERNET 权限
字节流Image(pixelMap)动态生成

💡 objectFitCover(裁剪填充)、Contain(完整显示)、Fill(拉伸填满)。


Column 垂直布局

Column({ space?: number | string }) 子组件沿垂直方向(主轴)排列的容器。

Column({ space: 10 }) {   // space:子组件间距
Text('春眠不觉晓')
Text('处处闻啼鸟')
}
.width('100%')
.justifyContent(FlexAlign.Center) // 主轴对齐
.alignItems(HorizontalAlign.Center) // 交叉轴对齐

Row 水平布局

Row({ space?: number | string }) 子组件沿水平方向(主轴)排列的容器。

Row({ space: 10 }) {
Text('明月松间照')
Text('清泉石上流')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween) // 两端对齐

Stack 层叠布局

Stack({ alignContent?: Alignment }) 子组件层叠显示,后写的在上层。

Stack({ alignContent: Alignment.BottomEnd }) {
Column()
.width(200)
.height(200)
.backgroundColor('#ccdbfd')
Text('角标') // 层叠在最上层
.backgroundColor('#ff6b6b')
}

Alignment 枚举:TopStart/Top/Center/BottomEnd 等 9 个方位。

小案例:右上角"角标"徽标

Stack({ alignContent: Alignment.TopEnd }) {
Text('通知')
.padding(16)
.backgroundColor('#f0f0f0')
Text('3') // 数字角标
.fontSize(10)
.fontColor(Color.White)
.width(16)
.height(16)
.textAlign(TextAlign.Center)
.borderRadius(8)
.backgroundColor('#ff6b6b')
}

Flex 弹性布局

Flex(options?: FlexOptions) 灵活的主轴/换行控制,是 Row/Column 的通用父级。

@Entry
@ComponentV2
struct FlexExample {
@Local items: string[] = ['举头望明月', '低头思故乡', '海内存知己', '天涯若比邻', '落红不是无情物', '化作春泥更护花'] // ① 数据源

build() {
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap }) {
ForEach(this.items, (item: string) => {
Text(item)
.padding(8)
.margin(4)
.backgroundColor('#f0f0f0')
})
}
}
}

💡 数据源不必拘泥于数字或英文,换成诗词数组照样遍历,讲课时张口就来一句诗,风雅又提神——这正是我们在 1.8.2 配置 pmarr 诗词实时模板的意义。

Flex 属性说明
direction主轴方向(Row/Column)
wrap是否换行
justifyContent主轴对齐
alignItems交叉轴对齐

Grid 网格布局

Grid(scroller?: Scroller) 网格容器,配合 GridItem 子组件使用。

@Entry
@ComponentV2
struct GridExample {
@Local items: string[] = ['春眠不觉晓', '处处闻啼鸟', '夜来风雨声', '花落知多少', '白日依山尽', '黄河入海流'] // ① 数据源(6 句诗,2×3 格子)

build() {
Grid() {
ForEach(this.items, (item: string) => {
GridItem() {
Text(item)
.width('100%')
.height('100%')
.textAlign(TextAlign.Center)
}
})
}
.columnsTemplate('1fr 1fr 1fr') // 3列等宽
.rowsGap(8) // 行间距
.columnsGap(8) // 列间距
.width('100%')
.height(180)
}
}

💡 columnsTemplate('1fr 1fr 1fr') 表示 3 列等分。可混用固定与弹性:'100px 1fr 2fr'


layoutWeight 自适应权重

.layoutWeight(value: number | string) 子组件按权重瓜分父容器剩余空间。

Row({ space: 8 }) {
Text('固定80')
.width(80) // 固定宽度
.height(40)
Text('自适应')
.layoutWeight(1) // 占剩余空间
.height(40)
}
.width('100%')

🔑 layoutWeight 是响应式布局的核心,让内容自适应不同屏幕宽度。


Blank 弹性空白

Blank(min?: number | string) 自动填充剩余空间的空白组件,常用于左右两端布局。

Row() {
Text('Left')
Blank() // 弹性空白,占据中间空隙
Text('Right')
}
.width('100%')

position 相对定位

.position(value: Position) 相对父容器左上角定位。

Column() {
Text('明月松间照')
.position({ x: 0, y: 0 }) // 相对父容器定位
Text('清泉石上流')
.position({ x: 100, y: 0 })
}
.width(300)
.height(200)

样式设置

尺寸

.width(100)        // 固定宽度(vp)
.width('100%') // 父容器百分比
.width('50%')
.height(50)
.aspectRatio(2) // 宽高比(宽:高)

间距

.padding(16)                        // 四边内边距
.padding({ left: 10, right: 10 }) // 指定方向
.padding({ top: 8, bottom: 8, left: 16, right: 16 })
.margin(10) // 外边距
属性说明
padding内边距(内容与边框的距离)
margin外边距(组件与外部元素的距离)

背景与边框

.backgroundColor('#f5f5f5')       // 背景色
.borderRadius(8) // 圆角
.border({ width: 1, color: '#000000' }) // 边框

阴影

.shadow({
radius: 10, // 阴影模糊半径
color: '#1a000000', // 阴影颜色(带透明度)
offsetX: 0, // 水平偏移
offsetY: 2 // 垂直偏移
})

卡片式设计范式

本课程大量使用"卡片"设计,统一范式如下:

Column({ space: 12 }) {
Text('区块标题')
.fontSize(18)
.fontWeight(FontWeight.Bold)
// ... 区块内容
}
.width('100%')
.padding(16)
.backgroundColor(Color.White) // 白底卡片
.borderRadius(12) // 大圆角

页面背景统一使用 #f5f5f5,卡片用白色,形成清晰的视觉层级。


综合示例

对应 code/Chapter3Index.ets,页面包含 7 个区块:

  1. Text 文本:标题、强调色文本、辅助文本
  2. TextInput + Button:输入内容实时显示,按钮统计点击次数
  3. Toggle 开关:控制"接收通知"状态
  4. Row 布局:水平排列 A/B/C
  5. layoutWeight:固定列 + 自适应列对比
  6. Grid 网格:3 列 2 行共 6 格(诗词数组遍历)
  7. Stack 层叠:背景块 + 右下角标
@Entry
@ComponentV2
struct Index {
@Local inputText: string = ''
@Local isOn: boolean = false
@Local clickCount: number = 0

build() {
Scroll() {
Column({ space: 20 }) {
Text('Chapter 3: ArkUI Components & Layout')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Center)

// 1. Text 文本组件
Column({ space: 8 }) {
Text('Text 组件')
.fontSize(18)
.fontWeight(FontWeight.Bold)
Text('人生若只如初见')
.fontSize(20)
.fontColor('#007DFF')
.fontWeight(FontWeight.Bold)
Text('何事秋风悲画扇')
.fontSize(14)
.fontColor('#666666')
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)

// 2. 输入框 + 按钮
Column({ space: 12 }) {
Text('TextInput + Button')
.fontSize(18)
.fontWeight(FontWeight.Bold)

TextInput({ placeholder: '请输入内容', text: this.inputText })
.width('100%')
.height(44)
.onChange((v: string) => { this.inputText = v })

Button(`点击次数: ${this.clickCount}`)
.width('100%')
.height(44)
.type(ButtonType.Capsule)
.backgroundColor('#007DFF')
.onClick(() => { this.clickCount++ })

Text(`输入内容: ${this.inputText || '(空)'}`)
.fontSize(14)
.fontColor('#666666')
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)

// 3. Toggle 开关
Column({ space: 12 }) {
Text('Toggle 开关')
.fontSize(18)
.fontWeight(FontWeight.Bold)

Row() {
Text('接收通知')
.fontSize(16)
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.isOn })
.onChange((v: boolean) => { this.isOn = v })
}
.width('100%')

Text(this.isOn ? '通知已开启 ✅' : '通知已关闭')
.fontSize(14)
.fontColor(this.isOn ? '#00A86B' : '#999999')
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)

// 4. Row 水平布局
Column({ space: 12 }) {
Text('Row 水平布局')
.fontSize(18)
.fontWeight(FontWeight.Bold)

Row({ space: 10 }) {
Text('A')
.width(60).height(40)
.textAlign(TextAlign.Center)
.backgroundColor('#e8f4f8')
Text('B')
.width(60).height(40)
.textAlign(TextAlign.Center)
.backgroundColor('#e8f4e8')
Text('C')
.width(60).height(40)
.textAlign(TextAlign.Center)
.backgroundColor('#f4e8f4')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)

// 5. Column 垂直布局 + layoutWeight
Column({ space: 12 }) {
Text('layoutWeight 自适应')
.fontSize(18)
.fontWeight(FontWeight.Bold)

Row({ space: 8 }) {
Text('固定 80')
.width(80).height(40)
.fontSize(12)
.textAlign(TextAlign.Center)
.backgroundColor('#ffd6a5')
Text('自适应 1')
.layoutWeight(1).height(40)
.fontSize(12)
.textAlign(TextAlign.Center)
.backgroundColor('#9bf6ff')
Text('自适应 2')
.layoutWeight(2).height(40)
.fontSize(12)
.textAlign(TextAlign.Center)
.backgroundColor('#bdb2ff')
}
.width('100%')
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)

// 6. Grid 网格布局
Column({ space: 12 }) {
Text('Grid 网格布局')
.fontSize(18)
.fontWeight(FontWeight.Bold)

Grid() {
ForEach(['春眠不觉晓', '处处闻啼鸟', '夜来风雨声', '花落知多少', '白日依山尽', '黄河入海流'], (item: string) => {
GridItem() {
Text(item)
.width('100%')
.height('100%')
.textAlign(TextAlign.Center)
.fontSize(14)
.backgroundColor('#e0e0e0')
}
})
}
.columnsTemplate('1fr 1fr 1fr')
.rowsGap(8)
.columnsGap(8)
.width('100%')
.height(100)
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)

// 7. Stack 层叠布局
Column({ space: 12 }) {
Text('Stack 层叠布局')
.fontSize(18)
.fontWeight(FontWeight.Bold)

Stack({ alignContent: Alignment.BottomEnd }) {
Column()
.width('100%')
.height(80)
.backgroundColor('#ccdbfd')
Text('角标')
.fontSize(12)
.fontColor(Color.White)
.padding(6)
.backgroundColor('#ff6b6b')
.borderRadius(4)
}
.width('100%')
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
}
.width('100%')
.padding(16)
}
.width('100%')
.height('100%')
.backgroundColor('#f5f5f5')
}
}

运行效果(Previewer/模拟器):

  • 页面可滚动,自上而下展示 7 个组件/布局区块
  • 输入框输入内容实时回显
  • 点击按钮计数递增
  • 开关切换"通知已开启/关闭"
  • 网格 6 格、层叠角标清晰可见

运行效果截图:

Chapter3 综合示例总览

Text 文本组件

TextInput + Button 输入与按钮

Row 水平排列

Toggle 开关

Stack 层叠布局

Grid 网格布局

layoutWeight 弹性自适应


手势交互

手势是一系列基础事件不断上报积累后,达成一定特点时被识别成的交互结果,如点击、长按、拖动、滑动、旋转、捏合等。

ArkUI 系统组件会自动识别部分手势(如按钮的点击、列表的滑动),也可在组件上显式绑定自定义手势:

组件.gesture(
手势(参数)
.onAction((事件对象) => { /* 触发回调 */ })
)

常用手势

手势触发条件
点击 TapGesture单击(count 支持多次点击)
长按 LongPressGesture默认时长 500ms 触发
拖动 PanGesture默认滑动距离 5vp 触发
捏合 PinchGesture2~5 指,默认最小捏合距离 5vp
旋转 RotationGesture2~5 指,默认最小旋转 1° 触发
滑动 SwipeGesture最小识别触发速度 100vp/s

对应 code/Chapter3GesturePage.ets,一个组件绑定多手势(触发类用 GestureMode.Exclusive 互斥、连续类用 GestureMode.Parallel 并发):

@Entry
@Component
struct GesturePage {
@State offsetX: number = 0
@State offsetY: number = 0
@State angleValue: number = 0
@State scaleValue: number = 1

build() {
Column({ space: 24 }) {
// 触发类手势:单击 / 长按 / 滑动
Text('点我')
.width(160).height(160).textAlign(TextAlign.Center)
.backgroundColor('#6f9bff').borderRadius(80)
.gesture(
GestureGroup(
GestureMode.Exclusive,
TapGesture().onAction(() => { this.toast('单击手势') }),
LongPressGesture().onAction(() => { this.toast('长按手势') }),
SwipeGesture({ direction: SwipeDirection.All })
.onAction(() => { this.toast('滑动手势') })
)
)

// 连续类手势:拖动 / 旋转 / 捏合
Text('拖我')
.width(160).height(160).textAlign(TextAlign.Center)
.backgroundColor('#6fde8b').borderRadius(80)
.translate({ x: this.offsetX, y: this.offsetY })
.rotate({ angle: this.angleValue })
.scale({ x: this.scaleValue, y: this.scaleValue })
.gesture(
GestureGroup(
GestureMode.Parallel,
PanGesture()
.onActionUpdate((e: GestureEvent) => {
this.offsetX = e.offsetX
this.offsetY = e.offsetY
})
.onActionEnd(() => { this.offsetX = 0; this.offsetY = 0 }),
RotationGesture()
.onActionUpdate((e: GestureEvent) => { this.angleValue = e.angle }),
PinchGesture()
.onActionUpdate((e: GestureEvent) => { this.scaleValue = e.scale })
)
)
}
}

toast(msg: string) {
this.getUIContext().getPromptAction().showToast({ message: msg })
}
}

常用手势演示

组合手势

GestureGroup 将两种及以上手势组合为复合手势,支持三种识别模式:

模式说明
GestureMode.Sequence顺序识别:手势 1 成功后,手势 2 才开始
GestureMode.Parallel并发识别:多个手势同时识别
GestureMode.Exclusive互斥识别:一次只识别一个手势

仿微信语音输入案例(长按说话 + 左右滑动选择「取消 / 转文字」),对应 code/Chapter3CombinedGestures.ets

@Entry
@ComponentV2
struct CombinedGestures {
@Local isShow: Visibility = Visibility.Hidden
screenWidth: number = 0
@Local selectType: 'default' | 'cancel' | 'toText' = 'default'

build() {
Column({ space: 30 }) {
Blank().layoutWeight(1)

Row() {
Text('取消').SelectText(-30)
.backgroundColor(this.selectType === 'cancel' ? Color.Red : Color.Gray)
Text('转文字').SelectText(30)
.backgroundColor(this.selectType === 'toText' ? Color.Red : Color.Gray)
}
.backgroundColor('#bed9e2').width('100%')
.justifyContent(FlexAlign.SpaceAround).layoutWeight(1)
.visibility(this.isShow)

Button('长按语音输入')
.margin({ bottom: 40 })
.gesture(
GestureGroup(
GestureMode.Sequence, // 顺序:先长按,再拖动
LongPressGesture()
.onAction(() => { this.isShow = Visibility.Visible }),
PanGesture()
.onActionUpdate((e: GestureEvent) => {
if (e.fingerList[0].globalX < this.screenWidth / 2) {
this.selectType = 'cancel' // 左滑:取消
} else {
this.selectType = 'toText' // 右滑:转文字
}
})
.onActionEnd(() => { this.isShow = Visibility.Hidden })
)
)
}
.onAreaChange((_old, _new) => { this.screenWidth = _new.width as number })
}
}

@Extend(Text)
function SelectText(angleValue: number) {
.width(150).height(50).textAlign(TextAlign.Center)
.fontSize(30).rotate({ angle: angleValue }).borderRadius(25)
}

组合手势仿语音输入

💡 旋转、捏合等连续手势需要双指操作,模拟器无法模拟,需真机体验。


常见问题

Q:如何实现等宽布局? 使用 layoutWeightText('A').layoutWeight(1),多个子组件各占 1 份均分空间。

Q:如何隐藏组件? 使用 visibility 属性:Text('A').visibility(Visibility.Hidden)(占位隐藏)或 Visibility.None(不占位)。

Q:页面内容超出屏幕怎么办?Scroll() 包裹容器,实现滚动。

Q:按钮点击无响应? 检查是否被上层组件遮挡(Stack 层叠顺序),或组件 enabled(false) 被禁用。


本章小结

知识点说明
基础组件Text/Button/TextInput/Toggle/Image
容器组件Column/Row/Stack/Flex/Grid
layoutWeight弹性自适应核心
卡片范式白底 + 圆角 + 阴影
样式体系尺寸/间距/圆角/边框/阴影
手势系统Tap/LongPress/Pan/Rotation/Pinch/Swipe + GestureGroup

课后练习

  1. 使用 Row 和 Column 构建一个登录界面(账号、密码、登录按钮)
  2. 使用 Grid 实现九宫格宫格布局
  3. 设计一个个人信息卡片(头像 + 姓名 + 描述)
  4. layoutWeight 实现"左固定右自适应"布局
  5. TapGesture + LongPressGesture 实现"长按删除,单击选中"的列表项
  6. GestureGroup(GestureMode.Sequence) 实现"长按后拖动排序"
  7. 将练习内容整合进 Chapter3/Index.ets

参考资料

评论

加载中…
加载中...