HarmonyOS NEXT ArkTS 聊天界面布局实战——Column + Scroll + Row 构建完整聊天交互


一、引言:聊天界面——移动端最经典的交互范式
1.1 聊天界面的布局地位
在移动应用开发的历史上,聊天界面(Chat Interface)始终占据着最核心的交互范式地位。从早期的短信应用到微信、Telegram、WhatsApp 等即时通讯巨头,从客服系统到社交平台私信,消息列表 + 输入框的布局结构几乎没有改变过。
这种布局之所以成为经典,是因为它完美地满足了人类沟通的基本需求:
- 查看历史消息:上下滚动浏览对话记录
- 区分消息归属:自己发出的气泡在右、对方在左
- 实时输入发送:底部固定输入栏,随时键入内容
在鸿蒙 ArkUI 中,实现这种布局只需要三个核心组件——Column、Scroll、Row——以及它们之间的巧妙配合。
1.2 为什么是 Column + Scroll + Row
| 组件 | 角色 | 类比 |
|---|---|---|
| Column | 全屏纵向容器 | 类似 Android 的 LinearLayout(vertical) |
| Scroll | 可滚动消息区域 | 类似 iOS 的 UIScrollView |
| Row | 每条消息的横向布局 | 类似 CSS 的 flex-direction: row |
Column 提供全屏的基础架构,Scroll 处理消息列表的滚动行为,Row 控制每条消息中"头像 + 气泡"的对齐方式。三者组合,足以应对从简单聊天到复杂社交信息流的各种场景。
1.3 本文实践内容
本文以一个完整的聊天界面 Demo 为例,深入剖析以下核心技术:
- Column 纵向布局:标题栏 + 消息列表 + 输入栏三段式
- Scroll 滚动容器:Scroller 控制器 + 自动滚底
- Row 横向对齐:FlexAlign 控制消息气泡方向
- 消息气泡设计:borderRadius 不对称圆角模拟尖角
- @Builder 组件化:消息气泡统一构建器
- @State 状态管理:消息列表与输入框的响应式更新
- 交互流程:发送 + 自动回复 + 滚动到底部
项目基于 HarmonyOS NEXT 6.1.1(API 24),编译链 Hvigor 6.1.1,完整源码共 325 行,所有代码均已在 DevEco Studio 中构建验证通过。
1.4 ArkTS 声明式 UI 的核心思想
在深入聊天界面代码之前,需要先理解 ArkTS 声明式 UI 的几个核心概念:
1. 状态驱动(State-Driven)
传统命令式 UI 开发中,开发者需要手动调用 textView.setText("新内容") 或 button.setEnabled(true) 来更新 UI。而在 ArkTS 中,UI 是状态的函数:
UI = state(状态)
开发者只需要管理状态变量(@State),当状态变化时,框架自动计算出新的 UI 描述并与旧描述对比(diff),然后增量更新实际渲染树。
2. 声明式 DSL
ArkTS 的 UI 代码采用声明式领域特定语言(DSL):
Column() { // 创建 Column 容器
Text('Hello') // 创建 Text 子组件
.fontSize(20) // 设置属性(链式调用)
.fontColor('#333333')
}
这种嵌套结构直观反映了 UI 的层级关系:Column 包含 Text,Text 有字体大小和颜色属性。
3. 组件化思维
ArkTS 鼓励将 UI 拆分为独立的 @Builder 方法或 @Component 结构体。每个组件只负责自己的那一部分 UI,通过参数接收数据,通过 @State 管理内部状态。这种思维与 React 的函数式组件、Flutter 的 Widget 同出一源。
二、整体布局架构:三段式 Column
2.1 三层纵向结构
聊天界面的 Column 布局分为三个层次:
Column (全屏容器, #EDEDED 灰色聊天背景)
├── Row (height:56vp) ① 顶部标题栏 (蓝色 #409EFF)
│ ‹ 返回 [小]小李 ⋮ 更多
│ 在线
│
├── Scroll (layoutWeight:1) ② 消息列表 (弹性占满剩余空间)
│ └── Column
│ ├── Row (End) 自己消息:绿色头像 + 白色气泡(右)
│ ├── Row (Start) 对方消息:蓝色头像 + 白色气泡(左)
│ └── ... 循环渲染(ForEach)
│
└── Row (height:56vp) ③ 底部输入栏 (灰色 #F5F5F5)
😊 [ 输入消息... ] [发送]
采用"固定 + 弹性 + 固定"的经典三段式:
- 顶部标题栏:固定 56vp,显示联系人信息和操作按钮
- 消息列表:
layoutWeight(1)弹性占满屏幕中间区域 - 底部输入栏:固定 56vp,输入框 + 发送按钮
2.2 数据结构设计:消息的完整定义
在编写 UI 之前,需要先定义消息的数据结构。ArkTS 中使用 interface 和 enum 来声明类型:
/** 消息角色枚举 */
enum MsgRole {
SELF, // 自己发送,气泡在右侧
OTHER // 对方发送,气泡在左侧
}
/** 单条消息数据结构 */
interface MsgData {
id: number; // 消息唯一标识
role: MsgRole; // 发送者角色(决定气泡方向)
content: string; // 消息文本内容
time: string; // 发送时间(格式 "HH:mm")
avatar: string; // 头像文字(单个汉字或字母)
name: string; // 发送者昵称
}
为什么字段这样设计?
id使用 number 类型:作为列表渲染的 key,用于帮助框架在增删消息时准确定位变化的项role使用枚举而非 boolean:MsgRole.SELF/MsgRole.OTHER比isSelf: true/false可读性更好,且为将来扩展(如"系统消息")留有余地time使用 string 而非 Date:展示层只关心格式化的时间字符串,格式化逻辑在数据创建时完成avatar使用 string:用单个字符(“小”、“我”)渲染在圆形背景上模拟头像,避免了图片资源的依赖
2.3 初始消息数据的设计
初始的 8 条模拟消息不仅仅是占位数据,它们本身就是一个"引导教程"——对话内容解释了聊天界面自身的布局原理:
@State messages: MsgData[] = [
// 消息 1:对方先打招呼
{ id: 1, role: MsgRole.OTHER, content: '你好!欢迎体验鸿蒙聊天界面布局 🎉', ... },
// 消息 2:对方介绍使用的技术
{ id: 2, role: MsgRole.OTHER, content: '这个界面采用 Column + Scroll + Row 组合实现...', ... },
// 消息 3:自己提问
{ id: 3, role: MsgRole.SELF, content: '了解了!消息气泡是怎么做到左对齐和右对齐的呢?', ... },
// 消息 4:对方解答对齐方式
{ id: 4, role: MsgRole.OTHER, content: '消息气泡的核心是 Row 容器配合 FlexAlign 对齐...', ... },
// ...后续消息继续以问答形式解释布局
];
这种"自说明"的数据设计是不需要额外引导页的优雅方案——用户在浏览对话的过程中自然理解了界面的布局原理。
2.2 为什么 Scroll 要用 layoutWeight
在 Column 布局中,如果不使用 layoutWeight,子组件的高度分配需要精确计算:
// ❌ 不推荐:手动计算高度
Column() {
buildHeader() // 56vp
buildMessageList() // 剩余高度不易计算
buildInputBar() // 56vp
}
而使用 layoutWeight 后,框架自动完成高度分配:
// ✅ 推荐:Scroll 弹性占满
Column() {
this.buildHeader() // 固定高度
Scroll(this.scroller) { /* ... */ }
.layoutWeight(1) // 占满顶部和底部之间的所有空间
this.buildInputBar() // 固定高度
}
layoutWeight 的计算机制(以聊天界面为例):
Column 总高度 = 设备屏幕高度 = 800vp(假设)
步骤 1:测量固定高度的子组件
顶部栏 = 56vp
底部栏 = 56vp
步骤 2:计算剩余空间
剩余空间 = 800 - 56 - 56 = 688vp
步骤 3:按权重分配
Scroll.layoutWeight(1) = 688vp × (1/1) = 688vp
无论屏幕尺寸如何变化,Scroll 总能恰好占满标题栏和输入栏之间的空间。
三、消息气泡的布局奥秘
3.1 气泡对齐:Blank() 弹性占位
消息气泡的核心布局挑战是:如何让一些消息靠左、另一些靠右?
在 ArkUI 中,解决方案是使用 Blank() 组件作为弹性占位符:
自己消息(右对齐):
Row
├── Blank() ← 弹性占位,占据所有空闲宽度
├── Column 气泡内容
│ └── Text 消息文本
└── Text 头像(绿色)
Blank() 放在所有内容前面,Row 的 FlexAlign 默认为 Start,但 Blank() 会"撑满"前面的空间,将后面的内容推到右侧。
对方消息(左对齐):
Row
├── Text 头像(蓝色)
├── Column 气泡内容
│ ├── Text 昵称
│ └── Text 消息文本
└── Blank() ← 弹性占位,占据所有空闲宽度
Blank() 放在所有内容后面,将前面的内容"吸"在左侧。
3.2 气泡尖角:不对称 borderRadius
聊天气泡的一个关键视觉特征是"尖角"——指向发送者一侧的小尖角。在 ArkUI 中,这通过不对称的 borderRadius 实现:
// 自己消息:右上角小圆角 → 尖角指向右上方(自己方向)
.borderRadius({
topLeft: 12, // 大圆角
topRight: 4, // ★ 小圆角 = 尖角
bottomLeft: 12,
bottomRight: 12
})
// 对方消息:左上角小圆角 → 尖角指向左上方(对方方向)
.borderRadius({
topLeft: 4, // ★ 小圆角 = 尖角
topRight: 12,
bottomLeft: 12,
bottomRight: 12
})
圆角值 4 和 12 的视觉差异恰到好处——12 是标准的卡片圆角,4 则几乎接近直角,模拟了气泡"从边缘伸出来"的尖角效果。如果使用 0(直角)会显得过于生硬,使用更大的值则会失去尖角的视觉效果。
3.3 气泡宽度限制:constraintSize
.constraintSize({ maxWidth: '70%' })
constraintSize 设置了气泡的最大宽度为父容器宽度的 70%。为什么是 70%?因为:
- 留出头像空间:右侧(或左侧)有 40vp 的头像 + 8vp 间距
- 视觉平衡:气泡不应占据全部宽度,否则无法区分消息归属
- 长文本处理:超长内容会自动换行,不会撑出屏幕边界
这个 70% 的限制是聊天界面设计的最佳实践,被微信、Telegram 等主流应用广泛采用。
3.4 时间戳的放置
时间戳放在气泡下方,通过 margin({ top: 4 }) 与气泡保持 4vp 间距。时间戳的对齐方式与气泡一致(自己消息右对齐、对方消息左对齐),形成清晰的视觉归属。
四、Scroll 组件的配置详解
4.1 Scroller 控制器
private scroller: Scroller = new Scroller();
Scroll(this.scroller) {
// 消息内容
}
将 Scroller 实例传入 Scroll 组件,可以在外部代码中控制 Scroll 的滚动行为:
/** 滚动到底部 */
scrollToBottom(): void {
this.scroller.scrollEdge(Edge.End);
}
scrollEdge(Edge.End) 是滚动到 Scroll 末尾的最直接方法。与之对应的还有 scrollEdge(Edge.Start) 滚动到顶部。
4.2 延迟滚动:等待渲染完成
aboutToAppear(): void {
setTimeout((): void => {
this.scrollToBottom();
}, 200);
}
// 发送消息后
setTimeout(() => this.scrollToBottom(), 100);
为什么需要 setTimeout 延迟?因为 ArkUI 的 UI 渲染是异步的。当 @State messages 更新后,框架需要经历以下流程:
messages = [...messages, newMsg]
→ 框架检测到状态变化
→ 触发 build() 增量更新(异步调度)
→ 新消息的 UI 组件被创建 → 布局引擎测量并计算位置
→ setTimeout 回调执行 → scrollEdge(Edge.End) 滚动到最新位置
100ms 的延迟足以让框架完成一次渲染-布局周期。过短的延迟(如 0ms)可能在新消息尚未渲染完成时就执行滚动,导致滚动位置不正确。
4.3 隐藏滚动条
.scrollBarState(BarState.Off)
聊天界面中通常隐藏滚动条,用户通过手势直觉完成滚动操作。BarState.Off 彻底隐藏滚动条,BarState.On 始终显示,BarState.Auto 在滚动时自动浮现。对于聊天界面,BarState.Off 是最符合用户期望的选择。
4.4 Scroll 性能考量
当消息数量增多时,Scroll 组件内部包含的子组件数量会线性增长。ArkUI 的 Scroll 组件对子组件的渲染做了优化——只会渲染可见区域附近的组件。但对于包含大量复杂子组件的场景,以下优化策略可以帮助提升性能:
1. 限制消息列表长度
在 sendMessage() 方法中添加上限控制:
sendMessage(): void {
// ... 发送逻辑
this.messages = [...this.messages, newMsg];
// 上限控制:最多保留 150 条
if (this.messages.length > 150) {
this.messages = this.messages.slice(-100);
}
}
slice(-100) 保留最后 100 条消息,删除最早的消息。这既保证了聊天记录的连续性,又避免了无限增长导致的性能问题。
2. 使用 LazyForEach 替代 ForEach
对于超过 100 条的大列表,LazyForEach 只在需要时创建和渲染子组件:
// 需要实现一个 DataSource 类
class MessageDataSource extends BasicDataSource {
// ... 实现数据源方法
}
// 在 UI 中使用
LazyForEach(this.dataSource, (msg: MsgData) => {
this.buildBubble(msg)
}, (msg: MsgData) => msg.id.toString())
3. 避免在 buildBubble 中执行复杂计算
消息气泡的构建方法应该只做 UI 布局,不做数据处理。所有文本格式化、时间计算等操作都应该在数据层面完成,传入 MsgData 时已经是格式化好的字符串。
五、@State 状态管理与消息交互
5.1 消息列表的响应式更新
@State messages: MsgData[] = [
// 初始消息(8 条模拟对话)
];
@State 装饰器使 messages 数组成为响应式数据。当数组内容变化时,所有引用 messages 的 UI 部分自动重渲染。
关键规则:必须创建新的数组引用才能触发 UI 更新。
// ✅ 正确:展开运算符创建新数组
this.messages = [...this.messages, newMsg];
// ❌ 错误:push 不改变数组引用,不会触发 UI 更新
this.messages.push(newMsg);
这是 ArkTS 响应式系统的约定——框架通过引用比较(===)检测数组和对象的变化。展开运算符 [...arr, newItem] 创建了一个全新的数组实例,引用变化了,框架就知道需要更新 UI。
5.2 发送消息的完整流程
sendMessage(): void {
const text: string = this.inputText.trim();
if (text.length === 0) return; // ① 空内容不发送
const now: Date = new Date();
const time: string =
`${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
this.messages = [...this.messages, { // ② 追加新消息
id: this.nextId++,
role: MsgRole.SELF,
content: text,
time: time,
avatar: '我',
name: '我'
}];
this.inputText = ''; // ③ 清空输入框
setTimeout(() => this.scrollToBottom(), 100); // ④ 滚到底部
setTimeout(() => this.autoReply(), 1200); // ⑤ 1.2s 后自动回复
}
步骤拆解:
| 步骤 | 操作 | 目的 |
|---|---|---|
| ① | trim().length === 0 判断 |
防止发送纯空格消息 |
| ② | [...messages, newMsg] |
追加 + 触发 UI 更新 |
| ③ | inputText = '' |
清空输入框,按钮自动变灰 |
| ④ | scrollToBottom() × 100ms |
自动滚到最新消息 |
| ⑤ | autoReply() × 1200ms |
模拟对方回复 |
5.3 自动回复的逻辑
autoReply(): void {
const replies: string[] = [
'收到收到 👍',
'继续聊!布局还可以配合 List 组件实现。',
'对,Column + Scroll 是最基础的聊天布局方案。',
'你可以试着多发几条看看滚动效果 😄',
'这是个好问题!让我想想...',
'完全支持!这种布局方式非常灵活。'
];
const time: string = /* 当前时间 */;
this.messages = [...this.messages, {
id: this.nextId++,
role: MsgRole.OTHER,
content: replies[Math.floor(Math.random() * replies.length)], // 随机选
time: time,
avatar: '小',
name: '小李'
}];
setTimeout(() => this.scrollToBottom(), 100);
}
Math.floor(Math.random() * replies.length) 从 6 条预定义回复中随机选取一条。每次发送消息后 1.2 秒触发自动回复,模拟真实的聊天延迟感。
5.4 输入框与按钮的状态联动
@State inputText: string = '';
inputText 连接着 TextInput 和发送按钮:
TextInput({ text: this.inputText, placeholder: '输入消息...' })
.onChange((v: string) => { this.inputText = v; })
Button() { Text('发送') }
.backgroundColor(this.inputText.trim().length > 0 ? '#409EFF' : '#C0C0C0')
.enabled(this.inputText.trim().length > 0)
状态变化链路:
用户在输入框中键入文字
→ TextInput.onChange 回调被触发
→ this.inputText = 刚输入的内容
→ @State 检测到 inputText 变化
→ Button 的 backgroundColor 重新计算
→ 输入框有内容时:蓝色 (#409EFF) + 可点击
→ 输入框为空时:灰色 (#C0C0C0) + 禁用
整个过程中,开发者只需要管理 @State inputText 这一个变量,UI 的联动完全由框架自动处理。这就是声明式 UI 的核心优势。
六、@Builder 组件化:拆解 UI 的逻辑单元
6.1 三个 @Builder 方法
聊天界面将 UI 拆分为三个 @Builder 方法:
| @Builder | 职责 | 参数 | 行数 |
|---|---|---|---|
buildHeader() |
顶部标题栏 | 无 | 27 |
buildMessageList() |
消息列表容器 | 无 | 14 |
buildBubble(msg) |
单条消息气泡 | MsgData |
70 |
buildInputBar() |
底部输入栏 | 无 | 28 |
6.2 @Builder 的消息气泡逻辑
buildBubble(msg) 是聊天界面中最复杂的 @Builder 方法。它根据 msg.role 的值渲染不同的布局分支:
@Builder
buildBubble(msg: MsgData): void {
if (msg.role === MsgRole.SELF) {
// ====== 自己:右对齐 ======
Row() {
Blank() // 弹性占位
Column() { /* 气泡 + 时间 */ } // 内容
.alignItems(FlexAlign.End)
Text(msg.avatar) // 绿色头像
.backgroundColor('#87D068')
}
} else {
// ====== 对方:左对齐 ======
Row() {
Text(msg.avatar) // 蓝色头像
.backgroundColor('#5B8FF9')
Column() { /* 昵称 + 气泡 + 时间 */ } // 内容
.alignItems(FlexAlign.Start)
Blank() // 弹性占位
}
}
}
@Builder 设计的关键考量:
- 单一数据源:只接受
MsgData一个参数,所有渲染信息都来自数据对象 - 纯 UI 逻辑:不包含业务逻辑(发送、回复等),只有布局和样式
- 职责清晰:不负责遍历(遍历在父组件中由 ForEach 完成),只负责渲染单条消息
七、顶部标题栏与底部输入栏的细节
7.1 顶部标题栏设计
@Builder
buildHeader(): void {
Row() {
Text('‹') // 返回按钮
Text('小') // 圆形头像
.width(36).height(36).borderRadius(18)
.backgroundColor('#5B8FF9')
Column() {
Text('小李') // 联系人名称
Text('在线') // 在线状态
.fontSize(11).fontColor('#B0D0FF')
}
Blank() // 弹性占位
Text('⋮') // 更多操作
}
.height(56)
.backgroundColor('#409EFF')
}
几个关键的细节:
- 圆形头像:
width: 36, height: 36, borderRadius: 18(宽/2 = 18) - 双层文字:Column 容纳"小李"和"在线"两行文字,
alignItems(HorizontalAlign.Start)左对齐 - 更多按钮:
⋮(Unicode 字符 U+22EE)表示更多操作
7.2 底部输入栏的弹性布局
@Builder
buildInputBar(): void {
Row() {
Text('😊') // 表情按钮
.fontSize(22)
TextInput({ text: this.inputText, placeholder: '输入消息...' })
.layoutWeight(1) // ★★★ 弹性占满
.height(40).borderRadius(20)
.onChange((v) => { this.inputText = v; })
.onSubmit(() => { this.sendMessage(); })
Button() { Text('发送') }
.type(ButtonType.Capsule).height(40)
.backgroundColor(this.inputText.length > 0 ? '#409EFF' : '#C0C0C0')
.enabled(this.inputText.length > 0)
.onClick(() => { this.sendMessage(); })
}
.height(56)
}
layoutWeight(1) 的效果:
Row 总宽度 = 设备宽度
→ 左侧 😊 占用固定宽度(约 34vp)
→ 右侧 [发送] 按钮占用固定宽度(约 60vp)
→ 中间 TextInput 通过 layoutWeight(1) 获得:
剩余宽度 = 设备宽 - 34 - 60 - 左右间距(20)
TextInput.width = 剩余宽度 × 1 = 完整剩余宽度
TextInput 的 borderRadius(20) 让输入框呈现胶囊形状(高度 = 40,半径 = 20 恰好是半高),与整体圆润的 UI 风格统一。
八、项目工程配置
8.1 编译版本配置
// build-profile.json5
{
"app": {
"products": [
{
"name": "default",
"targetSdkVersion": "6.1.1(24)",
"compatibleSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS"
}
]
}
}
| 配置项 | 值 | 说明 |
|---|---|---|
targetSdkVersion |
6.1.1(24) | 目标 SDK 版本,API 24 |
compatibleSdkVersion |
6.1.1(24) | 向后兼容版本 |
runtimeOS |
HarmonyOS | 运行操作系统 |
caseSensitiveCheck |
true | 文件名大小写严格检查 |
8.2 页面路由配置
// main_pages.json
{
"src": [
"pages/ChatLayoutDemo"
]
}
8.3 EntryAbility 入口
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/ChatLayoutDemo', (err) => {
if (err.code) {
hilog.error(0x0000, 'App', 'Failed: %{public}s', JSON.stringify(err));
return;
}
hilog.info(0x0000, 'App', 'Succeeded.');
});
}
}
九、常见问题与解决方案
9.1 消息列表不滚动
现象:Scroll 包含多条消息但无法滚动。
原因:Scroll 没有有效的高度。如果 Scroll 的父容器高度未正确设置,Scroll 的子内容总高度可能小于 Scroll 自身高度。
解决:确保 Scroll 设置了 layoutWeight(1),且父 Column 设置了 height('100%')。
// 正确配置
Column()
.height('100%') // 父容器必须有明确高度
{
Scroll()
.layoutWeight(1) // Scroll 弹性占满
// ...
}
9.2 新消息不显示
现象:调用 sendMessage() 后消息数组长度增加,但 UI 上看不到新消息。
原因:使用了 push() 而不是展开运算符。
// ❌ 错误:不会触发 UI 更新
this.messages.push(newMsg);
// ✅ 正确:创建新数组引用
this.messages = [...this.messages, newMsg];
9.3 滚动不到最底部
现象:新消息出现后,底部总是有少量空白或最新消息被遮挡。
原因:滚动的时机过早,新消息的 UI 尚未渲染完成。
解决:使用 setTimeout 延迟 100ms 再滚动:
// ❌ 可能不生效
this.scroller.scrollEdge(Edge.End);
// ✅ 等待渲染完成
setTimeout(() => this.scroller.scrollEdge(Edge.End), 100);
9.4 输入框内容不更新
现象:在 TextInput 中打字,文本不显示或按钮状态不联动。
原因:@State inputText 没有被正确绑定。
解决:确保 TextInput 的 text 属性和 onChange 回调都正确关联:
TextInput({ text: this.inputText, placeholder: '输入消息...' })
.onChange((v: string) => { this.inputText = v; })
十、从 Demo 到生产:升级路径
10.1 功能升级对照
| 功能 | Demo 实现 | 生产级方案 |
|---|---|---|
| 消息数据 | 内存数组初始化的模拟数据 | WebSocket + 后端 API |
| 消息存储 | 无持久化 | 本地 SQLite / 云端同步 |
| 自动回复 | 本地预置回复 | 真实聊天对方/机器人 API |
| 图片消息 | 纯文本消息 | Image + 图片选择器 |
| 语音消息 | 无 | 录音组件 + 音频播放 |
| 表情面板 | 一个 😊 按钮 | 自定义表情面板 Grid |
| 消息状态 | 无 | 已发送/已读/发送失败 |
| 用户头像 | 字符 Text 圆形 | Image 网络图片 + 缓存 |
10.2 性能优化建议
消息数量增多时,需要考虑以下优化:
- LazyForEach 替代 ForEach:当消息超过 50 条时,LazyForEach 可以按需加载和销毁不可见的消息项
LazyForEach(this.messageDataSource, (msg: MsgData) => {
this.buildBubble(msg)
}, (msg: MsgData) => msg.id.toString())
- 消息数量限制:在
sendMessage()中限制消息数组最大长度,超过时删除最早的消息
if (this.messages.length > 200) {
this.messages = this.messages.slice(-150);
}
- 图片懒加载:使用
Image的load()方法控制加载时机,避免一次性加载大量图片
十一、布局 API 速查
11.1 核心组件
| 组件 | 用途 | 本项目使用位置 |
|---|---|---|
Column |
纵向排列 | 全屏容器、消息内容、昵称+状态 |
Row |
横向排列 | 标题栏、每条消息气泡、输入栏 |
Scroll |
可滚动容器 | 消息列表区域 |
TextInput |
文本输入 | 底部输入框 |
Button |
按钮 | 发送按钮 |
Blank |
弹性占位 | 消息气泡左右对齐 |
Text |
文本显示 | 所有文字内容 |
11.2 关键属性与 API
| 属性/API | 用途 | 示例 |
|---|---|---|
layoutWeight(n) |
弹性权重 | .layoutWeight(1) 占满剩余空间 |
borderRadius() |
圆角 | .borderRadius({ topLeft:12, topRight:4 }) |
constraintSize() |
尺寸约束 | .constraintSize({ maxWidth:'70%' }) |
scrollEdge(Edge.End) |
滚到底部 | this.scroller.scrollEdge(Edge.End) |
scrollBarState() |
滚动条状态 | .scrollBarState(BarState.Off) |
TextDecorationType |
文字装饰 | .decoration({ type: LineThrough }) |
FlexAlign |
对齐方式 | .alignItems(FlexAlign.End) |
11.3 构建命令
# 快速验证项目配置
hvigorw PreBuildApp --no-daemon
# Debug 构建
hvigorw assembleApp --mode debug --no-daemon
# Release 构建
hvigorw assembleApp --mode release --no-daemon
十二、总结
12.1 技术要点回顾
本文通过一个 325 行的完整聊天界面 Demo,系统讲解了 ArkUI 在聊天场景下的核心布局技术:
- 三段式 Column 架构:固定标题 + 弹性消息列表 + 固定输入栏
- 消息气泡对齐:Blank() + Row 实现左右切换
- 气泡尖角效果:borderRadius 不对称值模拟
- 宽度限制:constraintSize({ maxWidth: ‘70%’ })
- 响应式状态:@State 管理消息列表和输入框
- 数组更新:展开运算符
[...arr, newItem]触发 UI 刷新 - 自动滚底:Scroller + setTimeout 延迟滚动
- 组件化:@Builder 拆分标题栏、气泡、输入栏
这些技术的组合构成了 ArkUI 聊天界面的标准布局模式。无论是简单的消息应用,还是复杂的社交信息流,都可以基于这个架构进行扩展和定制。
12.2 ArkUI 布局的核心理念
通过本文的实战,可以总结出 ArkUI 布局的几个核心理念:
- 声明式优于命令式:开发者描述"UI 应该是什么样",而不是"如何一步步构建 UI"
- 状态驱动 UI:@State 变量变化 → UI 自动更新,无需手动操作 DOM
- 组合优于继承:Column/Row/Scroll 通过组合满足所有布局需求
- 组件化拆分:@Builder 将复杂 UI 拆分为可独立维护的逻辑单元
这些理念与 React、Flutter、SwiftUI 等现代 UI 框架一脉相承。掌握了 ArkUI,也就掌握了现代声明式 UI 开发的通用方法论。
更多推荐


所有评论(0)