156.[HarmonyOS NEXT 实战案例十二 :List系列] 聊天消息列表 - 进阶篇
2025-06-30 22:38:51
103次阅读
0个评论
[HarmonyOS NEXT 实战案例十二 :List系列] 聊天消息列表 - 进阶篇
项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star
效果演示
1. 概述
在基础篇中,我们学习了如何实现一个基本的聊天消息列表,包括多种消息类型的展示和基本交互功能。在本进阶篇中,我们将探索更多高级特性,使聊天应用更加完善和专业,提供更好的用户体验。
1.1 进阶功能概览
- 消息加载与分页
- 高级交互效果
- 消息搜索与过滤
- 多媒体消息增强
- 消息加密与安全
- 离线消息处理
- 通知与提醒
2. 消息加载与分页
在实际应用中,聊天记录可能非常多,一次性加载所有消息会导致性能问题。我们需要实现分页加载和无限滚动功能。
2.1 实现历史消息分页加载
// 在ChatMessageList组件中添加以下属性和方法
@State isLoadingHistory: boolean = false // 是否正在加载历史消息
private pageSize: number = 20 // 每页加载的消息数量
private currentPage: number = 1 // 当前页码
private hasMoreHistory: boolean = true // 是否还有更多历史消息
// 加载历史消息
loadHistoryMessages() {
if (this.isLoadingHistory || !this.hasMoreHistory) {
return
}
this.isLoadingHistory = true
// 模拟网络请求加载历史消息
setTimeout(() => {
// 假设这是从服务器获取的历史消息
const historyMessages: Message[] = this.generateHistoryMessages(this.pageSize)
// 如果返回的消息数量小于页大小,说明没有更多历史消息了
if (historyMessages.length < this.pageSize) {
this.hasMoreHistory = false
}
// 将历史消息添加到消息列表的前面
this.messages = [...historyMessages, ...this.messages]
this.currentPage++
this.isLoadingHistory = false
// 保持滚动位置,避免跳到底部
this.maintainScrollPosition(historyMessages.length)
}, 1000)
}
// 生成模拟的历史消息数据
private generateHistoryMessages(count: number): Message[] {
const messages: Message[] = []
const startId = this.messages.length > 0 ?
this.messages[0].id - count : count
for (let i = 0; i < count; i++) {
const id = startId + i
// 避免生成负数ID
if (id <= 0) continue
messages.push({
id: id,
sender: i % 2 === 0 ? 'me' : 'other',
type: i % 5 === 0 ? 'image' :
i % 7 === 0 ? 'voice' :
i % 11 === 0 ? 'file' :
i % 13 === 0 ? 'location' : 'text',
content: `历史消息 #${id}`,
media: i % 5 === 0 ? $r('app.media.big22') : undefined,
duration: i % 7 === 0 ? Math.floor(Math.random() * 60) + 1 : undefined,
fileInfo: i % 11 === 0 ? {
name: `文件${id}.pdf`,
size: `${Math.floor(Math.random() * 10) + 1}MB`,
type: 'PDF'
} : undefined,
location: i % 13 === 0 ? {
name: '鸿蒙科技园',
address: '中国广东省深圳市龙岗区'
} : undefined,
time: this.getRandomPastTime(),
status: 'read'
})
}
return messages.sort((a, b) => a.id - b.id)
}
// 获取随机的过去时间
private getRandomPastTime(): string {
const now = new Date()
const randomMinutes = Math.floor(Math.random() * 60 * 24) // 最多过去24小时
now.setMinutes(now.getMinutes() - randomMinutes)
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
return `${hours}:${minutes}`
}
// 维持滚动位置
private maintainScrollPosition(newItemsCount: number) {
// 计算新的滚动位置,保持用户当前查看的消息在视图中的位置
this.scroller.scrollToIndex(newItemsCount)
}
2.2 在List组件中添加下拉加载功能
List({ scroller: this.scroller }) {
// 加载中提示
if (this.isLoadingHistory) {
ListItem() {
Row() {
LoadingProgress()
.width(24)
.height(24)
.color('#999999')
Text('正在加载历史消息...')
.fontSize(14)
.fontColor('#999999')
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding(16)
}
}
// 没有更多历史消息提示
if (!this.hasMoreHistory && !this.isLoadingHistory) {
ListItem() {
Text('没有更多消息了')
.fontSize(14)
.fontColor('#999999')
.padding(16)
}
.width('100%')
.justifyContent(FlexAlign.Center)
}
// 消息列表
ForEach(this.messages, (message: Message) => {
// 消息列表项(与基础篇相同)
})
}
.width('100%')
.layoutWeight(1)
.backgroundColor('#F5F5F5')
.onReachStart(() => {
// 当滚动到顶部时,加载更多历史消息
this.loadHistoryMessages()
})
3. 高级交互效果
3.1 消息长按菜单
实现长按消息弹出操作菜单的功能,包括复制、转发、删除、撤回等选项。
@State selectedMessageId: number = -1 // 当前选中的消息ID
@State showContextMenu: boolean = false // 是否显示上下文菜单
@State menuPosition: { x: number, y: number } = { x: 0, y: 0 } // 菜单位置
// 在消息项中添加长按手势
.gesture(
LongPressGesture()
.onAction((event: GestureEvent) => {
this.selectedMessageId = message.id
this.menuPosition = { x: event.x, y: event.y }
this.showContextMenu = true
})
)
// 上下文菜单组件
@Builder
ContextMenu() {
if (this.showContextMenu && this.selectedMessageId !== -1) {
Stack() {
// 半透明背景,点击关闭菜单
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(0, 0, 0, 0.4)')
.onClick(() => {
this.showContextMenu = false
this.selectedMessageId = -1
})
// 菜单内容
Column() {
// 获取选中的消息
const selectedMessage = this.messages.find(msg => msg.id === this.selectedMessageId)
// 复制选项(仅文本消息可用)
if (selectedMessage?.type === 'text') {
this.MenuOption('复制', $r('app.media.big15'), () => {
// 实现复制功能
this.copyToClipboard(selectedMessage.content)
this.showContextMenu = false
})
}
// 转发选项
this.MenuOption('转发', $r('app.media.big16'), () => {
// 实现转发功能
this.showContextMenu = false
})
// 收藏选项
this.MenuOption('收藏', $r('app.media.big17'), () => {
// 实现收藏功能
this.showContextMenu = false
})
// 删除选项
this.MenuOption('删除', $r('app.media.big13'), () => {
// 实现删除功能
this.deleteMessage(this.selectedMessageId)
this.showContextMenu = false
})
// 撤回选项(仅自己发送且时间在2分钟内的消息可撤回)
if (selectedMessage?.sender === 'me' && this.canRecallMessage(selectedMessage)) {
this.MenuOption('撤回', $r('app.media.big14'), () => {
// 实现撤回功能
this.recallMessage(this.selectedMessageId)
this.showContextMenu = false
})
}
}
.width(180)
.padding(8)
.backgroundColor('#FFFFFF')
.borderRadius(8)
.position({
x: this.calculateMenuX(),
y: this.calculateMenuY()
})
.shadow({
radius: 8,
color: 'rgba(0, 0, 0, 0.2)',
offsetX: 0,
offsetY: 2
})
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
}
}
// 菜单选项构建器
@Builder
MenuOption(text: string, icon: Resource, action: () => void) {
Row() {
Image(icon)
.width(20)
.height(20)
.margin({ right: 12 })
Text(text)
.fontSize(16)
.fontColor('#333333')
}
.width('100%')
.height(48)
.padding({ left: 16, right: 16 })
.onClick(() => {
action()
})
.hover({
backgroundColor: '#F5F5F5'
})
}
// 计算菜单X坐标
private calculateMenuX(): number {
// 确保菜单不超出屏幕边界
const screenWidth = px2vp(window.getWindowWidth())
const menuWidth = 180
let x = this.menuPosition.x
if (x + menuWidth > screenWidth) {
x = screenWidth - menuWidth - 16
}
if (x < 16) {
x = 16
}
return x
}
// 计算菜单Y坐标
private calculateMenuY(): number {
// 确保菜单不超出屏幕边界
const screenHeight = px2vp(window.getWindowHeight())
const menuHeight = 240 // 估计高度
let y = this.menuPosition.y
if (y + menuHeight > screenHeight) {
y = screenHeight - menuHeight - 16
}
if (y < 16) {
y = 16
}
return y
}
// 判断消息是否可以撤回(2分钟内)
private canRecallMessage(message: Message): boolean {
// 获取消息时间
const messageParts = message.time.split(':')
const messageHours = parseInt(messageParts[0])
const messageMinutes = parseInt(messageParts[1])
// 获取当前时间
const now = new Date()
const currentHours = now.getHours()
const currentMinutes = now.getMinutes()
// 计算时间差(分钟)
const totalMessageMinutes = messageHours * 60 + messageMinutes
const totalCurrentMinutes = currentHours * 60 + currentMinutes
const diffMinutes = totalCurrentMinutes - totalMessageMinutes
// 如果是当天的消息且在2分钟内,则可以撤回
return diffMinutes >= 0 && diffMinutes <= 2
}
// 复制文本到剪贴板
private copyToClipboard(text: string) {
// 实际应用中需要使用系统API实现剪贴板功能
console.info(`已复制文本: ${text}`)
// 显示提示
this.showToast('已复制')
}
// 删除消息
private deleteMessage(messageId: number) {
this.messages = this.messages.filter(msg => msg.id !== messageId)
// 显示提示
this.showToast('已删除')
}
// 撤回消息
private recallMessage(messageId: number) {
// 查找消息索引
const index = this.messages.findIndex(msg => msg.id === messageId)
if (index !== -1) {
// 将消息替换为撤回提示
this.messages[index] = {
...this.messages[index],
type: 'text',
content: '你撤回了一条消息',
isRecalled: true
}
}
// 显示提示
this.showToast('已撤回')
}
// 显示Toast提示
private showToast(message: string) {
// 实际应用中需要使用系统API实现Toast功能
console.info(`Toast: ${message}`)
}
3.2 消息回复功能
实现引用回复功能,可以引用之前的消息进行回复。
@State replyToMessage: Message | null = null // 要回复的消息
// 在上下文菜单中添加回复选项
this.MenuOption('回复', $r('app.media.big18'), () => {
this.replyToMessage = this.messages.find(msg => msg.id === this.selectedMessageId) || null
this.showContextMenu = false
})
// 在输入区域上方显示回复预览
Column() {
// 回复预览
if (this.replyToMessage) {
Row() {
Column() {
Text('回复')
.fontSize(12)
.fontColor('#999999')
Row() {
Text(this.replyToMessage.sender === 'me' ? '我' : this.chatInfo.name)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
Text(this.getMessagePreview(this.replyToMessage))
.fontSize(14)
.fontColor('#666666')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ left: 8 })
}
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Image($r('app.media.big13'))
.width(20)
.height(20)
.onClick(() => {
this.replyToMessage = null
})
}
.width('100%')
.padding(12)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.margin({ bottom: 8 })
}
// 输入框和按钮(与基础篇相同)
}
// 获取消息预览文本
private getMessagePreview(message: Message): string {
switch (message.type) {
case 'text':
return message.content
case 'image':
return '[图片]'
case 'voice':
return `[语音 ${message.duration}秒]`
case 'file':
return `[文件 ${message?.fileInfo?.name || ''}]`
case 'location':
return `[位置 ${message?.location?.name || ''}]`
default:
return ''
}
}
// 修改发送消息方法,支持回复功能
sendMessage() {
if (this.inputMessage.trim() === '') {
return
}
// 添加新消息
const newMessage: Message = {
id: this.messages.length + 1,
sender: 'me',
type: 'text',
content: this.inputMessage,
time: this.getCurrentTime(),
status: 'sending',
replyTo: this.replyToMessage ? {
id: this.replyToMessage.id,
sender: this.replyToMessage.sender,
content: this.getMessagePreview(this.replyToMessage)
} : undefined
}
this.messages.push(newMessage)
// 清空输入框和回复信息
this.inputMessage = ''
this.replyToMessage = null
// 模拟发送过程(与基础篇相同)
}
// 在消息显示中添加回复引用
if (message.replyTo) {
Column() {
Row() {
Divider()
.vertical(true)
.height(36)
.width(2)
.color('#007AFF')
.margin({ right: 8 })
Column() {
Text(message.replyTo.sender === 'me' ? '我' : this.chatInfo.name)
.fontSize(12)
.fontWeight(FontWeight.Medium)
.fontColor(message.sender === 'me' ? 'rgba(255, 255, 255, 0.7)' : '#666666')
Text(message.replyTo.content)
.fontSize(12)
.fontColor(message.sender === 'me' ? 'rgba(255, 255, 255, 0.7)' : '#666666')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.margin({ bottom: 8 })
// 消息内容(根据类型显示不同内容)
}
.alignItems(message.sender === 'me' ? HorizontalAlign.End : HorizontalAlign.Start)
}
4. 消息搜索与过滤
4.1 实现消息搜索功能
@State isSearchMode: boolean = false // 是否处于搜索模式
@State searchKeyword: string = '' // 搜索关键词
@State searchResults: Message[] = [] // 搜索结果
@State currentSearchIndex: number = -1 // 当前搜索结果索引
// 在聊天头部添加搜索按钮
Image($r('app.media.big19'))
.width(24)
.height(24)
.margin({ right: 16 })
.onClick(() => {
this.isSearchMode = true
})
// 搜索模式下的头部
@Builder
SearchHeader() {
Row() {
Image($r('app.media.big11'))
.width(24)
.height(24)
.onClick(() => {
this.isSearchMode = false
this.searchKeyword = ''
this.searchResults = []
this.currentSearchIndex = -1
})
TextInput({ text: this.searchKeyword })
.width('100%')
.height(40)
.backgroundColor('#F5F5F5')
.borderRadius(20)
.padding({ left: 16, right: 16 })
.margin({ left: 16, right: 16 })
.placeholder('搜索聊天记录')
.onChange((value: string) => {
this.searchKeyword = value
this.searchMessages()
})
if (this.searchResults.length > 0) {
Text(`${this.currentSearchIndex + 1}/${this.searchResults.length}`)
.fontSize(14)
.fontColor('#666666')
.margin({ right: 8 })
}
Image($r('app.media.big20'))
.width(24)
.height(24)
.margin({ right: 8 })
.onClick(() => {
this.navigateToPreviousResult()
})
Image($r('app.media.big21'))
.width(24)
.height(24)
.onClick(() => {
this.navigateToNextResult()
})
}
.width('100%')
.height(60)
.padding({ left: 16, right: 16 })
.backgroundColor('#FFFFFF')
.borderColor('#E5E5E5')
.borderWidth({ bottom: 1 })
}
// 搜索消息
private searchMessages() {
if (!this.searchKeyword.trim()) {
this.searchResults = []
this.currentSearchIndex = -1
return
}
// 搜索文本消息
this.searchResults = this.messages.filter(msg => {
if (msg.type === 'text') {
return msg.content.toLowerCase().includes(this.searchKeyword.toLowerCase())
} else if (msg.type === 'file' && msg.fileInfo) {
return msg.fileInfo.name.toLowerCase().includes(this.searchKeyword.toLowerCase())
} else if (msg.type === 'location' && msg.location) {
return msg.location.name.toLowerCase().includes(this.searchKeyword.toLowerCase()) ||
msg.location.address.toLowerCase().includes(this.searchKeyword.toLowerCase())
}
return false
})
// 重置当前索引并滚动到第一个结果
this.currentSearchIndex = this.searchResults.length > 0 ? 0 : -1
this.scrollToSearchResult()
}
// 导航到上一个搜索结果
private navigateToPreviousResult() {
if (this.searchResults.length === 0) return
this.currentSearchIndex--
if (this.currentSearchIndex < 0) {
this.currentSearchIndex = this.searchResults.length - 1
}
this.scrollToSearchResult()
}
// 导航到下一个搜索结果
private navigateToNextResult() {
if (this.searchResults.length === 0) return
this.currentSearchIndex++
if (this.currentSearchIndex >= this.searchResults.length) {
this.currentSearchIndex = 0
}
this.scrollToSearchResult()
}
// 滚动到当前搜索结果
private scrollToSearchResult() {
if (this.currentSearchIndex === -1 || this.searchResults.length === 0) return
const currentResult = this.searchResults[this.currentSearchIndex]
const messageIndex = this.messages.findIndex(msg => msg.id === currentResult.id)
if (messageIndex !== -1) {
this.scroller.scrollToIndex(messageIndex)
// 高亮显示搜索结果
this.highlightSearchResult(currentResult.id)
}
}
// 高亮显示搜索结果
@State highlightedMessageId: number = -1
private highlightSearchResult(messageId: number) {
this.highlightedMessageId = messageId
// 2秒后取消高亮
setTimeout(() => {
if (this.highlightedMessageId === messageId) {
this.highlightedMessageId = -1
}
}, 2000)
}
// 在消息列表项中添加高亮效果
.backgroundColor(message.id === this.highlightedMessageId ? 'rgba(0, 122, 255, 0.1)' : 'transparent')
.animation({
duration: 300,
curve: Curve.EaseInOut
})
4.2 实现消息过滤功能
@State filterType: string = 'all' // 过滤类型:all, text, media, file, location
// 在搜索模式下添加过滤选项
Row() {
this.FilterOption('全部', 'all')
this.FilterOption('文本', 'text')
this.FilterOption('媒体', 'media')
this.FilterOption('文件', 'file')
this.FilterOption('位置', 'location')
}
.width('100%')
.height(44)
.backgroundColor('#FFFFFF')
.padding({ left: 16, right: 16 })
.margin({ top: 8 })
// 过滤选项构建器
@Builder
FilterOption(text: string, type: string) {
Text(text)
.fontSize(14)
.fontColor(this.filterType === type ? '#007AFF' : '#666666')
.fontWeight(this.filterType === type ? FontWeight.Medium : FontWeight.Normal)
.backgroundColor(this.filterType === type ? 'rgba(0, 122, 255, 0.1)' : 'transparent')
.borderRadius(16)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.margin({ right: 8 })
.onClick(() => {
this.filterType = type
this.applyFilter()
})
}
// 应用过滤
private applyFilter() {
if (this.filterType === 'all') {
// 不过滤,使用原始搜索结果
this.searchMessages()
return
}
// 根据类型过滤搜索结果
const filteredResults = this.searchResults.filter(msg => {
if (this.filterType === 'text') {
return msg.type === 'text'
} else if (this.filterType === 'media') {
return msg.type === 'image' || msg.type === 'voice'
} else if (this.filterType === 'file') {
return msg.type === 'file'
} else if (this.filterType === 'location') {
return msg.type === 'location'
}
return true
})
this.searchResults = filteredResults
this.currentSearchIndex = this.searchResults.length > 0 ? 0 : -1
this.scrollToSearchResult()
}
5. 多媒体消息增强
5.1 图片消息预览与缩放
@State showImagePreview: boolean = false // 是否显示图片预览
@State previewImage: Resource | null = null // 预览的图片
// 在图片消息上添加点击事件
.onClick(() => {
this.previewImage = message.media
this.showImagePreview = true
})
// 图片预览组件
@Builder
ImagePreviewDialog() {
if (this.showImagePreview && this.previewImage) {
Stack() {
// 半透明背景
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(0, 0, 0, 0.9)')
.onClick(() => {
this.showImagePreview = false
this.previewImage = null
})
// 图片预览
Column() {
Image(this.previewImage)
.objectFit(ImageFit.Contain)
.width('100%')
.height('80%')
.gesture(
PinchGesture()
.onActionStart((event: GestureEvent) => {
// 处理缩放开始
})
.onActionUpdate((event: GestureEvent) => {
// 处理缩放更新
})
.onActionEnd(() => {
// 处理缩放结束
})
)
// 操作按钮
Row() {
// 保存按钮
Column() {
Image($r('app.media.big22'))
.width(24)
.height(24)
.fillColor('#FFFFFF')
Text('保存')
.fontSize(12)
.fontColor('#FFFFFF')
.margin({ top: 8 })
}
.onClick(() => {
// 实现保存图片功能
this.showToast('图片已保存')
})
// 转发按钮
Column() {
Image($r('app.media.big16'))
.width(24)
.height(24)
.fillColor('#FFFFFF')
Text('转发')
.fontSize(12)
.fontColor('#FFFFFF')
.margin({ top: 8 })
}
.margin({ left: 48, right: 48 })
// 编辑按钮
Column() {
Image($r('app.media.big23'))
.width(24)
.height(24)
.fillColor('#FFFFFF')
Text('编辑')
.fontSize(12)
.fontColor('#FFFFFF')
.margin({ top: 8 })
}
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 24 })
}
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
}
}
5.2 语音消息播放控制
@State playingVoiceId: number = -1 // 当前播放的语音消息ID
@State playbackProgress: number = 0 // 播放进度(0-100)
@State isPlaying: boolean = false // 是否正在播放
// 在语音消息上添加点击事件
.onClick(() => {
if (this.playingVoiceId === message.id) {
// 如果点击的是当前正在播放的消息,则暂停/继续播放
this.togglePlayback()
} else {
// 如果点击的是其他消息,则开始播放新消息
this.startPlayback(message.id)
}
})
// 修改语音消息构建器,添加播放状态和进度条
@Builder
VoiceMessage(message: Message) {
Row() {
if (message.sender === 'other') {
// 播放/暂停按钮
Image(this.playingVoiceId === message.id && this.isPlaying ?
$r('app.media.big24') : $r('app.media.big25'))
.width(24)
.height(24)
.margin({ right: 8 })
}
Column() {
// 语音时长
Text(`${message.duration}''`)
.fontSize(16)
.fontColor(message.sender === 'me' ? '#FFFFFF' : '#333333')
// 播放进度条(仅在播放当前消息时显示)
if (this.playingVoiceId === message.id) {
Progress({ value: this.playbackProgress, total: 100 })
.height(2)
.width('100%')
.color(message.sender === 'me' ? '#FFFFFF' : '#007AFF')
.backgroundColor(message.sender === 'me' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.1)')
.margin({ top: 4 })
}
}
.width('100%')
if (message.sender === 'me') {
// 播放/暂停按钮
Image(this.playingVoiceId === message.id && this.isPlaying ?
$r('app.media.big24') : $r('app.media.big25'))
.width(24)
.height(24)
.margin({ left: 8 })
}
}
.width(message.duration * 8 + 60) // 根据语音长度动态调整宽度
.height(40)
.padding({ left: 12, right: 12 })
.backgroundColor(message.sender === 'me' ? '#007AFF' : '#F0F0F0')
.borderRadius(20)
}
// 开始播放语音
private startPlayback(messageId: number) {
// 停止当前播放
if (this.playingVoiceId !== -1) {
this.stopPlayback()
}
this.playingVoiceId = messageId
this.isPlaying = true
this.playbackProgress = 0
// 获取语音消息
const voiceMessage = this.messages.find(msg => msg.id === messageId)
if (!voiceMessage || !voiceMessage.duration) return
// 模拟播放进度
const duration = voiceMessage.duration * 1000 // 转换为毫秒
const interval = 100 // 每100毫秒更新一次进度
const steps = duration / interval
const increment = 100 / steps
this.playbackTimer = setInterval(() => {
this.playbackProgress += increment
if (this.playbackProgress >= 100) {
this.stopPlayback()
}
}, interval)
}
// 暂停/继续播放
private togglePlayback() {
if (this.isPlaying) {
// 暂停播放
clearInterval(this.playbackTimer)
this.isPlaying = false
} else {
// 继续播放
this.isPlaying = true
// 获取语音消息
const voiceMessage = this.messages.find(msg => msg.id === this.playingVoiceId)
if (!voiceMessage || !voiceMessage.duration) return
// 计算剩余时间和步骤
const duration = voiceMessage.duration * 1000 // 转换为毫秒
const remainingProgress = 100 - this.playbackProgress
const remainingTime = (duration * remainingProgress) / 100
const interval = 100 // 每100毫秒更新一次进度
const steps = remainingTime / interval
const increment = remainingProgress / steps
this.playbackTimer = setInterval(() => {
this.playbackProgress += increment
if (this.playbackProgress >= 100) {
this.stopPlayback()
}
}, interval)
}
}
// 停止播放
private stopPlayback() {
clearInterval(this.playbackTimer)
this.playingVoiceId = -1
this.isPlaying = false
this.playbackProgress = 0
}
// 在组件销毁时清理
aboutToDisappear() {
this.stopPlayback()
}
6. 消息加密与安全
6.1 端到端加密标识
// 在聊天头部添加加密标识
Row() {
Image($r('app.media.big26'))
.width(16)
.height(16)
Text('端到端加密')
.fontSize(12)
.fontColor('#4CAF50')
.margin({ left: 4 })
}
.margin({ top: 4 })
// 添加加密信息对话框
@State showEncryptionInfo: boolean = false
// 在加密标识上添加点击事件
.onClick(() => {
this.showEncryptionInfo = true
})
// 加密信息对话框
@Builder
EncryptionInfoDialog() {
if (this.showEncryptionInfo) {
Stack() {
// 半透明背景
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(0, 0, 0, 0.4)')
.onClick(() => {
this.showEncryptionInfo = false
})
// 对话框内容
Column() {
// 标题
Text('端到端加密')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 16 })
// 图标
Image($r('app.media.big26'))
.width(64)
.height(64)
.margin({ bottom: 16 })
// 说明文本
Text('您与张三的聊天使用端到端加密。您们交换的消息和通话仅存储在您的设备上,任何人(包括我们)都无法读取或收听这些内容。')
.fontSize(14)
.fontColor('#666666')
.textAlign(TextAlign.Center)
.margin({ bottom: 24 })
// 了解更多按钮
Button('了解更多')
.fontSize(16)
.fontColor('#007AFF')
.backgroundColor('transparent')
.onClick(() => {
// 跳转到加密说明页面
})
// 确定按钮
Button('确定')
.width('100%')
.height(44)
.fontSize(16)
.fontColor('#FFFFFF')
.backgroundColor('#007AFF')
.borderRadius(22)
.margin({ top: 16 })
.onClick(() => {
this.showEncryptionInfo = false
})
}
.width('80%')
.padding(24)
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
}
}
6.2 阅后即焚消息
// 在消息模型中添加自动销毁属性
interface Message {
// 其他属性...
autoDestruct?: boolean; // 是否为阅后即焚消息
destructAfter?: number; // 查看后销毁时间(秒)
destructCountdown?: number; // 销毁倒计时
}
// 在上下文菜单中添加阅后即焚选项
this.MenuOption('阅后即焚', $r('app.media.big27'), () => {
this.showDestructOptions = true
this.showContextMenu = false
})
// 阅后即焚选项对话框
@State showDestructOptions: boolean = false
@State selectedDestructTime: number = 30 // 默认30秒
@Builder
DestructOptionsDialog() {
if (this.showDestructOptions) {
Stack() {
// 半透明背景
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(0, 0, 0, 0.4)')
.onClick(() => {
this.showDestructOptions = false
})
// 对话框内容
Column() {
// 标题
Text('设置阅后即焚时间')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 24 })
// 时间选项
Row() {
this.DestructTimeOption(5, '5秒')
this.DestructTimeOption(10, '10秒')
this.DestructTimeOption(30, '30秒')
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
.margin({ bottom: 16 })
Row() {
this.DestructTimeOption(60, '1分钟')
this.DestructTimeOption(300, '5分钟')
this.DestructTimeOption(600, '10分钟')
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
.margin({ bottom: 24 })
// 确定按钮
Button('确定')
.width('100%')
.height(44)
.fontSize(16)
.fontColor('#FFFFFF')
.backgroundColor('#007AFF')
.borderRadius(22)
.onClick(() => {
this.enableDestructMode()
this.showDestructOptions = false
})
// 取消按钮
Button('取消')
.width('100%')
.height(44)
.fontSize(16)
.fontColor('#666666')
.backgroundColor('transparent')
.margin({ top: 16 })
.onClick(() => {
this.showDestructOptions = false
})
}
.width('80%')
.padding(24)
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
}
}
// 阅后即焚时间选项
@Builder
DestructTimeOption(seconds: number, text: string) {
Column() {
Text(text)
.fontSize(16)
.fontColor(this.selectedDestructTime === seconds ? '#007AFF' : '#333333')
.fontWeight(this.selectedDestructTime === seconds ? FontWeight.Medium : FontWeight.Normal)
}
.width(80)
.height(80)
.justifyContent(FlexAlign.Center)
.borderRadius(8)
.backgroundColor(this.selectedDestructTime === seconds ? 'rgba(0, 122, 255, 0.1)' : '#F5F5F5')
.onClick(() => {
this.selectedDestructTime = seconds
})
}
// 启用阅后即焚模式
@State isDestructMode: boolean = false
private enableDestructMode() {
this.isDestructMode = true
this.showToast(`已开启阅后即焚模式,消息将在查看${this.selectedDestructTime}秒后销毁`)
}
// 修改发送消息方法,支持阅后即焚
sendMessage() {
// ...
// 添加新消息
const newMessage: Message = {
// 其他属性...
autoDestruct: this.isDestructMode,
destructAfter: this.isDestructMode ? this.selectedDestructTime : undefined
}
// ...
}
// 在消息显示中添加阅后即焚标识和倒计时
if (message.autoDestruct) {
Row() {
Image($r('app.media.big27'))
.width(16)
.height(16)
.fillColor(message.sender === 'me' ? 'rgba(255, 255, 255, 0.7)' : '#666666')
if (message.destructCountdown !== undefined) {
Text(`${message.destructCountdown}s`)
.fontSize(12)
.fontColor(message.sender === 'me' ? 'rgba(255, 255, 255, 0.7)' : '#666666')
.margin({ left: 4 })
}
}
.margin({ top: 4 })
}
// 处理阅后即焚消息的查看和销毁
private startDestructCountdown(messageId: number) {
// 查找消息
const index = this.messages.findIndex(msg => msg.id === messageId)
if (index === -1) return
const message = this.messages[index]
if (!message.autoDestruct || message.destructCountdown !== undefined) return
// 开始倒计时
message.destructCountdown = message.destructAfter
const timer = setInterval(() => {
if (message.destructCountdown <= 0) {
// 销毁消息
clearInterval(timer)
this.messages.splice(index, 1)
return
}
message.destructCountdown--
}, 1000)
}
7. 常见问题与解决方案
7.1 长消息列表性能优化
问题:当聊天记录非常多时,列表渲染和滚动性能可能会下降。
解决方案:
- 使用
LazyForEach
替代ForEach
,实现虚拟列表 - 实现分页加载和无限滚动
- 优化列表项渲染,减少不必要的重绘
- 使用
onVisibleAreaChange
事件监听可见区域变化,只处理可见的消息
List({ scroller: this.scroller }) {
// ...
}
.onVisibleAreaChange((first: number, last: number) => {
// 只处理可见区域内的消息
for (let i = first; i <= last; i++) {
const message = this.messages[i]
if (message && message.autoDestruct && message.sender === 'other' && message.destructCountdown === undefined) {
this.startDestructCountdown(message.id)
}
}
})
7.2 多设备同步问题
问题:用户可能在多个设备上使用聊天应用,需要保持消息同步。
解决方案:
- 实现基于云的消息同步机制
- 使用消息ID和时间戳确保消息顺序一致
- 实现消息状态同步(已读、已送达等)
- 处理冲突解决(例如,同时在不同设备编辑或删除消息)
7.3 网络连接不稳定处理
问题:移动设备的网络连接可能不稳定,导致消息发送失败或延迟。
解决方案:
- 实现消息队列和重试机制
- 显示清晰的网络状态指示器
- 在离线状态下允许编写和排队消息
- 实现消息发送状态的实时更新
// 在聊天头部添加网络状态指示器
@State networkStatus: 'online' | 'offline' | 'connecting' = 'online'
// 网络状态指示器
if (this.networkStatus !== 'online') {
Row() {
if (this.networkStatus === 'connecting') {
LoadingProgress()
.width(16)
.height(16)
.color('#FFC107')
.margin({ right: 8 })
Text('正在连接...')
.fontSize(14)
.fontColor('#FFC107')
} else {
Image($r('app.media.big28'))
.width(16)
.height(16)
.fillColor('#F44336')
.margin({ right: 8 })
Text('当前处于离线状态,消息将在恢复连接后发送')
.fontSize(14)
.fontColor('#F44336')
}
}
.width('100%')
.padding(8)
.backgroundColor(this.networkStatus === 'connecting' ? '#FFF8E1' : '#FFEBEE')
}
8. 总结与扩展
在本进阶篇中,我们探索了聊天消息列表的多种高级功能,包括消息加载与分页、高级交互效果、消息搜索与过滤、多媒体消息增强、消息加密与安全等。这些功能可以大大提升聊天应用的用户体验和功能完整性。
00
- 0回答
- 4粉丝
- 0关注
相关话题
- 155.[HarmonyOS NEXT 实战案例十二 :List系列] 聊天消息列表 - 基础篇
- 152.[HarmonyOS NEXT 实战案例十二:List系列] 卡片样式列表组件实战:打造精美电商应用 进阶篇
- 148.[HarmonyOS NEXT 实战案例八 :List系列] 粘性头部列表进阶篇
- 146.[HarmonyOS NEXT 实战案例七 :List系列] 可选择列表进阶篇
- 154.[HarmonyOS NEXT 实战案例十一 :List系列] 自定义内容列表 - 进阶篇
- 134.[HarmonyOS NEXT 实战案例六:List系列] 垂直列表组件实战:打造高效联系人列表 进阶篇
- 142.[HarmonyOS NEXT 实战案例九:List系列] 分组列表组件实战:打造分类设置菜单 进阶篇
- 138.[HarmonyOS NEXT 实战案例七:List系列] 多列列表组件实战:打造精美应用推荐页 进阶篇
- [HarmonyOS NEXT 实战案例:聊天应用] 进阶篇 - 交互功能与状态管理
- 144.[HarmonyOS NEXT 实战案例十:List系列] 字母索引列表组件实战:打造高效联系人应用 进阶篇
- 140.[HarmonyOS NEXT 实战案例八:List系列] 滑动操作列表组件实战:打造高效待办事项应用 进阶篇
- 136.[HarmonyOS NEXT 实战案例七:List系列] 水平列表组件实战:打造精美图片库 进阶篇
- 151.[HarmonyOS NEXT 实战案例十二:List系列] 卡片样式列表组件实战:打造精美电商应用 基础篇
- 150.[HarmonyOS NEXT 实战案例十一:List系列] 下拉刷新和上拉加载更多列表组件实战:打造高效新闻应用 进阶篇
- 147.[HarmonyOS NEXT 实战案例八 :List系列] 粘性头部列表基础篇