本文原创发布在华为开发者社区

介绍

本示例介绍如何使用speechRecognizer实时语言转文字,并且根据光标位置插入文字,以及文本一键清空功能。

实现实时语音转文字功能源码链接

效果预览

请添加链接描述

使用说明

  1. 点击顶部按钮可切换本人或非本人模拟聊天界面发送消息,本人发送右对齐,非本人发送左对齐。
  2. 点击RichEditor组件唤起输入法,已发送的消息自动避让。
  3. 长按启动实时语音转文字,松开停止语音转文字,根据光标所在位置插入语音识别的文字,点击清空可以清除RichEditor组件中的内容。
  4. 点击发送可以将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)
}

实时语音转文字

  1. 语音转文字需使用麦克风权限,需要在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);
    }
  2. 调用系统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);
  }
}

鸿蒙场景化代码
1 声望0 粉丝