请添加图片描述
请添加图片描述

一、引言:鸿蒙原生开发的崭新篇章

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');
  }
}

关键要点:

  • 生命周期方法onCreateonWindowStageCreateonForeground 是标准的启动序列。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 条目对应一个页面文件。EntryAbilityloadContent 加载的页面必须是这里注册过的。如果页面未在 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)      // 不参与弹性分配,按按钮内容宽度
}

在这个例子中:

  1. Text('固定文本') 按文本内容实际宽度显示
  2. TextInput() 通过 layoutWeight(1) 占据除左右两个组件之外的所有剩余宽度
  3. 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 })
}

输入框与按钮的联动逻辑

  1. 用户在 TextInput 中输入文本,onChange 回调将内容同步到 @State inputText
  2. inputText.trim().length > 0 决定发送按钮的背景色(蓝色 vs 灰色)和启用状态
  3. 点击「发送」或按回车(onSubmit)触发 sendMessage()
  4. 发送后自动清空 inputText,按钮自动回到禁用状态
  5. 1.2 秒后自动回复触发,将对方消息追加到聊天列表

这里 backgroundColorenabled 都直接引用 @State 变量,不需要任何额外的状态管理代码。声明式 UI 的优势在此体现—— UI 是状态的函数,状态变化时 UI 自动更新。


五、实战二:Grid 实现日期网格布局

如果说聊天界面展示了「线性布局」的能力,那么日历组件则展示了「网格布局」的魅力。Grid 组件是 ArkUI 中用于创建二维网格的核心组件。

5.1 日历布局的数据逻辑

在开始写 UI 代码之前,我们需要先理清日历背后的数据逻辑:

  1. 一个月可能有 28、29、30 或 31 天
  2. 每个月的第一天可能是周日到周六的任何一天(决定了月初偏移量)
  3. 日历网格固定为 7 列(周日到周六)
  4. 总格数必须是 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(正方形)。这使得日期格子无论屏幕宽度如何都保持正方形,看起来很规整。
  • columnsGaprowsGap:设置了格子之间的间距,避免了格子间紧贴造成的视觉拥挤。
  • 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 标记的变量具备以下特性:

  1. 响应式绑定:当变量值变化时,所有在 build() 方法中引用了该变量的 UI 部分会自动进入待更新队列
  2. 不可变性要求:对于对象和数组类型,必须创建新实例才能触发 UI 更新。这就是为什么追加消息时使用 [...this.messageList, newMsg] 而不是 push
  3. 浅比较检测:框架通过浅比较检查状态是否变化。对于基本类型(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 支持三种条件渲染方式:

  1. if / else if / else:用于根据条件渲染不同的 UI 分支
  2. ForEach:用于遍历数组渲染多个子组件
  3. 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 module
  • pages: "$profile:main_pages":通过 $profile 引用资源文件 resources/base/profile/main_pages.json
  • abilities:注册了 EntryAbility 作为启动 Ability
  • skills:声明了应用可以响应的系统意图(打开桌面图标 = 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.json5signingConfigs 中配置。

发布流程:

  1. 申请鸿蒙开发者证书
  2. 在 AppGallery Connect 中创建应用并获取签名指纹
  3. 配置 signingConfigs
  4. 执行 hvigorw assembleApp --mode release

九、性能优化与最佳实践

9.1 避免不必要的重渲染

@State 变量变化时,整个 build() 方法会重新执行以生成新的 UI 描述。这意味着如果 build() 方法中包含了不必要的计算操作,这些操作会无谓地重复执行。

优化技巧:

  1. 将复杂计算提前到 @Builder 外部
// ❌ 不好的做法:在 build() 中计算
build() {
  Column() {
    Text(this.currentYear + '年' + this.currentMonth + '月')
  }
}

// ✅ 好的做法:在单独方法中计算
getMonthTitle(): string {
  return `${this.currentYear}${this.currentMonth}`;
}
build() {
  Column() {
    Text(this.getMonthTitle())
  }
}
  1. 使用状态变量的局部化:只将需要触发 UI 更新的变量标记为 @State,不需要 UI 更新的内部状态使用普通私有变量。

  2. keyGenerator 提升 ForEach 性能:为 ForEach 提供 key 生成函数,帮助框架在数据变化时准确定位变化项,避免全量重建。

9.2 文本组件的截断与折叠

在气泡消息中,如果文本过长需要截断处理,Text 组件提供了 maxLinestextOverflow

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 中以下核心技术:

  1. 布局基础:Column(纵向)、Row(横向)、Grid(网格)三大布局容器的使用
  2. 滚动容器:Scroll + Scroller 控制器的配合使用
  3. 弹性布局:layoutWeight 的权重分配机制
  4. 状态管理:@State 装饰器的响应式原理和数组更新的引用要求
  5. 组件化:@Builder 装饰器实现 UI 逻辑复用
  6. 约束布局:constraintSize 限制组件最大最小尺寸
  7. 边框与背景:borderRadius 不对称设计实现气泡尖角效果
  8. 项目结构: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 在以下方面有了显著提升:

  • 动画系统animateTotransition 动画 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 { ... }

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐