HarmonyOS NEXT ArkTS 布局实战深度解析——从聊天界面到日历网格,全面掌握鸿蒙原生声明式布局


一、引言:鸿蒙原生开发的崭新篇章
1.1 HarmonyOS NEXT 的历史性跨越
2024年第四季度,华为正式发布了 HarmonyOS NEXT(鸿蒙星河版),这是首个完全移除 Android AOSP 代码、基于纯鸿蒙内核的操作系统版本。这一版本标志着鸿蒙真正成为与 iOS、Android 三分天下的独立移动生态。HarmonyOS NEXT 不再兼容 Android 应用,所有应用必须以鸿蒙原生方式开发,使用的语言是 ArkTS——基于 TypeScript 扩展的鸿蒙原生声明式编程语言。
对于开发者而言,这意味着一次全新的学习曲线,但同时也带来了前所未有的性能优化空间和系统级能力调用。HarmonyOS NEXT 6.1.1(API 24)在 2025 年底至 2026 年间逐步铺开,带来了更加成熟的 ArkUI 框架和更完善的工具链支持。
1.2 ArkTS 与 ArkUI:鸿蒙开发的两大基石
ArkTS(Ark TypeScript)是鸿蒙原生应用的主要开发语言。它在标准 TypeScript 的基础上,扩展了装饰器语法和声明式 UI 构建能力,形成了独特的开发范式。ArkUI 则是基于 ArkTS 的声明式 UI 框架,提供了一整套跨设备的 UI 组件和布局容器。
在 ArkUI 中,开发者通过组合系统预置的布局容器(如 Column、Row、Grid、Flex)来构建用户界面。这种声明式 UI 模式与 React Native、Flutter 等框架的思路一致——UI 是状态的函数,状态变化时 UI 自动更新。
1.3 本文的实践内容
本文将以一个真实的鸿蒙 NEXT 应用项目为例,深度剖析以下核心布局技术:
- 聊天界面布局:使用 Column + Scroll + Row 构建完整的消息列表与输入交互
- 弹性和自适应布局:layoutWeight 机制详解
- 日期网格布局:使用 Grid 组件实现 7 列月历
- 状态管理:@State 装饰器的工作原理
- 组件化:@Builder 装饰器的最佳实践
- 页面路由:Ability 与 pages 配置
项目 SDK 版本为 HarmonyOS NEXT 6.1.1(API 24),编译工具链为 Hvigor 6.1.1。
二、项目整体架构解析
2.1 项目结构总览
在开始深入布局细节之前,让我们先了解整个项目的文件组织结构。一个标准的 HarmonyOS NEXT 应用项目遵循以下层级:
MyApplication/ # 工程根目录
├── AppScope/ # 应用全局配置
│ ├── app.json5 # 应用级配置(bundleName、版本等)
│ └── resources/ # 全局资源
├── entry/ # entry 模块(可视为 Android 的 app module)
│ ├── src/main/
│ │ ├── ets/ # ArkTS 源文件
│ │ │ ├── entryability/ # UIAbility 入口
│ │ │ ├── pages/ # 页面组件
│ │ │ └── components/ # 公共组件
│ │ ├── resources/ # 模块级资源
│ │ └── module.json5 # 模块配置
│ ├── build-profile.json5 # 模块构建配置
│ └── oh-package.json5 # 模块包管理
├── build-profile.json5 # 工程构建配置(含 SDK 版本)
├── hvigor/ # 构建工具配置
│ └── hvigor-config.json5
├── oh-package.json5 # 工程级包管理
└── package.json # Node.js 依赖管理
其中 entry/src/main/ets/pages/ 目录存放所有的页面组件。每个 .ets 文件通常对应一个独立页面,通过 @Entry 装饰器标记为入口页面。
2.2 构建配置与 SDK 版本
在项目根目录的 build-profile.json5 中定义了关键的 SDK 版本信息:
{
"app": {
"products": [
{
"name": "default",
"targetSdkVersion": "6.1.1(24)",
"compatibleSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS"
}
]
}
}
这里 compatibleSdkVersion: "6.1.1(24)" 表示目标设备需运行 HarmonyOS NEXT 6.1.1 及以上版本。API 24 是鸿蒙 NEXT 的重要里程碑,引入了多项 ArkUI 增强特性。
在 entry/build-profile.json5 中可以看到应用的构建模式配置:
{
"apiType": "stageMode", // Stage 模型(鸿蒙推荐的应用模型)
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": false // 发布时可开启混淆
}
}
}
}
]
}
stageMode 是 HarmonyOS NEXT 的应用模型,替代了早期的 FA(Feature Ability)模型。
2.3 EntryAbility——应用的起点
EntryAbility 是应用的入口,它继承自 UIAbility 基类,负责应用的生命周期管理和页面加载。来看核心实现:
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(DOMAIN, 'EnglishApp', 'Ability onCreate');
}
onDestroy(): void {
hilog.info(DOMAIN, 'EnglishApp', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(DOMAIN, 'EnglishApp', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/ChatLayoutDemo', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'EnglishApp',
'Failed to load content: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'EnglishApp', 'Succeeded in loading content.');
});
}
onWindowStageDestroy(): void {
hilog.info(DOMAIN, 'EnglishApp', 'Ability onWindowStageDestroy');
}
onForeground(): void {
hilog.info(DOMAIN, 'EnglishApp', 'Ability onForeground');
}
onBackground(): void {
hilog.info(DOMAIN, 'EnglishApp', 'Ability onBackground');
}
}
关键要点:
- 生命周期方法:
onCreate→onWindowStageCreate→onForeground是标准的启动序列。onWindowStageCreate是加载页面的最佳时机。 - windowStage.loadContent:接受页面路径(对应
pages/目录下的文件)和回调函数。页面路径的命名规则是去掉.ets后缀,并相对于pages/目录。 - Kit 导入方式:HarmonyOS NEXT 统一使用
@kit.xxx形式的导入路径,替代了早期的@ohos.xxx方式。 - hilog 日志:使用
hilog.info(domain, tag, format, args)打印日志,domain是开发者自定义的十六进制标识。
2.4 页面路由配置
页面路由在 entry/src/main/resources/base/profile/main_pages.json 中配置:
{
"src": [
"pages/Index",
"pages/ChatLayoutDemo"
]
}
每一个 src 条目对应一个页面文件。EntryAbility 中 loadContent 加载的页面必须是这里注册过的。如果页面未在 main_pages.json 中注册,运行时会无法找到页面。
三、核心布局容器:Column、Row 与布局哲学
在深入具体的聊天界面之前,我们需要先理解 ArkUI 中最核心的三种布局容器:Column、Row 和 Stack。
3.1 Column(纵向布局容器)
Column 是 ArkUI 中最常用的布局组件之一,它按照从上到下的方向排列子组件。
Column() {
Text('第一个元素')
.fontSize(16)
Text('第二个元素')
.fontSize(16)
Text('第三个元素')
.fontSize(16)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F7FA')
Column 的核心属性包括:
alignItems(HorizontalAlign):水平方向的对齐方式。默认为HorizontalAlign.Center(居中对齐)。可选值还有HorizontalAlign.Start(左对齐)和HorizontalAlign.End(右对齐)。justifyContent(FlexAlign):垂直方向的分布方式。默认为FlexAlign.Start(从顶部开始分布)。可选值包括FlexAlign.Center(居中)、FlexAlign.End(底部)、FlexAlign.SpaceBetween(两端对齐)、FlexAlign.SpaceAround(均匀分布)等。width()和height():设置容器的宽高,可接受百分比字符串如'100%'或具体数值。
3.2 Row(横向布局容器)
Row 是 Column 的横向版本,从左到右排列子组件。
Row() {
Text('左侧')
Blank()
Text('右侧')
}
.width('100%')
.height(56)
.backgroundColor('#409EFF')
.alignItems(VerticalAlign.Center)
Row 的核心属性与 Column 类似但方向不同:
alignItems(VerticalAlign):子组件在垂直方向(交叉轴)的对齐方式。VerticalAlign.Center是常用值。justifyContent(FlexAlign):子组件在水平方向(主轴)的分布方式。
3.3 Blank——弹性占位利器
Blank() 是 ArkUI 中非常实用的弹性占位组件。它会自动占据父容器主轴方向的剩余空间。
Row() {
Text('左')
Blank() // 在左右文本之间压出最大间距
Text('右')
}
当有多处 Blank 时,剩余空间按照设定的 layoutWeight 比例分配。这在聊天界面中用于将消息气泡推到正确的一侧。
3.4 layoutWeight——弹性权重机制
layoutWeight 可能是 ArkUI 布局中最重要的属性之一。它定义了组件在主轴方向上占据剩余空间的比例权重。
Row() {
Text('固定文本') // 不设 layoutWeight,按内容宽度
TextInput()
.layoutWeight(1) // 占据剩余空间的 1 份
Button('发送')
.layoutWeight(0) // 不参与弹性分配,按按钮内容宽度
}
在这个例子中:
Text('固定文本')按文本内容实际宽度显示TextInput()通过layoutWeight(1)占据除左右两个组件之外的所有剩余宽度Button('发送')按按钮内容 + padding 的宽度显示
layoutWeight 的底层逻辑:当父容器为 Row 或 Column 时,子组件的 layoutWeight 属性会覆盖其原本的宽度测量结果。系统先测量所有未设 layoutWeight(或 weight 为 0)的子组件,然后将父容器的剩余空间按权重比例分配给设置了 layoutWeight 的子组件。
这一机制在实现自适应输入框、可滚动列表占满剩余空间等场景中至关重要。在聊天界面中,Scroll 组件通过 layoutWeight(1) 占据标题栏和输入栏之间的所有剩余高度。
四、实战一:聊天界面布局(Column + Scroll + Row)
聊天界面是移动应用中最常见也最具代表性的布局场景。它综合运用了 Column、Scroll、Row、静态初始数据和动态交互等多种技术。
4.1 布局架构设计
整个聊天界面的布局采用三层垂直结构:
Column (全屏容器, #EDEDED 灰色背景)
├── Row ① 顶部标题栏(固定高度 56vp)
├── Scroll ② 消息列表(layoutWeight:1 占满剩余空间)
│ └── Column
│ ├── Row [End] ③ 自己消息:左空白 → 白色气泡 → 绿色圆形头像
│ ├── Row [Start] ④ 对方消息:蓝色圆形头像 → 白色气泡 → 右空白
│ └── ... ForEach 循环渲染所有消息
└── Row ⑤ 底部输入区域(固定高度 56vp)
├── Text (😊 表情)
├── TextInput layoutWeight(1) 弹性宽度
├── Button (发送)
这种架构的核心思想是固定+弹性+固定:上下两端固定高度,中间区域用 layoutWeight(1) 弹性占满。
4.2 消息数据结构设计
在 ArkTS 中,使用接口(interface)和枚举(enum)定义数据类型:
/** 消息发送者角色 */
enum MessageRole {
SELF, // 自己发送的消息(气泡在右侧)
OTHER // 对方发送的消息(气泡在左侧)
}
/** 单条消息的数据结构 */
interface ChatMessage {
id: number; // 消息唯一 ID
role: MessageRole; // 发送者角色
content: string; // 消息文本内容
time: string; // 发送时间(如 "14:30")
avatar: string; // 头像文字(用字符代替图片)
name: string; // 发送者昵称
}
为什么将头像设计为字符串而不是图片资源?这是为了演示方便——通过 Text 组件配合圆形裁剪(borderRadius(20))可以快速创建圆形头像。在实际生产环境中,可以使用 Image 组件加载网络图片或本地资源。
4.3 @State 状态驱动
在 ArkTS 中,@State 装饰器标记的变量是响应式的——当变量值发生变化时,所有引用该变量的 UI 部分会自动重新渲染。
@Entry
@Component
struct ChatLayoutDemo {
@State messageList: ChatMessage[] = [ /* 初始消息数据 */ ];
@State inputText: string = '';
private nextId: number = 10;
private scroller: Scroller = new Scroller();
// ...
}
@State messageList:当追加新消息时(使用展开运算符[...this.messageList, newMsg]),UI 自动渲染新消息。这里的关键是必须创建新的数组引用才能触发 UI 更新——直接push不会触发重渲染。@State inputText:实时绑定输入框的文本内容。当onChange回调被触发时,inputText更新,从而同步更新发送按钮的启用/禁用状态。Scroller:是一个非响应式的 Scroll 控制器。它提供了scrollEdge(Edge.End)方法来控制 Scroll 滚动到底部。之所以不需要 @State,是因为它不直接参与 UI 渲染。
4.4 消息气泡构建器——@Builder 组件化
@Builder 装饰器允许将一段 UI 逻辑封装为可重用的构建函数。在聊天界面中,消息气泡的构建逻辑被封装在 buildMessageBubble 方法中:
@Builder
buildMessageBubble(msg: ChatMessage): void {
if (msg.role === MessageRole.SELF) {
// ======== 自己发送的消息:右对齐 ========
Row() {
Blank() // 左侧弹性占位,把内容推到右边
Column() {
// 白色气泡(右上角小圆角,模拟聊天气泡的尖角)
Column() {
Text(msg.content)
.fontSize(15)
.fontColor('#333333')
.lineHeight(22)
}
.backgroundColor('#FFFFFF')
.borderRadius({
topLeft: 12, topRight: 4, // 右上角小圆角 = 尖角效果
bottomLeft: 12, bottomRight: 12
})
.padding({ left: 14, right: 14, top: 10, bottom: 10 })
.margin({ bottom: 4 })
.constraintSize({ maxWidth: '70%' }) // 气泡最大宽度 70%
Text(msg.time)
.fontSize(11)
.fontColor('#B0B0B0')
.textAlign(TextAlign.End)
.width('100%')
}
.alignItems(FlexAlign.End) // 气泡右对齐
// 右侧绿色圆形头像
Text(msg.avatar)
.fontSize(14)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.width(40).height(40)
.textAlign(TextAlign.Center)
.backgroundColor('#87D068') // 绿色
.borderRadius(20)
.margin({ left: 8 })
}
.width('100%')
.alignItems(VerticalAlign.Top)
.margin({ bottom: 16 })
} else {
// ======== 对方发送的消息:左对齐 ========
// 结构与上方对称,注意 borderRadius 的左右反转
// ...
}
}
消息气泡的设计哲学:
- 边框圆角的不对称性:自己消息的
borderRadius为右上角 4 其他角 12,对方消息为左上角 4 其他角 12。这 4 比 12 小的圆角模拟了聊天气泡的"尖角"指向发送者一侧的效果。 constraintSize({ maxWidth: '70%' }):这是气泡布局的关键。如果不加宽度约束,长消息会撑满全屏宽度。70% 的限制确保气泡不会覆盖整个屏幕,让用户看到左侧(或右侧)有对方的头像或空白,明确消息的归属感。alignItems(VerticalAlign.Top):消息 Row 使用顶部对齐,这样当消息高度和头像高度不一致时,头像始终保持在顶部而非垂直居中。这是微信等主流聊天应用的一致做法。
4.5 发送与自动回复逻辑
发送消息和模拟对方回复展示了 ArkTS 中的异步逻辑和状态转换:
/** 发送消息 */
sendMessage(): void {
const text: string = this.inputText.trim();
if (text.length === 0) {
return; // 空白消息不发送
}
const now: Date = new Date();
const timeStr: string =
`${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
const newMsg: ChatMessage = {
id: this.nextId++,
role: MessageRole.SELF,
content: text,
time: timeStr,
avatar: '我',
name: '我'
};
// ★★★ 关键:创建新数组以触发 UI 更新 ★★★
this.messageList = [...this.messageList, newMsg];
this.inputText = ''; // 清空输入框
// 延迟滚动到底部(等待新消息渲染)
setTimeout((): void => {
this.scrollToBottom();
}, 100);
}
关于 [...this.messageList, newMsg]:ArkTS 的响应式系统基于引用比较来检测变化。数组的 push 操作不会改变数组的引用,因此不会触发 UI 更新。使用展开运算符创建新数组会产生新的引用,通知框架重新渲染。
setTimeout 延迟 100ms 再滚动到底部,是因为 UI 渲染是异步的。新消息被追加后,框架需要一段时间来完成渲染计算和布局测量,立即滚动可能无法到达正确位置。
对方自动回复的逻辑使用 setTimeout 模拟 1.2 秒的网络延迟:
// 发送后延迟 1.2 秒模拟对方回复
setTimeout((): void => {
this.autoReply();
}, 1200);
其中 autoReply 从预定义的回复列表中随机选取一条:
const replies: string[] = [
'收到收到 👍',
'继续聊!这个布局还可以配合 List 组件。',
'对,Column + Scroll 是最基础的聊天布局方案。'
];
const reply: string = replies[Math.floor(Math.random() * replies.length)];
4.6 Scroll 组件配置详解
Scroll 是 ArkUI 中用于实现内容滚动的容器组件:
Scroll(this.scroller) {
Column() {
ForEach(this.messageList, (msg: ChatMessage) => {
this.buildMessageBubble(msg)
})
Blank().height(12)
}
.width('100%')
.padding({ left: 12, right: 12, top: 8 })
}
.layoutWeight(1) // ★★★ 弹性占满剩余空间 ★★★
.width('100%')
.scrollBarState(BarState.Off) // 隐藏滚动条
.edgeEffect(EdgeEffect.None) // 禁用边缘回弹
.enableScrollInteraction(true) // 允许手势滑动
关键配置解析:
layoutWeight(1):这是 Scroll 占满标题栏与输入栏之间空间的关键。外层的 Column 包含三个子层:标题栏(固定高度)、Scroll(弹性)、输入栏(固定高度)。Scroll 通过layoutWeight(1)获得所有剩余空间。scrollBarState(BarState.Off):隐藏滚动条,使 UI 更简洁。聊天界面通常不需要可见的滚动条,用户通过手势滑动滚动的认知已经非常成熟。edgeEffect(EdgeEffect.None):当滚动到最顶部或最底部时禁用回弹效果。传统聊天界面中滚动到底部不会产生回弹是用户期望的行为。enableScrollInteraction(true):明确开启手势滚动交互。默认即为 true,但显式声明可以增加代码的可读性。
4.7 底部输入栏的交互设计
底部输入栏是消息交互的关键区域,需要同时处理输入状态和按钮状态的联动:
@Builder
buildInputBar(): void {
Row() {
// 表情按钮
Text('😊')
.fontSize(22)
.margin({ left: 8, right: 4 })
// ★★★ TextInput:layoutWeight(1) 弹性宽度 ★★★
TextInput({ text: this.inputText, placeholder: '输入消息...' })
.layoutWeight(1) // 弹性占满
.height(40)
.backgroundColor('#FFFFFF')
.borderRadius(20) // 圆角
.padding({ left: 16, right: 16 })
.fontSize(15)
.placeholderColor('#C0C0C0')
.onChange((value: string): void => {
this.inputText = value; // 同步状态
})
.onSubmit((): void => { // 回车发送
this.sendMessage();
setTimeout((): void => { this.autoReply(); }, 1200);
})
.margin({ left: 6, right: 6 })
// 发送按钮(无内容时禁用)
Button() {
Text('发送')
.fontSize(14)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Medium)
}
.type(ButtonType.Capsule)
.height(40)
.backgroundColor(
this.inputText.trim().length > 0 ? '#409EFF' : '#C0C0C0'
)
.enabled(this.inputText.trim().length > 0) // 零内容禁用
.margin({ right: 8 })
.onClick((): void => {
this.sendMessage();
setTimeout((): void => { this.autoReply(); }, 1200);
})
}
.width('100%')
.height(56)
.backgroundColor('#F5F5F5')
.alignItems(VerticalAlign.Center)
.padding({ left: 4, right: 4 })
}
输入框与按钮的联动逻辑:
- 用户在 TextInput 中输入文本,
onChange回调将内容同步到@State inputText inputText.trim().length > 0决定发送按钮的背景色(蓝色 vs 灰色)和启用状态- 点击「发送」或按回车(
onSubmit)触发sendMessage() - 发送后自动清空
inputText,按钮自动回到禁用状态 - 1.2 秒后自动回复触发,将对方消息追加到聊天列表
这里 backgroundColor 和 enabled 都直接引用 @State 变量,不需要任何额外的状态管理代码。声明式 UI 的优势在此体现—— UI 是状态的函数,状态变化时 UI 自动更新。
五、实战二:Grid 实现日期网格布局
如果说聊天界面展示了「线性布局」的能力,那么日历组件则展示了「网格布局」的魅力。Grid 组件是 ArkUI 中用于创建二维网格的核心组件。
5.1 日历布局的数据逻辑
在开始写 UI 代码之前,我们需要先理清日历背后的数据逻辑:
- 一个月可能有 28、29、30 或 31 天
- 每个月的第一天可能是周日到周六的任何一天(决定了月初偏移量)
- 日历网格固定为 7 列(周日到周六)
- 总格数必须是 7 的倍数(7×最小周数)
数据生成函数:
generateCalendarDays(): void {
const days: DayInfo[] = [];
const today: Date = new Date();
const year: number = this.currentYear;
const month: number = this.currentMonth;
// 本月第一天是星期几(0=周日, 1=周一, ..., 6=周六)
const firstDayOfMonth: Date = new Date(year, month - 1, 1);
const firstWeekday: number = firstDayOfMonth.getDay();
// 本月总天数
const daysInMonth: number = new Date(year, month, 0).getDate();
// 上月最后一天(用于填充前部空白)
const prevMonthDays: number = new Date(year, month - 1, 0).getDate();
// 总格数 = 向上取整到 7 的倍数
const totalCells: number = Math.ceil((firstWeekday + daysInMonth) / 7) * 7;
for (let i = 0; i < totalCells; i++) {
let day: number;
let isCurrentMonth: boolean;
if (i < firstWeekday) {
// 月初空白:显示上月末尾的日期
day = prevMonthDays - firstWeekday + i + 1;
isCurrentMonth = false;
} else if (i >= firstWeekday + daysInMonth) {
// 月末空白:显示下月开头的日期
day = i - firstWeekday - daysInMonth + 1;
isCurrentMonth = false;
} else {
// 本月日期
day = i - firstWeekday + 1;
isCurrentMonth = true;
}
const isToday = /* 判断是否是今天 */;
const isSelected = /* 判断是否被选中 */;
days.push({
day, isCurrentMonth, isToday, isSelected,
dateStr: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
});
}
this.calendarDays = days;
}
这里有几个重要的 JavaScript Date API 技巧:
new Date(year, month, 0).getDate():当 day 参数为 0 时,Date 构造函数会返回上个月的最后一天。所以new Date(2026, 7, 0)返回的是 2026 年 6 月 30 日(因为 month 是从 0 开始的,7 表示 7 月的第 0 天 = 6 月的最后一天)。这个技巧可以轻松获取任意月的天数。Math.ceil((firstWeekday + daysInMonth) / 7) * 7:这是格数对齐的关键。例如 2026 年 7 月:7 月 1 日是周三(firstWeekday = 3),本月 31 天,(3 + 31) / 7 = 4.857,向上取整为 5,乘以 7 得到 35 格。正好覆盖了 7 月的所有天数和前后空白。
5.2 Grid 组件构建日期网格
Grid 是 ArkUI 中最适合日历布局的组件:
@Builder
buildDateGrid(): void {
Grid() {
ForEach(this.calendarDays, (item: DayInfo) => {
GridItem() {
Column() {
Text(`${item.day}`)
.fontSize(16)
.fontWeight(item.isToday
? FontWeight.Bold
: (item.isCurrentMonth ? FontWeight.Medium : FontWeight.Regular))
.fontColor(item.isSelected
? '#FFFFFF'
: (item.isToday
? '#409EFF'
: (item.isCurrentMonth ? '#303133' : '#C0C4CC')))
.textAlign(TextAlign.Center)
}
.width('100%').height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor(item.isSelected
? '#409EFF'
: (item.isToday && !item.isSelected ? '#ECF5FF' : Color.Transparent))
.border(item.isToday && !item.isSelected
? { width: 1, color: '#409EFF', style: BorderStyle.Solid }
: { width: 0 })
.borderRadius(item.isSelected || item.isToday ? 20 : 8)
.onClick((): void => {
if (item.isCurrentMonth) {
this.onDateSelected(item.day);
}
})
}
.width('100%')
.aspectRatio(1.0) // ★★★ 保持正方形格子 ★★★
})
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr') // ★★★ 7列等宽 ★★★
.columnsGap(4)
.rowsGap(4)
.scrollBarState(BarState.Off)
.editMode(false)
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(8)
}
Grid 核心属性解析:
columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr'):定义 7 列,每列宽度占 1 份 fr(fraction unit,分数单位)。fr 是 Grid/Flex 布局中表示剩余空间分配的单位,7 个 1fr 意味着每列等宽各占 1/7。aspectRatio(1.0):设置在 GridItem 上,强制宽高比为 1:1(正方形)。这使得日期格子无论屏幕宽度如何都保持正方形,看起来很规整。columnsGap和rowsGap:设置了格子之间的间距,避免了格子间紧贴造成的视觉拥挤。rowsTemplate留空:不设置行模板时,Grid 会根据 items 数量自动计算行数(items / columns),这是日历场景的最佳选择。
状态到样式的映射:日期格子有 4 种视觉状态,通过三元运算符链组合实现:
| 状态 | 文字颜色 | 背景 | 边框 |
|---|---|---|---|
| 选中态 | 白色 | 蓝色实心 #409EFF | 无 |
| 今天(未选) | 蓝色 #409EFF | 浅蓝 #ECF5FF | 蓝色 1px |
| 本月普通 | 深灰 #303133 | 透明 | 无 |
| 非本月 | 浅灰 #C0C4CC | 透明 | 无 |
5.3 月份导航逻辑
月份的切换通过加减 currentMonth 实现,边界情况自动处理年份进位:
goToPrevMonth(): void {
this.currentMonth--;
if (this.currentMonth < 1) {
this.currentMonth = 12;
this.currentYear--;
}
this.selectedDate = -1; // 清除选中状态
this.generateCalendarDays();
}
goToNextMonth(): void {
this.currentMonth++;
if (this.currentMonth > 12) {
this.currentMonth = 1;
this.currentYear++;
}
this.selectedDate = -1;
this.generateCalendarDays();
}
当月份变化时,@State currentYear 和 @State currentMonth 的变化触发 generateCalendarDays() 重新计算日历数据,新的数据通过 @State calendarDays 驱动 Grid 重新渲染。整个过程全部自动完成。
六、布局核心要素深度剖析
6.1 FlexAlign 枚举全解析
FlexAlign 是 ArkUI 中最常用的布局枚举之一,用于控制子组件在主轴(Main Axis)上的排列方式:
// Row 的主轴是水平方向,Column 的主轴是垂直方向
enum FlexAlign {
Start, // 从主轴起点开始排列(默认值)
Center, // 在主轴上居中对齐
End, // 从主轴终点开始排列
SpaceBetween, // 两端对齐,子组件间间距均匀
SpaceAround, // 子组件两侧间距相等(相邻组件间距 = 2 × 边缘间距)
SpaceEvenly // 所有间距(含边缘)完全相等
}
结合实际场景理解:
FlexAlign.Start+ Row:子组件从左到右依次排列FlexAlign.End+ Row:子组件从右到左依次排列(聊天界面中自己消息使用 Blank() + 气泡可实现类似效果)FlexAlign.SpaceBetween+ Row:适用于工具栏、Tab 栏等需要两端对齐的场景
6.2 HorizontalAlign 与 VerticalAlign
HorizontalAlign:控制 Column 子组件在水平交叉轴的对齐方式。Start(左对齐)、Center(居中对齐,默认)、End(右对齐)。VerticalAlign:控制 Row 子组件在垂直交叉轴的对齐方式。Top(顶部对齐)、Center(垂直居中,默认)、Bottom(底部对齐)。
需要注意 HorizontalAlign 是一个枚举,而 Text 组件的 textAlign(TextAlign.Center) 使用的是另一个枚举 TextAlign,不要混淆。
6.3 constraintSize——约束规格
constraintSize 是 ArkUI 中非常实用的布局约束 API,它接受一个对象指定组件的最小和最大宽高:
.constraintSize({
minWidth: 100, // 最小宽度(vp)
maxWidth: '70%', // 最大宽度(百分比)
minHeight: 40, // 最小高度
maxHeight: 200 // 最大高度
})
在聊天气泡中的使用:constraintSize({ maxWidth: '70%' }) 确保气泡不会超过屏幕宽度的 70%,即使消息文本再长也只能换行显示,不会撑满整屏。
6.4 Stack 布局——层叠布局
虽然本文的示例没有使用 Stack,但作为 ArkUI 的四大基本布局之一(Column、Row、Flex、Stack),Stack 在某些场景中非常有用。Stack 允许子组件层叠排列(类似 CSS 的 absolute/fixed 定位):
Stack() {
Image({ src: $r('app.media.background') })
.width('100%').height('100%')
Column() {
Text('层叠在上层的文字')
.fontColor('#FFFFFF')
.fontSize(20)
}
.width('100%')
.alignItems(HorizontalAlign.Center)
.position({ top: 100 }) // 相对 Stack 定位
}
Stack 默认子组件居中层叠,通过 alignContent 和子组件的 position 属性可以精细控制每个子组件的位置。
七、状态管理与组件化
7.1 @State 装饰器的底层原理
@State 是 ArkUI 响应式系统的核心装饰器。被 @State 标记的变量具备以下特性:
- 响应式绑定:当变量值变化时,所有在
build()方法中引用了该变量的 UI 部分会自动进入待更新队列 - 不可变性要求:对于对象和数组类型,必须创建新实例才能触发 UI 更新。这就是为什么追加消息时使用
[...this.messageList, newMsg]而不是push - 浅比较检测:框架通过浅比较检查状态是否变化。对于基本类型(number、string、boolean),直接比较值;对于对象和数组,比较引用
来看一个状态变化的完整链路:
用户点击「发送」按钮
→ onClick 回调触发
→ sendMessage() 执行
→ this.messageList = [...this.messageList, newMsg] (新数组引用)
→ 框架检测到 messageList 引用变化
→ 标记需要更新的 UI 部分
→ 在下一帧执行 build() 增量更新
→ 新的 GridItem 被创建并插入到 Scroll 末尾
→ setTimeout 回调执行 scrollToBottom()
7.2 @Builder 装饰器——组件化利器
@Builder 允许将一段 UI 构建逻辑封装为函数,在 build() 中或其他 @Builder 方法中调用。使用 @Builder 有以下几个显著优势:
优势一:逻辑复用
@Builder
buildMessageBubble(msg: ChatMessage): void {
// 统一的消息气泡构建逻辑
}
// 在 build() 中调用
ForEach(this.messageList, (msg: ChatMessage) => {
this.buildMessageBubble(msg)
})
优势二:代码解耦
将大的 build() 方法拆分为多个有意义的子构建器:
buildTitleBar():标题栏buildMessageBubble():消息气泡buildInputBar():底部输入栏
每个构建器可以独立阅读和维护,build() 方法则像一份目录一样清晰。
优势三:条件构建@Builder 方法内部支持条件渲染:
@Builder
buildMessageBubble(msg: ChatMessage): void {
if (msg.role === MessageRole.SELF) {
// 自己消息布局
} else {
// 对方消息布局
}
}
使用 @Builder 的注意事项:
@Builder方法的返回值类型必须是void- 在
build()中调用时使用this.buildXxx()形式 - 方法内部的 UI 组件会自动捕获当前组件的上下文
7.3 条件渲染与 ForEach
ArkTS 支持三种条件渲染方式:
if/else if/else:用于根据条件渲染不同的 UI 分支ForEach:用于遍历数组渲染多个子组件LazyForEach:大数据列表优化,按需加载和销毁子组件
ForEach 的完整语法:
ForEach(
arr: Array<any>, // 数据源
itemGenerator: (item, index) => void, // 子组件生成器
keyGenerator?: (item, index) => string // 可选的 key 生成器(用于优化)
)
keyGenerator 为每个 item 生成唯一标识,帮助框架在数据更新时准确地识别哪些 item 被新增、删除或重排,避免不必要的全量重渲染。
八、鸿蒙 NEXT 项目工程配置详解
8.1 module.json5 模块配置
entry/src/main/module.json5 定义了模块的运行配置:
{
"module": {
"name": "entry",
"type": "entry", // entry = 可独立运行的主模块
"description": "$string:module_desc",
"mainElement": "EntryAbility", // 入口 Ability
"deviceTypes": ["phone"],
"pages": "$profile:main_pages", // 引用 pages 配置
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:module_desc",
"icon": "$media:startIcon",
"label": "$string:module_desc",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["action.system.home"]
}
]
}
]
}
}
关键字段说明:
type: "entry":表示该模块是可独立运行的应用入口模块,对应 Android 的 application modulepages: "$profile:main_pages":通过$profile引用资源文件resources/base/profile/main_pages.jsonabilities:注册了EntryAbility作为启动 Abilityskills:声明了应用可以响应的系统意图(打开桌面图标 =entity.system.home+action.system.home)
8.2 资源引用系统
ArkUI 使用 $r() 和 $media() 等语法引用资源:
$r('app.string.app_name'):引用字符串资源$r('app.color.start_window_background'):引用颜色资源$r('app.media.startIcon'):引用图片资源$profile:main_pages:在 JSON 配置中引用 profile 资源
资源文件存放在 resources/base/ 目录下,按类型分为:
resources/base/
├── element/ # 字符串、颜色、数字等元素资源(JSON 格式)
├── media/ # 图片等媒体资源
└── profile/ # 配置文件
8.3 应用签名与构建
HarmonyOS NEXT 的应用调试构建不需要配置签名(自动使用 debug 证书)。Release 构建时需要配置签名,在 build-profile.json5 的 signingConfigs 中配置。
发布流程:
- 申请鸿蒙开发者证书
- 在 AppGallery Connect 中创建应用并获取签名指纹
- 配置
signingConfigs - 执行
hvigorw assembleApp --mode release
九、性能优化与最佳实践
9.1 避免不必要的重渲染
当 @State 变量变化时,整个 build() 方法会重新执行以生成新的 UI 描述。这意味着如果 build() 方法中包含了不必要的计算操作,这些操作会无谓地重复执行。
优化技巧:
- 将复杂计算提前到 @Builder 外部:
// ❌ 不好的做法:在 build() 中计算
build() {
Column() {
Text(this.currentYear + '年' + this.currentMonth + '月')
}
}
// ✅ 好的做法:在单独方法中计算
getMonthTitle(): string {
return `${this.currentYear}年${this.currentMonth}月`;
}
build() {
Column() {
Text(this.getMonthTitle())
}
}
-
使用状态变量的局部化:只将需要触发 UI 更新的变量标记为
@State,不需要 UI 更新的内部状态使用普通私有变量。 -
keyGenerator 提升 ForEach 性能:为 ForEach 提供 key 生成函数,帮助框架在数据变化时准确定位变化项,避免全量重建。
9.2 文本组件的截断与折叠
在气泡消息中,如果文本过长需要截断处理,Text 组件提供了 maxLines 和 textOverflow:
Text('这是一段非常长的消息内容...')
.maxLines(2) // 最多显示 2 行
.textOverflow({
overflow: TextOverflow.Ellipsis // 超出部分显示省略号
})
9.3 深色模式适配
鸿蒙 NEXT 原生支持深色模式适配。在 build-profile.json5 中开启后,可以使用 @Styles 和 @Extend 装饰器定义主题相关的样式:
// 定义可复用的样式
@Styles function backgroundColor(): void {
.backgroundColor(Color.White)
}
// 条件判断主题
Column() {
// ...
}
.backgroundColor(this.isDarkMode ? '#1A1A2E' : '#F5F7FA')
实际项目中建议将主题色定义为资源变量,通过 $r('app.color.background') 引用,这样更换主题时只需修改资源文件。
9.4 合理的布局层级控制
嵌套层级过深会影响布局性能。一般建议:
- 布局嵌套不超过 5~6 层
- 优先使用
Blank()和layoutWeight而非多层嵌套实现对齐 - 使用
@Builder拆分可以降低认知复杂度,但不影响运行时层级深度
十、从实践到生产——布局方案选型建议
10.1 何时使用 Column vs List
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 消息列表 | List / Scroll + Column | List 支持懒加载和项复用,适合长列表 |
| 表单输入 | Column + Scroll | 表单通常项数固定,Column 更简单 |
| 少量卡片 | Column | 卡片数量少(<10),Column 足够 |
| 聊天界面 | Scroll + Column | 消息数量可能很多,但 List 的生命周期管理更复杂 |
10.2 何时使用 Grid vs Row + Flex
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 日历 / 相册 | Grid | 规则的网格布局,固定列数 |
| 菜单 / 工具栏 | Row / Flex | 项数不固定或需要自适应 |
| 九宫格 | Grid | 规则网格,固定行列比例 |
10.3 布局开发的常见陷阱
陷阱一:Scroll 不滚动
Scroll 必须设置确定的高度或 layoutWeight。如果 Scroll 的子组件总高度小于等于 Scroll 的高度,Scroll 不会滚动。确保 Scroll 的高度通过固定值或 layoutWeight 确定。
陷阱二:@State 数组不更新
这是一个常见错误——使用 push 或直接修改数组元素(如 arr[0] = newValue)不会触发 UI 更新。必须创建新数组引用:
// ❌ 不会触发 UI 更新
this.messageList.push(newMsg);
// ✅ 会触发 UI 更新
this.messageList = [...this.messageList, newMsg];
陷阱三:Grid 内容不显示
Grid 必须有确定的有效宽度和列模板。如果 columnsTemplate 格式错误或容器宽度为 0,Grid 的子组件不会显示。确保:
columnsTemplate格式正确(如'1fr 1fr 1fr')- 容器设置了
width('100%')或固定宽度
陷阱四:Scroll + Grid 同时使用时的滚动冲突
当 Grid 内部也有滚动内容时,如果同时设置 Scroll 和 Grid 的滚动,会产生滚动冲突。解决方法是控制 Grid 的 scrollBarState(BarState.Off) 并确保 Grid 的 rowsTemplate 不设定(让 Grid 自动计算行高,不开启内部滚动)。
十一、总结与展望
11.1 技术要点回顾
通过本文的实践,我们掌握了 HarmonyOS NEXT 中以下核心技术:
- 布局基础:Column(纵向)、Row(横向)、Grid(网格)三大布局容器的使用
- 滚动容器:Scroll + Scroller 控制器的配合使用
- 弹性布局:layoutWeight 的权重分配机制
- 状态管理:@State 装饰器的响应式原理和数组更新的引用要求
- 组件化:@Builder 装饰器实现 UI 逻辑复用
- 约束布局:constraintSize 限制组件最大最小尺寸
- 边框与背景:borderRadius 不对称设计实现气泡尖角效果
- 项目结构:Stage 模型下的 Ability、页面路由、资源配置
11.2 从示例到生产
本文的两个示例应用——聊天界面和日历组件——虽然结构完整,但距离生产级别的应用还有一定距离。从示例到生产需要考虑:
- 数据源:从内存静态数据迁移到网络 API 或本地数据库
- 状态管理:使用
@State结合@Observed、@ObjectLink管理复杂嵌套状态 - 性能优化:使用
LazyForEach处理超长列表 - 错误处理:添加网络异常、数据加载失败等异常状态
- 无障碍:为组件添加无障碍标签和交互描述
- 国际化:使用
$r('app.string.xxx')替代硬编码字符串
11.3 鸿蒙 NEXT 的发展前景
随着 HarmonyOS NEXT 6.1.1 的持续迭代,ArkUI 框架的能力在不断增强。从 API 24 开始,ArkUI 在以下方面有了显著提升:
- 动画系统:
animateTo和transition动画 API 更加完善 - 自定义绘制:
Canvas组件提供更灵活的 2D 绘图能力 - 跨端适配:同一套 UI 代码可同时运行在手机、平板、折叠屏和车机等设备
- AI 集成:系统级 AI 能力可通过安全 API 调用
对于移动端开发者而言,现在正是深入鸿蒙原生开发的最佳时机。ArkTS 的语言范式对前端开发者友好(基于 TypeScript),而其声明式 UI 的架构思想对有 React/Vue/Flutter 经验的开发者来说也能快速上手。
附录:完整代码文件一览
附录 A:ChatLayoutDemo.ets(聊天界面 — 535 行)
完整代码已在第四章展示。核心布局结构:
Column ← 全屏容器
├── @Builder buildTitleBar ← Row 顶部栏
├── Scroll + layoutWeight(1) ← 可滚动消息区域
│ └── Column + ForEach + @Builder buildMessageBubble
│ ├── Row(End) ← 自己消息:空白 + 气泡 + 头像
│ └── Row(Start) ← 对方消息:头像 + 气泡 + 空白
└── @Builder buildInputBar ← Row 底部输入栏:表情 + 输入框 + 发送
附录 B:项目构建命令
# 开发调试构建
hvigorw assembleApp --mode debug
# 发布构建
hvigorw assembleApp --mode release
# 运行 PreBuild 检查(快速验证项目配置)
hvigorw PreBuildApp --no-daemon
# 查看所有可用任务
hvigorw tasks
附录 C:常见 API 速查表
| 组件/属性 | 用途 | 示例 |
|---|---|---|
Column() |
纵向布局 | Column() { Text('A'); Text('B') } |
Row() |
横向布局 | Row() { Text('A'); Text('B') } |
Grid() |
网格布局 | Grid() { GridItem() { ... } }.columnsTemplate('1fr 1fr') |
Scroll() |
滚动容器 | Scroll() { Column() { ... } } |
TextInput() |
文本输入框 | TextInput({ text: '', placeholder: '输入...' }) |
Button() |
按钮 | Button() { Text('点击') }.onClick(() => {}) |
Blank() |
弹性占位 | Row() { Text('左'); Blank(); Text('右') } |
Divider() |
分隔线 | Divider().height(1).color('#E8E8E8') |
.layoutWeight(n) |
弹性权重 | .layoutWeight(1) 占据 1 份剩余空间 |
.constraintSize() |
尺寸约束 | .constraintSize({ maxWidth: '70%' }) |
.borderRadius() |
圆角 | .borderRadius({ topLeft: 12, topRight: 4, bottomLeft: 12, bottomRight: 12 }) |
ForEach() |
遍历渲染 | ForEach(arr, (item) => { Text(item) }) |
@State |
响应式状态 | @State count: number = 0 |
@Builder |
UI 构建函数 | @Builder buildXxx(): void { ... } |
更多推荐




所有评论(0)