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

2025-06-30 22:29:15
103次阅读
0个评论

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

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

效果演示

image.png

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框架实现一个功能完善的聊天消息列表,包括多种消息类型的展示、消息状态的显示、输入区域的实现等。

收藏00

登录 后评论。没有帐号? 注册 一个。

全栈若城

  • 0回答
  • 4粉丝
  • 0关注