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

156.[HarmonyOS NEXT 实战案例十二 :List系列] 聊天消息列表 - 进阶篇

原创 若城 2025-06-29
115

[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. 总结与扩展

在本进阶篇中,我们探索了聊天消息列表的多种高级功能,包括消息加载与分页、高级交互效果、消息搜索与过滤、多媒体消息增强、消息加密与安全等。这些功能可以大大提升聊天应用的用户体验和功能完整性。

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

评论