暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

155. [HarmonyOS NEXT 实战案例十二 :List系列] 聊天消息列表 - 基础篇

原创 若城 2025-06-29
121

[HarmonyOS NEXT 实战案例十二 :List系列] 聊天消息列表 - 基础篇

项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star

效果演示

1. 概述

聊天应用是移动设备上最常用的应用类型之一,而聊天消息列表是这类应用的核心组件。一个优秀的聊天消息列表需要支持多种消息类型(文本、图片、语音、文件等),并且能够清晰地区分自己和对方的消息。本教程将介绍如何使用HarmonyOS NEXT的ArkUI框架实现一个功能完善的聊天消息列表。

1.1 应用场景

  • 即时通讯应用
  • 社交媒体的私信功能
  • 客服聊天系统
  • 团队协作工具的聊天功能

2. 核心组件介绍

在实现聊天消息列表时,我们将使用以下HarmonyOS NEXT的核心组件:

  • List & ListItem:用于创建垂直滚动的消息列表和列表项
  • Row & Column:用于布局排列
  • Image:用于显示头像和图片消息
  • Text:用于显示文本消息和时间
  • TextInput:用于输入消息
  • Scroller:用于控制列表滚动
  • LoadingProgress:用于显示消息发送状态

3. 数据模型设计

在开始实现UI之前,我们需要设计合适的数据模型来表示不同类型的消息和聊天对象。

3.1 聊天对象信息模型

interface ChatMessage { name: string, // 聊天对象名称 avatar: Resource, // 聊天对象头像 isOnline: boolean, // 是否在线 lastSeen?: string // 最后在线时间 }

3.2 文件信息模型

interface FileInfo { name: string, // 文件名 size: string, // 文件大小 type: string // 文件类型 }

3.3 位置信息模型

interface Location { name: string, // 位置名称 address: string // 位置地址 }

3.4 消息模型

interface Message { id: number; // 消息ID sender: 'me' | 'other'; // 发送者(自己或对方) type: 'text' | 'image' | 'voice' | 'file' | 'location'; // 消息类型 content: string; // 文本内容 media?: Resource; // 媒体资源(图片等) duration?: number; // 语音消息时长(秒) fileInfo?: FileInfo; // 文件信息 location?: Location; // 位置信息 time: string; // 发送时间 status: 'sending' | 'sent' | 'delivered' | 'read' | 'failed'; // 消息状态 }

4. 实现步骤

4.1 创建组件结构

首先,我们创建聊天消息列表组件的基本结构:

@Component export struct ChatMessageList { // 聊天对象信息 private chatInfo: ChatMessage = { name: '张三', avatar: $r('app.media.big22'), isOnline: true } // 消息数据 private messages: Message[] = [...] // 初始化数据 // 输入框内容 @State inputMessage: string = '' // 是否显示更多输入选项 @State showMoreOptions: boolean = false // 滚动控制器 private scroller: Scroller = new Scroller() // 发送消息 sendMessage() { // 实现发送消息逻辑 } // 获取当前时间 getCurrentTime(): string { // 实现获取当前时间逻辑 } // 滚动到底部 scrollToBottom() { // 实现滚动到底部逻辑 } build() { Column() { // 聊天头部 // 消息列表 // 输入区域 } .width('100%') .height('100%') } }

4.2 实现不同类型消息的构建器

为了处理不同类型的消息,我们使用@Builder装饰器创建专用的构建函数:

4.2.1 文本消息构建器

@Builder TextMessage(message: Message) { Text(message.content) .fontSize(16) .fontColor(message.sender === 'me' ? '#FFFFFF' : '#333333') .padding(12) .backgroundColor(message.sender === 'me' ? '#007AFF' : '#F0F0F0') .borderRadius(message.sender === 'me' ? 16 : 16) .borderRadius({ topLeft: message.sender === 'me' ? 16 : 4, topRight: message.sender === 'me' ? 4 : 16, bottomLeft: 16, bottomRight: 16 }) }

4.2.2 图片消息构建器

@Builder ImageMessage(message: Message) { Image(message.media) .width(200) .height(150) .objectFit(ImageFit.Cover) .borderRadius(8) }

4.2.3 语音消息构建器

@Builder VoiceMessage(message: Message) { Row() { if (message.sender === 'other') { Image($r('app.media.note_icon')) .width(24) .height(24) .margin({ right: 8 }) } Text(`${message.duration}''`) .fontSize(16) .fontColor(message.sender === 'me' ? '#FFFFFF' : '#333333') if (message.sender === 'me') { Image($r('app.media.01')) .width(24) .height(24) .margin({ left: 8 }) } } .width(message?.duration??1 * 8 + 60) // 根据语音长度动态调整宽度 .height(40) .padding({ left: 12, right: 12 }) .backgroundColor(message.sender === 'me' ? '#007AFF' : '#F0F0F0') .borderRadius(20) }

4.2.4 文件消息构建器

@Builder FileMessage(message: Message) { Row() { Image($r('app.media.big20')) .width(40) .height(40) Column() { Text(message?.fileInfo?.name||'') .fontSize(16) .fontColor(message.sender === 'me' ? '#FFFFFF' : '#333333') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row() { Text(message?.fileInfo?.type||'') .fontSize(14) .fontColor(message.sender === 'me' ? 'rgba(255, 255, 255, 0.7)' : '#666666') Text(message?.fileInfo?.size||'') .fontSize(14) .fontColor(message.sender === 'me' ? 'rgba(255, 255, 255, 0.7)' : '#666666') .margin({ left: 8 }) } .margin({ top: 4 }) } .alignItems(HorizontalAlign.Start) .margin({ left: 12 }) } .width(240) .padding(12) .backgroundColor(message.sender === 'me' ? '#007AFF' : '#F0F0F0') .borderRadius(8) }

4.2.5 位置消息构建器

@Builder LocationMessage(message: Message) { Column() { // 位置图片(实际应用中应该是地图) Image($r('app.media.map_icon2')) .width(240) .height(120) .objectFit(ImageFit.Cover) .borderRadius({ topLeft: 8, topRight: 8 }) // 位置信息 Column() { Text(message?.location?.name || '') .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor(message.sender === 'me' ? '#FFFFFF' : '#333333') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(message?.location?.address ||'') .fontSize(14) .fontColor(message.sender === 'me' ? 'rgba(255, 255, 255, 0.7)' : '#666666') .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) .margin({ top: 4 }) } .width('100%') .alignItems(HorizontalAlign.Start) .padding(12) } .width(240) .backgroundColor(message.sender === 'me' ? '#007AFF' : '#F0F0F0') .borderRadius(8) }

4.2.6 消息状态构建器

@Builder MessageStatus(status: string) { if (status === 'sending') { LoadingProgress() .width(16) .height(16) .color('#999999') } else if (status === 'sent') { Image($r('app.media.big18')) .width(16) .height(16) .fillColor('#999999') } else if (status === 'delivered') { Image($r('app.media.big17')) .width(16) .height(16) .fillColor('#999999') } else if (status === 'read') { Image($r('app.media.big16')) .width(16) .height(16) .fillColor('#4FC3F7') } else if (status === 'failed') { Image($r('app.media.big13')) .width(16) .height(16) .fillColor('#F44336') } }

4.3 实现聊天头部

// 聊天头部 Row() { Image($r('app.media.big11')) .width(24) .height(24) Image(this.chatInfo.avatar) .width(40) .height(40) .borderRadius(20) .margin({ left: 16 }) Column() { Text(this.chatInfo.name) .fontSize(18) .fontWeight(FontWeight.Medium) Row() { if (this.chatInfo.isOnline) { Text('在线') .fontSize(14) .fontColor('#4CAF50') } else if (this.chatInfo.lastSeen) { Text(`最后在线:${this.chatInfo.lastSeen}`) .fontSize(14) .fontColor('#999999') } } } .alignItems(HorizontalAlign.Start) .margin({ left: 12 }) Blank() Image($r('app.media.big12')) .width(24) .height(24) .margin({ right: 16 }) Image($r('app.media.big13')) .width(24) .height(24) .margin({ right: 16 }) Image($r('app.media.big14')) .width(24) .height(24) } .width('100%') .height(60) .padding({ left: 16, right: 16 }) .backgroundColor('#FFFFFF') .borderColor('#E5E5E5') .borderWidth({ bottom: 1 })

4.4 实现消息列表

// 消息列表 List({ scroller: this.scroller }) { ForEach(this.messages, (message:Message) => { ListItem() { Row() { // 对方头像(仅在对方消息时显示) if (message.sender === 'other') { Image(this.chatInfo.avatar) .width(40) .height(40) .borderRadius(20) .margin({ right: 8 }) } else { // 占位,保持对齐 Blank() .layoutWeight(1) } // 消息内容 Column() { // 根据消息类型显示不同内容 if (message.type === 'text') { this.TextMessage(message) } else if (message.type === 'image') { this.ImageMessage(message) } else if (message.type === 'voice') { this.VoiceMessage(message) } else if (message.type === 'file') { this.FileMessage(message) } else if (message.type === 'location') { this.LocationMessage(message) } // 消息时间和状态 Row() { Text(message.time) .fontSize(12) .fontColor('#999999') if (message.sender === 'me') { this.MessageStatus(message.status) } } .width('100%') .margin({ top: 4 }) .justifyContent(message.sender === 'me' ? FlexAlign.End : FlexAlign.Start) } .alignItems(message.sender === 'me' ? HorizontalAlign.End : HorizontalAlign.Start) .layoutWeight(message.sender === 'me' ? 5 : 5) // 自己头像(仅在自己消息时显示) if (message.sender === 'me') { Image($r('app.media.comment')) .width(40) .height(40) .borderRadius(20) .margin({ left: 8 }) } else { // 占位,保持对齐 Blank() .layoutWeight(1) } } .width('100%') .padding({ left: 16, right: 16, top: 8, bottom: 8 }) .alignItems(VerticalAlign.Top) .justifyContent(message.sender === 'me' ? FlexAlign.End : FlexAlign.Start) } }) } .width('100%') .layoutWeight(1) .backgroundColor('#F5F5F5') .onScrollIndex((start: number, end: number) => { // 可以在这里处理滚动事件 })

4.5 实现输入区域

// 输入区域 Column() { // 更多选项区域(可折叠) if (this.showMoreOptions) { Grid() { // 相册 GridItem() { Column() { Image($r('app.media.big17')) .width(48) .height(48) .borderRadius(24) .backgroundColor('#EEEEEE') .padding(12) Text('相册') .fontSize(12) .margin({ top: 8 }) } .alignItems(HorizontalAlign.Center) } // 拍照 GridItem() { Column() { Image($r('app.media.big12')) .width(48) .height(48) .borderRadius(24) .backgroundColor('#EEEEEE') .padding(12) Text('拍照') .fontSize(12) .margin({ top: 8 }) } .alignItems(HorizontalAlign.Center) } // 文件 GridItem() { Column() { Image($r('app.media.Facebook_icon_03')) .width(48) .height(48) .borderRadius(24) .backgroundColor('#EEEEEE') .padding(12) Text('文件') .fontSize(12) .margin({ top: 8 }) } .alignItems(HorizontalAlign.Center) } // 位置 GridItem() { Column() { Image($r('app.media.dcc_health_icon')) .width(48) .height(48) .borderRadius(24) .backgroundColor('#EEEEEE') .padding(12) Text('位置') .fontSize(12) .margin({ top: 8 }) } .alignItems(HorizontalAlign.Center) } // 联系人 GridItem() { Column() { Image($r('app.media.note_icon')) .width(48) .height(48) .borderRadius(24) .backgroundColor('#EEEEEE') .padding(12) Text('联系人') .fontSize(12) .margin({ top: 8 }) } .alignItems(HorizontalAlign.Center) } // 收藏 GridItem() { Column() { Image($r('app.media.music_icon')) .width(48) .height(48) .borderRadius(24) .backgroundColor('#EEEEEE') .padding(12) Text('收藏') .fontSize(12) .margin({ top: 8 }) } .alignItems(HorizontalAlign.Center) } } .columnsTemplate('1fr 1fr 1fr 1fr') .rowsTemplate('1fr') .columnsGap(16) .rowsGap(16) .padding(16) .height(100) .backgroundColor('#FFFFFF') } // 输入框和按钮 Row() { // 更多选项按钮 Image($r('app.media.mobile_calculator_ap')) .width(32) .height(32) .onClick(() => { this.showMoreOptions = !this.showMoreOptions }) // 输入框 TextInput({ text: this.inputMessage }) .width('100') .height(40) .backgroundColor('#F5F5F5') .borderRadius(20) .padding({ left: 16, right: 16 }) .margin({ left: 8, right: 8 }) .onChange((value: string) => { this.inputMessage = value }) // 语音按钮 Image($r('app.media.active_weather_icon')) .width(32) .height(32) .margin({ right: 8 }) // 发送按钮(仅在有输入内容时显示) if (this.inputMessage.trim() !== '') { Button() { Image($r('app.media.02')) .width(24) .height(24) .fillColor('#FFFFFF') } .width(32) .height(32) .borderRadius(16) .backgroundColor('#007AFF') .onClick(() => { this.sendMessage() }) } else { // 表情按钮 Image($r('app.media.02')) .width(32) .height(32) } } .width('80%') .padding({ left: 16, right: 16, top: 8, bottom: 8 }) .backgroundColor('#FFFFFF') .borderColor('#E5E5E5') .borderWidth({ top: 1 }) }

4.6 实现发送消息功能

// 发送消息 sendMessage() { if (this.inputMessage.trim() === '') { return } // 添加新消息 this.messages.push({ id: this.messages.length + 1, sender: 'me', type: 'text', content: this.inputMessage, time: this.getCurrentTime(), status: 'sending' }) // 清空输入框 this.inputMessage = '' // 模拟发送过程 setTimeout(() => { // 更新最后一条消息状态为已发送 this.messages[this.messages.length - 1].status = 'sent' // 滚动到底部 this.scrollToBottom() }, 500) // 模拟对方回复 setTimeout(() => { this.messages.push({ id: this.messages.length + 1, sender: 'other', type: 'text', content: '好的,我知道了!', time: this.getCurrentTime(), status: 'read' }) // 滚动到底部 this.scrollToBottom() }, 2000) } // 获取当前时间 getCurrentTime(): string { const now = new Date() const hours = now.getHours().toString().padStart(2, '0') const minutes = now.getMinutes().toString().padStart(2, '0') return `${hours}:${minutes}` } // 滚动到底部 scrollToBottom() { this.scroller.scrollToIndex(this.messages.length - 1) } aboutToAppear() { // 组件出现时滚动到底部 setTimeout(() => { this.scrollToBottom() }, 100) }

5. 技术要点分析

5.1 消息气泡的样式处理

在聊天应用中,区分自己和对方的消息是非常重要的。我们通过不同的颜色和气泡形状来实现这一点:

.backgroundColor(message.sender === 'me' ? '#007AFF' : '#F0F0F0') .fontColor(message.sender === 'me' ? '#FFFFFF' : '#333333') .borderRadius({ topLeft: message.sender === 'me' ? 16 : 4, topRight: message.sender === 'me' ? 4 : 16, bottomLeft: 16, bottomRight: 16 })

这种设计使得自己的消息显示为蓝色背景、白色文字,右侧有一个尖角;而对方的消息显示为灰色背景、黑色文字,左侧有一个尖角。

5.2 消息列表的布局处理

聊天消息列表的布局需要考虑自己和对方消息的对齐方式:

.alignItems(message.sender === 'me' ? HorizontalAlign.End : HorizontalAlign.Start) .justifyContent(message.sender === 'me' ? FlexAlign.End : FlexAlign.Start)

自己的消息靠右对齐,对方的消息靠左对齐,这样可以清晰地区分消息的发送者。

5.3 消息状态的显示

在聊天应用中,显示消息的发送状态是很重要的功能。我们通过不同的图标来表示不同的状态:

  • 发送中:显示加载动画
  • 已发送:显示单勾图标
  • 已送达:显示双勾图标
  • 已读:显示蓝色双勾图标
  • 发送失败:显示红色感叹号图标

这样用户可以清楚地知道自己的消息是否成功发送和对方是否已读。

5.4 滚动控制

在聊天应用中,新消息发送后需要自动滚动到底部,这可以通过Scroller控制器实现:

private scroller: Scroller = new Scroller() scrollToBottom() { this.scroller.scrollToIndex(this.messages.length - 1) }

在发送新消息和接收新消息后,调用scrollToBottom()方法,确保用户总是能看到最新的消息。

6. 常见问题与解决方案

6.1 消息列表性能问题

问题:当消息数量很多时,可能导致列表加载缓慢和滚动卡顿。

解决方案

  • 使用LazyForEach替代ForEach,实现虚拟列表
  • 设置合理的cachedCount值,控制缓存的列表项数量
  • 考虑分页加载历史消息,而不是一次性加载所有消息

6.2 输入框高度自适应

问题:当输入内容较多时,单行输入框可能不够用。

解决方案

  • 使用TextArea替代TextInput,支持多行输入
  • 实现输入框高度自适应,根据内容自动调整高度
  • 设置最大高度限制,避免输入框占据过多空间

6.3 消息发送失败处理

问题:网络不稳定时,消息可能发送失败。

解决方案

  • 实现消息发送失败的状态显示
  • 提供重新发送的功能
  • 实现本地消息缓存,确保用户数据不会丢失

7. 总结与扩展

在本教程中,我们学习了如何使用HarmonyOS NEXT的ArkUI框架实现一个功能完善的聊天消息列表,包括多种消息类型的展示、消息状态的显示、输入区域的实现等。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论