我写个HarmonyOS Next版本的微信聊天01
前言
代码会统一放在码云上,纯静态的完整代码会放在末尾
案例目标
这个是安卓手机上的真正的微信聊天界面功能效果
实际效果
案例功能
- 页面沉浸式
- 聊天内容滚动
- 输入框状态切换
- 聊天信息框宽度自适应
- 输入法避让
- 语音消息根据时长自动宽度
- canvas声纹 按住说话
- 手势坐标检测取消发送-语音转文字
- 发送文字
- 录音-发送语音
- 声音播放-语音消息
- AI 语音转文字
新建项目
修改项目桌面名称和图标
entry\src\main\resources\zh_CN\element\string.json
{ "string": [ { "name": "module_desc", "value": "模块描述" }, { "name": "EntryAbility_desc", "value": "description" }, { "name": "EntryAbility_label", "value": "我的聊天项目" // 😄 } ] }
\entry\src\main\module.json5
... "abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "description": "$string:EntryAbility_desc", "icon": "$media:chat",😄 ...
$media:chat 来自于 resource下的名为chat的图标
设置沉浸式
- 图一为默认情况下的页面布局,可以看到我们的页面是无法触及到顶部状态栏和底部菜单栏的
- 图二为设置了沉浸式效果后,布局按钮可以触及到顶部状态栏了
- 图三为动态获取到了顶部状态栏的高度,然后给容器添加了相应的padding,挤压布局元素到顶部状态栏的下方
设置沉浸式和获取顶部状态栏高度
\entry\src\main\ets\entryability\EntryAbility.ets
... onWindowStageCreate(windowStage: window.WindowStage): void { // Main window is created, set main page for this ability hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); windowStage.loadContent('pages/Index', (err) => { if (err.code) { hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); return; } hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.'); // 设置应用全屏 let windowClass: window.Window = windowStage.getMainWindowSync(); // 获取应用主窗口 windowClass.setWindowLayoutFullScreen(true) let type = window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR; // 导航条避让 let avoidArea = windowClass.getWindowAvoidArea(type); let bottomRectHeight = avoidArea.bottomRect.height; // 获取到导航条区域的高度 const vpHeight = px2vp(bottomRectHeight) // 转换成 vp单位的数值 // 把导航栏高度数据 存在全局 AppStorage.setOrCreate("vpHeight", vpHeight) }); } ...
页面使用导航栏高度设置padding
@Entry @Component struct Index { @StorageProp("vpHeight") topHeight: number = 0 build() { Column() { Button("按钮") } .width("100%") .height("100%") .backgroundColor(Color.Yellow) .padding({ top: this.vpHeight, }) } }
搭建页面基本布局
@Entry @Component struct Index { // 状态栏高度 @StorageProp("vpHeight") vpHeight: number = 0 build() { Column() { // 1 顶部标题栏 Row() { Image($r("app.media.left")) .width(25) Text("kto卋讓硪玩孫悟空") Image($r("app.media.more")) .width(25) } .width("100%") .justifyContent(FlexAlign.SpaceBetween) .border({ width: { bottom: 1 }, color: "#ddd" }) .padding(10) // 2 聊天滚动容器 // 3 输入面板 } .height('100%') .width('100%') .backgroundColor("#EDEDED") .padding({ top: this.vpHeight + 20 }) } }
页面滚动和文字信息框
build() { Column() { // 1 顶部标题栏 ..... // 2 聊天滚动容器 Scroll() { Column({ space: 10 }) { this.chatTextBuilder("吃饭", `22:23`) } .width("100%") .padding(10) .justifyContent(FlexAlign.Start) } .layoutWeight(1) .align(Alignment.Top) .expandSafeArea([SafeAreaType.KEYBOARD], [SafeAreaEdge.BOTTOM]) } .height('100%') .width('100%') .backgroundColor("#EDEDED") .backgroundImageSize(ImageSize.Cover) .padding({ top: this.vpHeight + 20 }) } // 文字消息 @Builder chatTextBuilder(text: string, time: string) { Column({ space: 5 }) { Text(time) .width("100%") .textAlign(TextAlign.Center) .fontColor("#666") .fontSize(14) Row() { Flex({ justifyContent: FlexAlign.End }) { Row() { Text(text) .padding(11); Text() .width(10) .height(10) .backgroundColor("#93EC6C") .position({ right: 0, top: 15 }) .translate({ x: 5, }) .rotate({ angle: 45 }); } .backgroundColor("#93EC6C") .margin({ right: 15 }) .borderRadius(5); Image($r("app.media.avatar")) .width(40) .aspectRatio(1); } .width("100%"); } .width("100%") .padding({ left: 40 }) .justifyContent(FlexAlign.End) } .width("100%") }
亮点
以下代码是实现上面,自适应宽度的关键
- 当文字较小时,绿色聊天框宽度自适应
- 当文字较多时,绿色聊天框宽度自动变宽,但是不会铺满一行,微信也是这样设计的
底部消息发送框
显示输入框还是 "按住说话"
可以看到,底部消息发送框起码有三种状态
- 按住说话
- 文本输入框
- 文本输入框 - 发送
程序中,通过枚举决定 按住说话-文本输入框两种状态
/** * 当前输入状态 语音或者文本 */ enum WXInputType { /** * 语音输入 */ voice = 0, /** * 文本输入 */ text = 1 }
显示 “发送” 按钮
另外,通过判断文本输入的长度来决定 是否显示 绿色的 发送
显示文本输入框自动获得焦点
设置输入时 键盘避让
不设置避让时,可以看到底部聊天被弹出的键盘顶上去了。
解决方法
设置拓展安全区域为软键盘区域,同时设置扩展安全区域的方向为下方区域
发送文本消息
定义消息类型枚举
enum MessageType { /** * 声音 */ voice = 0, /** * 文本 */ text = 1 }
定义消息类
用来快速生成消息对象,可以表示语音消息和文本消息
// 消息 class ChatMessage { /** * 消息类型:【录音、文本】 */ type: MessageType /** * 内容 [录音-文件路径,文本-内容] */ content: string /** * 消息时间 */ time: string /** * 声音的持续时间 单位毫秒 */ duration?: number /** * 录音转的文字 */ translateText?: string /** * 是否显示转好的文字 */ isShowTranslateText: boolean = false constructor(type: MessageType, content: string, duration?: number, translateText?: string) { this.type = type this.content = content const date = new Date() this.time = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}` this.duration = duration this.translateText = translateText } }
定义消息数组
// 消息 @State chatList: ChatMessage[] = []
定义发送文本消息的方法
// 发送文本消息 sendTextMessage = () => { if (!this.textValue.trim()) { return } const chat = new ChatMessage(MessageType.text, this.textValue.trim()) this.chatList.push(chat) this.textValue = "" }
注册发送文本消息事件
Button("发送") .backgroundColor("#08C060") .type(ButtonType.Normal) .fontColor("#fff") .borderRadius(5) .onClick(this.sendTextMessage)
遍历消息数组
// 2 聊天滚动容器 Scroll() { Column({ space: 10 }) { ForEach(this.chatList, (item: ChatMessage, index: number) => { if (item.type === MessageType.text) { this.chatTextBuilder(item.content, item.time) } }) }.width("100%") .padding(10) .justifyContent(FlexAlign.Start) } .layoutWeight(1) .align(Alignment.Top) .expandSafeArea([SafeAreaType.KEYBOARD], [SafeAreaEdge.BOTTOM])