本文原创发布在华为开发者社区。
介绍
本示例介绍如何使用speechRecognizer实时语言转文字,并且根据光标位置插入文字,以及文本一键清空功能。
效果预览
使用说明
- 点击顶部按钮可切换本人或非本人模拟聊天界面发送消息,本人发送右对齐,非本人发送左对齐。
- 点击RichEditor组件唤起输入法,已发送的消息自动避让。
- 长按启动实时语音转文字,松开停止语音转文字,根据光标所在位置插入语音识别的文字,点击清空可以清除RichEditor组件中的内容。
- 点击发送可以将RichEditor组件中的内容发送出去,可发送文字和图片消息。
实现思路
聊天页面左右布局
通过Flex组件实现左右布局,本人发送时设置方向为FlexDirection.RowReverse,非本人发送时设置方向为FlexDirection.Row。具体实现如下:
List({scroller: this.listScroller}) {
ForEach(this.data, (item: MsgContent) => {
ListItem() {
// 通过isSelf判断是否是本人发送的消息,来决定Flex组件的direction是否需要进行反转。
Flex({ direction: item.isSelf ? FlexDirection.RowReverse : FlexDirection.Row, space: { main: LengthMetrics.vp(8) } }) {
Image($r('app.media.avatar'))
.width(32)
.aspectRatio(1)
Text() {
...
}
...
}
.width('100%')
}
.margin(12)
}, (item: MsgContent, index: number) => `${JSON.stringify(item)}_${JSON.stringify(index)}`)
}
图文混排消息显示
整体使用Text去布局,文字通过内嵌Span组件显示,图片显示通过内嵌ImageSpan组件显示,具体代码如下:
Text() {
ForEach(item.content, (content: MsgTextImage) => {
if (content.type === MessageType.Text) {
Span(content.content)
}
if (content.type === MessageType.Image) {
ImageSpan(content.content)
.clip(true)
.objectFit(ImageFit.Contain) // 图片填充效果设置为Contain,防止图片超出范围
.size({ width: '20vp', height: '20vp' })
.margin(2)
.verticalAlign(ImageSpanAlignment.CENTER)
}
}, (item: MsgTextImage, index: number) => `${JSON.stringify(item)}_${JSON.stringify(index)}`)
}
.padding(6)
.borderRadius(4)
.lineSpacing(LengthMetrics.vp(8)) // 设置下每行之间的空格,这样不至于看着很紧凑
.backgroundColor('#ADD8E6')
.constraintSize({ maxWidth: '75%', minHeight: 32 }) // 这里设置下最大宽度和最小高度,消息太长时不要覆盖整个屏幕宽度
点击发送消息时,需对RichEditor组件中的消息转换成其他结构,发送完毕,清理RichEditor输入区域具体代码如下:
private sendMessage() {
let message: MsgTextImage[] = [];
richController.getSpans().forEach(span => {
if ((span as RichEditorTextSpanResult).textStyle !== undefined) {
message.push({ // 文本消息转换,type为Text,使用Span组件显示
type: MessageType.Text,
content: (span as RichEditorTextSpanResult).value
});
} else {
message.push({ // 图片消息转换,type为Image,使用ImageSpan组件显示
type: MessageType.Image,
content: (span as RichEditorImageSpanResult).valueResourceStr
});
}
})
if (message.length > 0) {
this.data.push({
isSelf: this.isSelf,
content: message
});
}
richController.deleteSpans();
}
实现已发送的消息自动避让
首先在aboutToAppear中设置键盘模式为上抬模式,代码如下:
aboutToAppear(): void {
this.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE);
}
但仅仅只设置上抬模式还不够,消息数超过屏幕时,仍然看不到最新发送的消息。因此,在点击RichEditor组件唤起输入法时,需要将消息滚动到底部,代码如下:
RichEditor(this.options)
.width('100%')
.borderRadius(4)
.backgroundColor('#08000000')
.constraintSize({ maxHeight: 128 })
.placeholder(this.placeHolder, { fontColor: '#4D242E3E', font: { size: 13 } })
.layoutWeight(1)
.clip(true)
.onDidChange(() => {
this.isHaveMsg = (richController.getSpans().length !== Number(0))
})
.onEditingChange(this.editingChangedCb) // 监听编辑状态是否发生改变,并执行回调
editingChanged = () => {
this.curMenuAction = EditMenuAction.None;
// 需要延迟一会再触发,键盘弹起之后再触发,否则无效果
setTimeout(() => {
this.listScroller.scrollEdge(Edge.End);
}, 100)
}
实时语音转文字
语音转文字需使用麦克风权限,需要在module.json5文件中申明麦克风权限,使用时去请求麦克风权限
// module.json5中申明权限 "requestPermissions": [ { "name": "ohos.permission.MICROPHONE", "reason": "$string:microphone_reason", "usedScene": { "abilities": [ "EntryAbility" ], "when": "always" }, } ] // 页面即将加载时,请求麦克风权限 async aboutToAppear(): Promise<void> { this.speechRecognizer.intiEngine(); await requestPermission(['ohos.permission.MICROPHONE'], getContext() as common.UIAbilityContext); } // 调用系统API请求所需权限 export async function requestPermission(permissions: Permissions[], context: common.UIAbilityContext): Promise<boolean> { let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); const result = await atManager.requestPermissionsFromUser(context, permissions); return !!result.authResults.length && result.authResults.every(authResults => authResults === 0); }
调用系统speechRecognizerAPI进行实时语音识别,具体代码如下:
import { speechRecognizer } from '@kit.CoreSpeechKit'; export class SpeechRecognizer { private engineParams: speechRecognizer.CreateEngineParams = { language: 'zh-CN', // 目前系统API只支持设置中文 online: 1, // 目前系统API只支持离线模式 extraParams: { 'locate': 'CN', 'recognizerMode': 'long' } // 设置recognizerMode为长时模式,设置短时模式时说完一句话会自动结束识别 }; private asrEngine?: speechRecognizer.SpeechRecognitionEngine; private sessionId: string = 'SpeechRecognizer_' + Date.now(); public async intiEngine() { this.asrEngine = await speechRecognizer.createEngine(this.engineParams); } // 开始语音识别,并提供回调函数,用于返回结果 public start(callback: (srr: speechRecognizer.SpeechRecognitionResult) => void = () => {}) { this.setListener(callback); this.startListening(); } // 停止语音识别 public stop() { this.asrEngine?.finish(this.sessionId); } public shutdown() { this.asrEngine?.shutdown(); } // 启动监听 private startListening() { let recognizerParams: speechRecognizer.StartParams = { sessionId: this.sessionId, audioInfo: { audioType: 'pcm', sampleRate: 16000, soundChannel: 1, sampleBit: 16 }, extraParams: { recognitionMode: 0, maxAudioDuration: 60000 } } this.asrEngine?.startListening(recognizerParams); } private setListener(callback: (srr: speechRecognizer.SpeechRecognitionResult) => void = () => {}) { let listener: speechRecognizer.RecognitionListener = { onStart(sessionId: string, eventMessage: string) { }, onEvent(sessionId: string, eventCode: number, eventMessage: string) { }, onResult(sessionId: string, result: speechRecognizer.SpeechRecognitionResult) { // 语音识别到结果后,通过回调函数将识别的结果返回 callback && callback(result); }, onComplete(sessionId: string, eventMessage: string) { // recognizerMode设置为短时模式时,如果仍需继续识别,需要在此处再次调用startListening,启动监听。 }, onError(sessionId: string, errorCode: number, errorMessage: string) { }, } this.asrEngine?.setListener(listener); } }
根据光标位置插入语音识别的文字
通过getCaretOffset获取到光标所在位置。在插入文字时设置对应的偏移量来达成目的,RichEditor输入框无内容时增加正在识别...文字提示,有内容时增加...内容提示
startSpeechRecognizer() {
// 输入框无内容时,直接使用提示文本
this.placeHolder = '正在识别...';
this.fillPlaceHolder(); // 填充提示文本
this.speechRecognizer.start((result) => {
this.insertSpan(result.result); // 处理语音识别到的结果
if (result.isFinal) { // 语音识别完,将caretOffset,speechTextLength重置
this.caretOffset = richController.getCaretOffset();
this.speechTextLength = 0;
}
})
}
stopSpeechRecognizer() {
this.placeHolder = '';
this.speechRecognizer.stop();
// 结束识别,需将插入的...提示删除
richController.deleteSpans({ start: this.caretOffset, end: this.caretOffset + 3 });
}
private insertSpan(text: string) {
if (text.length <= 0) { // 未识别到内容时,直接return
return;
}
if (this.speechTextLength === 0) { // 首次识别,直接根据光标位置将识别到的文字插入
richController.addTextSpan(text, { offset: this.caretOffset });
} else {
// 非首次识别,需先将上次识别的文字删除,再填充新识别的文字
richController.deleteSpans({ start: this.caretOffset, end: this.caretOffset + this.speechTextLength });
richController.addTextSpan(text, { offset: this.caretOffset });
}
this.speechTextLength = text.length; // 每次记录识别文字的长度
}
private fillPlaceHolder() {
this.caretOffset = richController.getCaretOffset();
if (richController.getSpans().length > 0) {
// 输入框有内容时,插入...提示,并更新caretOffset位置
richController.addTextSpan('...', {
offset: this.caretOffset,
style: {
fontColor: '#4D242E3E',
fontSize: 13,
}
})
richController.setCaretOffset(this.caretOffset);
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。