
【HarmonyOS AI赋能】朗读控件详解 原创
【HarmonyOS AI赋能】朗读控件详解
一、前言
鸿蒙系统提供了系统级别的朗读控件,来实现对文本进行朗读的业务需求。不需要复杂的SDK接入和集成,就可实现商业级别的朗读效果。
朗读控件分为听筒组件和朗读控制器,以及朗读面板三部分组成。
朗读面板又分为吸边小面板和全屏朗读面板。
需要注意的是,仅支持中国境内(不包含中国香港、中国澳门、中国台湾)提供服务。并且实时朗读的正文信息长度10000字符以内。
二、如何使用朗读控件?
以下代码为上图所示的DEMO源码,可直接新建工程后,贴到index.ets类中,启自动签名后,启动查看效果。下面为大家详细拆解如何使用。
// 导入语音朗读相关的组件和类型
import { TextReader, TextReaderIcon, ReadStateCode } from '@kit.SpeechKit';
@Entry
@Component
struct Index {
/**
* 待加载的文章列表
*/
@State readInfoList: TextReader.ReadInfo[] = [];
/**
* 当前选中的文章
*/
@State selectedReadInfo: TextReader.ReadInfo = this.readInfoList[0];
/**
* 朗读状态
*/
@State readState: ReadStateCode = ReadStateCode.WAITING;
/**
* 初始化状态标记
*/
@State isInit: boolean = false;
// 组件即将显示时触发
async aboutToAppear(){
/**
* 模拟加载文章数据
*/
let readInfoList: TextReader.ReadInfo[] = [{
id: '001',
title: {
text:'水调歌头.明月几时有',
isClickable:true
},
author:{
text:'宋.苏轼',
isClickable:true
},
date: {
text:'2024/01/01',
isClickable:false
},
bodyInfo: '明月几时有?把酒问青天。不知天上宫阙,今夕是何年?'
}];
// 更新状态变量
this.readInfoList = readInfoList;
this.selectedReadInfo = this.readInfoList[0];
// 初始化朗读组件
this.init();
}
/**
* 初始化朗读组件
*/
async init() {
// 朗读参数配置
const readerParam: TextReader.ReaderParam = {
isVoiceBrandVisible: true, // 显示品牌信息
businessBrandInfo: {
panelName: '小艺朗读', // 面板名称
panelIcon: $r('app.media.startIcon') // 面板图标
}
}
try {
// 获取上下文
let context: Context | undefined = this.getUIContext().getHostContext()
if (context) {
// 初始化朗读组件
await TextReader.init(context, readerParam);
this.isInit = true; // 标记初始化完成
this.setActionListener(); // 设置事件监听
}
} catch (err) {
// 初始化失败时打印错误信息
console.error(`TextReader failed to init. Code: ${err.code}, message: ${err.message}`);
}
}
// 设置朗读事件监听
setActionListener() {
// 监听朗读状态变化
TextReader.on('stateChange', (state: TextReader.ReadState) => {
this.onStateChanged(state);
});
// 监听加载更多请求
TextReader.on('requestMore', () => {
TextReader.loadMore([], true);
})
}
// 处理朗读状态变化
onStateChanged = (state: TextReader.ReadState) => {
// 只处理当前选中文章的状态变化
if (this.selectedReadInfo?.id === state.id) {
this.readState = state.state;
} else {
this.readState = ReadStateCode.WAITING;
}
}
// 构建UI界面
build() {
Column() {
// 朗读状态图标
TextReaderIcon({ readState: this.readState })
.margin({ right: 20 })
.width(32)
.height(32)
.onClick(async () => {
// 点击图标时开始朗读
try {
await TextReader.start(this.readInfoList, this.selectedReadInfo?.id);
} catch (err) {
// 朗读失败时打印错误信息
console.error(`TextReader failed to start. Code: ${err.code}, message: ${err.message}`);
}
})
}
.height('100%')
}
}
(1)听筒控件TextReaderIcon
提供的听筒控件,可以同步朗读状态,如上动态图所示,有现成的朗读效果,如果业务需要使用,可以用。或者直接跳过也可以,控件参数比较简单,如下代码所示:
TextReaderIcon({ readState: this.readState })
.width(32)
.height(32)
.onClick(async () => {
// do something...
})
readState 需要通过朗读控制器TextReader去监听,当前的朗读状态,然后设置给朗读控件,就可以实现朗读控件的动态效果。
TextReader.on('stateChange', (state: TextReader.ReadState) => {
});
并且根据DEMO代码可发现,听筒控件的点击事件,触发了朗读控制器对象的开启操作。
综上所述,我们可以不使用话筒控件,直接使用朗读控制器,调用其接口实现文本朗读的效果。
(2)朗读控制器TextReader
TextReader是整个朗读操作逻辑的核心操作对象,系统接口提供了该单例对象。使用之前需要先初始化:
// 朗读参数配置
const readerParam: TextReader.ReaderParam = {
isVoiceBrandVisible: true, // 显示品牌信息
businessBrandInfo: {
panelName: '朗读', // 面板名称
},
isMinibarNeeded: true
}
await TextReader.init(context, readerParam);
然后再进行常规的启动,暂停(pause),销毁暂停(stop)【ps: 我现在对系统接口,这种类似双暂停的命名很无语 = =。猛地看起来,两个暂停,傻傻分不清楚。但是目前stop后者,多用于整个生命周期回收重置的调用处理。】:
// 朗读启动配置
const startParams: TextReader.StartParams = {
isMinibarHidden: this.mTextReaderInitData?.isMinibarNeeded ?? true,
}
// 填充朗读内容
let readInfoList: TextReader.ReadInfo[] = [{
id: '002',
title: {
text:'水调歌头.明月几时有2',
isClickable:true
},
author:{
text:'宋.苏轼2',
isClickable:true
},
date: {
text:'2025/02/02',
isClickable:false
},
bodyInfo: '2明月几时有?把酒问青天。不知天上宫阙,今夕是何年?'
}];
// 启动朗读
await TextReader.start(readInfoList, this.readInfoList[0].id, startParams);
再之后进行根据业务需求,做一些监听和反监听的处理了,种类很多详情参见api接口:
TextReader.on('stateChange', (state: TextReader.ReadState) => {
});
(3)朗读面板
关于朗读面板,我理解是通过子窗口来实现,吸边小面板和全屏面板的效果。因为文档中有强调使用朗读控件初始化前,需要使用windowManager进行舞台窗口对象的注入(
WindowManager.setWindowStage(windowStage);):
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { WindowManager } from '@kit.SpeechKit';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
}
onWindowStageCreate(windowStage: window.WindowStage): void {
WindowManager.setWindowStage(windowStage);
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
return;
}
});
}
}
但是在实际使用中我发现,即使不调用该注入方法。初始化也不会报错。目前已提交工单,后续结论同步到该文章。
朗读面板的操作逻辑很简单,分别提供了显示和隐藏两个面板(吸边小面板和全屏面板)的属性或者接口,来控制显隐。
吸边小面板可通过属性和方法分别设置显隐:
首先是在初始化配置参数中:
const readerParam: TextReader.ReaderParam = {
isMinibarNeeded: true
}
其次是在启动配置参数中:
const startParams: TextReader.StartParams = {
isMinibarHidden: true,
}
再之后就是方法接口:
TextReader.showMinibar();
TextReader.hideMinibar();
全屏朗读面板默认启动朗读后就会显示,系统提供了两套接口,可以在start后调用hide就可隐藏全屏朗读面板:
TextReader.hidePanel();
TextReader.showPanel();
await TextReader.start(readInfoList, this.mCurrentReadInfo.id);
TextReader.hidePanel();
三、工具类封装源码共享:
封装ReaderIconView朗读图标,联动管理类的朗读状态,即插即用。
import { ReadStateCode, TextReaderIcon } from "@kit.SpeechKit";
import { TextReaderMgr, TextReaderRegister } from "../mgr/TextReaderMgr";
import { common } from "@kit.AbilityKit";
@Component
export struct ReaderIconView {
private TAG: string = "ReaderIconView";
/**
* 朗读状态
*/
@State readState: ReadStateCode = ReadStateCode.WAITING;
private mTextReaderRegister: TextReaderRegister = {
onStateChange: (state: ReadStateCode): void => {
this.readState = state;
console.log(this.TAG, "mTextReaderRegister onStateChange state: " + state);
}
}
aboutToAppear(): void {
const context = getContext(this) as common.UIAbilityContext;
TextReaderMgr.Ins().initReader(context, null, null, this.mTextReaderRegister);
console.log(this.TAG, " aboutToAppear initReader done");
}
build() {
TextReaderIcon({ readState: this.readState })
.width("100%")
.height("100%")
}
}
封装单例朗读管理类,用于便捷操作朗读相关接口,封装细节,方便快速调用:
import { ReadStateCode, TextReader } from "@kit.SpeechKit";
/**
* 初始化配置对象
*/
export class TextReaderInitData {
// 全屏面板标题名称
panelName: string = "";
// 是否需要吸边小面板
isMinibarNeeded: boolean = true;
// 是否需要全屏面板
isPanelNeeded: boolean = true;
}
/**
* 控制器操作回调
*/
export interface TextReaderCall {
onReady: () => void
onInitFail: (err: string) => void
onFail: (err: string) => void
}
/**
* 监听回调
*/
export interface TextReaderRegister {
onStateChange: (state: ReadStateCode) => void
}
/**
* 错误码
*/
export enum TextReaderFail {
UnInit = "0",
TextReaderInfoNULL = "1"
}
/**
* 文本朗读对象
*/
export class TextReaderInfo {
title: string = "";
content: string = "";
author?: string = "";
date?: string = "";
}
/**
* 文本朗读管理类
*/
export class TextReaderMgr {
private TAG: string = "TextReaderMgr";
private static mTextReaderMgr: TextReaderMgr | null = null;
private mInit: boolean = false;
private mTextReaderCall: TextReaderCall | null = null;
private mTextReaderInitData: TextReaderInitData | null = null;
private mTextReaderRegister: TextReaderRegister | null = null;
private mCurrentReadInfo: TextReader.ReadInfo | null = null;
public static Ins() {
if (!TextReaderMgr.mTextReaderMgr) {
TextReaderMgr.mTextReaderMgr = new TextReaderMgr();
}
return TextReaderMgr.mTextReaderMgr;
}
/**
* 设置朗读事件监听
*/
private setActionListener() {
// 监听朗读状态变化
TextReader.on('stateChange', (state: TextReader.ReadState) => {
let readState: ReadStateCode = ReadStateCode.WAITING;
if (this.mCurrentReadInfo?.id === state.id) {
readState = state.state;
} else {
readState = ReadStateCode.WAITING;
}
this.mTextReaderRegister?.onStateChange(readState);
});
// 监听加载更多请求
TextReader.on('requestMore', (callbackStr) => {
console.log(this.TAG, " callbackStr: " + callbackStr);
let readInfoList: TextReader.ReadInfo[] = [{
id: '002',
title: {
text: '水调歌头.明月几时有2',
isClickable: true
},
author: {
text: '宋.苏轼2',
isClickable: true
},
date: {
text: '2025/02/02',
isClickable: false
},
bodyInfo: '2明月几时有?把酒问青天。不知天上宫阙,今夕是何年?'
}];
TextReader.loadMore(readInfoList, true);
})
}
/**
* 初始化朗读播放控件
*/
public async initReader(context: Context, callback?: TextReaderCall | null, data?: TextReaderInitData | null,
register?: TextReaderRegister) {
this.mTextReaderCall = callback ?? null;
this.mTextReaderInitData = data ?? null;
this.mTextReaderRegister = register ?? null;
// 朗读参数配置
const readerParam: TextReader.ReaderParam = {
isVoiceBrandVisible: data?.panelName == "" ? false : true ?? true, // 显示品牌信息
businessBrandInfo: {
panelName: data?.panelName == "" ? '朗读' : data?.panelName ?? '朗读', // 面板名称
},
isMinibarNeeded: data?.isMinibarNeeded ?? true
}
try {
if (context) {
// 初始化朗读组件
await TextReader.init(context, readerParam);
this.mInit = true; // 标记初始化完成
this.setActionListener(); // 设置事件监听
this.mTextReaderCall?.onReady();
}
} catch (err) {
// 初始化失败时打印错误信息
console.error(this.TAG, `TextReader failed to init. Code: ${err.code}, message: ${err.message}`);
this.mTextReaderCall?.onInitFail(JSON.stringify(err));
}
}
/**
* 文本朗读播放接口(不显示字幕全屏面板和吸边小面板,直接朗读文本)
* @param content 实时朗读的正文信息(长度10000字符以内)
*/
public async startContent(context: Context, content: string) {
await this.initReader(context);
let readInfoList: TextReader.ReadInfo[] = [{
id: '0',
title: {
text: '',
isClickable: true
},
bodyInfo: content
}];
this.mCurrentReadInfo = readInfoList[0];
await TextReader.start(readInfoList, this.mCurrentReadInfo.id);
TextReader.hidePanel();
}
/**
* 启动朗读
* @param infoArr
*/
public async start(infoArr: TextReaderInfo[]) {
// 判断当前是否初始化成功过
if (!this.mInit) {
console.error(this.TAG, "start error ! mInit false !");
this.mTextReaderCall?.onFail(TextReaderFail.UnInit);
return;
}
if (!infoArr) {
console.error(this.TAG, "start error ! infoArr null !");
this.mTextReaderCall?.onFail(TextReaderFail.TextReaderInfoNULL);
return;
}
// 朗读启动配置
const startParams: TextReader.StartParams = {
isMinibarHidden: this.mTextReaderInitData?.isMinibarNeeded ?? true,
}
// 填充朗读内容
let readInfoList: TextReader.ReadInfo[] = [];
for (let index = 0; index < infoArr.length; index++) {
const info = infoArr[index];
let tempInfo: TextReader.ReadInfo = {
id: " " + index,
title: {
text: info.title,
isClickable: true,
},
bodyInfo: info.content,
date: {
text: info.author ?? "",
isClickable: true,
},
author: {
text: info.author ?? "",
isClickable: true,
}
}
readInfoList.push(tempInfo);
}
this.mCurrentReadInfo = readInfoList[0];
// 启动朗读
await TextReader.start(readInfoList, this.mCurrentReadInfo.id, startParams);
}
}
