【HarmonyOS AI赋能】朗读控件详解
一、前言
鸿蒙系统提供了系统级别的朗读控件,来实现对文本进行朗读的业务需求。不需要复杂的SDK接入和集成,就可实现商业级别的朗读效果。
朗读控件分为听筒组件和朗读控制器,以及朗读面板三部分组成。 朗读面板又分为吸边小面板和全屏朗读面板。
需要注意的是,仅支持中国境内(不包含中国香港、中国澳门、中国台湾)提供服务。并且实时朗读的正文信息长度10000字符以内。
二、如何使用朗读控件?
以下代码为上图所示的DEMO源码,可直接新建工程后,贴到index.ets类中,启自动签名后,启动查看效果。下面为大家详细拆解如何使用。
1// 导入语音朗读相关的组件和类型 2import { TextReader, TextReaderIcon, ReadStateCode } from '@kit.SpeechKit'; 3 4@Entry 5@Component 6struct Index { 7 8 /** 9 * 待加载的文章列表 10 */ 11 @State readInfoList: TextReader.ReadInfo[] = []; 12 13 /** 14 * 当前选中的文章 15 */ 16 @State selectedReadInfo: TextReader.ReadInfo = this.readInfoList[0]; 17 18 /** 19 * 朗读状态 20 */ 21 @State readState: ReadStateCode = ReadStateCode.WAITING; 22 23 /** 24 * 初始化状态标记 25 */ 26 @State isInit: boolean = false; 27 28 // 组件即将显示时触发 29 async aboutToAppear(){ 30 /** 31 * 模拟加载文章数据 32 */ 33 let readInfoList: TextReader.ReadInfo[] = [{ 34 id: '001', 35 title: { 36 text:'水调歌头.明月几时有', 37 isClickable:true 38 }, 39 author:{ 40 text:'宋.苏轼', 41 isClickable:true 42 }, 43 date: { 44 text:'2024/01/01', 45 isClickable:false 46 }, 47 bodyInfo: '明月几时有?把酒问青天。不知天上宫阙,今夕是何年?' 48 }]; 49 50 // 更新状态变量 51 this.readInfoList = readInfoList; 52 this.selectedReadInfo = this.readInfoList[0]; 53 54 // 初始化朗读组件 55 this.init(); 56 } 57 58 /** 59 * 初始化朗读组件 60 */ 61 async init() { 62 // 朗读参数配置 63 const readerParam: TextReader.ReaderParam = { 64 isVoiceBrandVisible: true, // 显示品牌信息 65 businessBrandInfo: { 66 panelName: '小艺朗读', // 面板名称 67 panelIcon: $r('app.media.startIcon') // 面板图标 68 } 69 } 70 71 try { 72 // 获取上下文 73 let context: Context | undefined = this.getUIContext().getHostContext() 74 if (context) { 75 // 初始化朗读组件 76 await TextReader.init(context, readerParam); 77 this.isInit = true; // 标记初始化完成 78 this.setActionListener(); // 设置事件监听 79 } 80 } catch (err) { 81 // 初始化失败时打印错误信息 82 console.error([`TextReader failed to init. Code: ${err.code}, message: ${err.message}`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.code.md)); 83 } 84 } 85 86 // 设置朗读事件监听 87 setActionListener() { 88 // 监听朗读状态变化 89 TextReader.on('stateChange', (state: TextReader.ReadState) => { 90 this.onStateChanged(state); 91 }); 92 93 // 监听加载更多请求 94 TextReader.on('requestMore', () => { 95 TextReader.loadMore([], true); 96 }) 97 } 98 99 // 处理朗读状态变化 100 onStateChanged = (state: TextReader.ReadState) => { 101 // 只处理当前选中文章的状态变化 102 if (this.selectedReadInfo?.id === state.id) { 103 this.readState = state.state; 104 } else { 105 this.readState = ReadStateCode.WAITING; 106 } 107 } 108 109 // 构建UI界面 110 build() { 111 Column() { 112 // 朗读状态图标 113 TextReaderIcon({ readState: this.readState }) 114 .margin({ right: 20 }) 115 .width(32) 116 .height(32) 117 .onClick(async () => { 118 // 点击图标时开始朗读 119 try { 120 await TextReader.start(this.readInfoList, this.selectedReadInfo?.id); 121 } catch (err) { 122 // 朗读失败时打印错误信息 123 console.error([`TextReader failed to start. Code: ${err.code}, message: ${err.message}`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.code.md)); 124 } 125 }) 126 } 127 .height('100%') 128 } 129} 130
(1)听筒控件TextReaderIcon提供的听筒控件,可以同步朗读状态,如上动态图所示,有现成的朗读效果,如果业务需要使用,可以用。或者直接跳过也可以,控件参数比较简单,如下代码所示:
1 TextReaderIcon({ readState: this.readState }) 2 .width(32) 3 .height(32) 4 .onClick(async () => { 5 // do something... 6 }) 7
readState 需要通过朗读控制器TextReader去监听,当前的朗读状态,然后设置给朗读控件,就可以实现朗读控件的动态效果。
1 TextReader.on('stateChange', (state: TextReader.ReadState) => { 2 3 }); 4
并且根据DEMO代码可发现,听筒控件的点击事件,触发了朗读控制器对象的开启操作。
综上所述,我们可以不使用话筒控件,直接使用朗读控制器,调用其接口实现文本朗读的效果。
(2)朗读控制器TextReaderTextReader是整个朗读操作逻辑的核心操作对象,系统接口提供了该单例对象。使用之前需要先初始化:
1 // 朗读参数配置 2 const readerParam: TextReader.ReaderParam = { 3 isVoiceBrandVisible: true, // 显示品牌信息 4 businessBrandInfo: { 5 panelName: '朗读', // 面板名称 6 }, 7 isMinibarNeeded: true 8 } 9 await TextReader.init(context, readerParam); 10
然后再进行常规的启动,暂停(pause),销毁暂停(stop)【ps: 我现在对系统接口,这种类似双暂停的命名很无语 = =。猛地看起来,两个暂停,傻傻分不清楚。但是目前stop后者,多用于整个生命周期回收重置的调用处理。】:
1 // 朗读启动配置 2 const startParams: TextReader.StartParams = { 3 isMinibarHidden: this.mTextReaderInitData?.isMinibarNeeded ?? true, 4 } 5 // 填充朗读内容 6 let readInfoList: TextReader.ReadInfo[] = [{ 7 id: '002', 8 title: { 9 text:'水调歌头.明月几时有2', 10 isClickable:true 11 }, 12 author:{ 13 text:'宋.苏轼2', 14 isClickable:true 15 }, 16 date: { 17 text:'2025/02/02', 18 isClickable:false 19 }, 20 bodyInfo: '2明月几时有?把酒问青天。不知天上宫阙,今夕是何年?' 21 }]; 22 // 启动朗读 23 await TextReader.start(readInfoList, this.readInfoList[0].id, startParams); 24
再之后进行根据业务需求,做一些监听和反监听的处理了,种类很多详情参见api接口:
1 TextReader.on('stateChange', (state: TextReader.ReadState) => { 2 3 }); 4
(3)朗读面板关于朗读面板,我理解是通过子窗口来实现,吸边小面板和全屏面板的效果。因为文档中有强调使用朗读控件初始化前,需要使用windowManager进行舞台窗口对象的注入(WindowManager.setWindowStage(windowStage);):
1import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit'; 2import { window } from '@kit.ArkUI'; 3import { WindowManager } from '@kit.SpeechKit'; 4 5 6export default class EntryAbility extends UIAbility { 7 onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { 8 this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET); 9 10 } 11 12 onWindowStageCreate(windowStage: window.WindowStage): void { 13 14 WindowManager.setWindowStage(windowStage); 15 windowStage.loadContent('pages/Index', (err) => { 16 if (err.code) { 17 return; 18 } 19 }); 20 } 21 22} 23
但是在实际使用中我发现,即使不调用该注入方法。初始化也不会报错。目前已提交工单,后续结论同步到该文章。
朗读面板的操作逻辑很简单,分别提供了显示和隐藏两个面板(吸边小面板和全屏面板)的属性或者接口,来控制显隐。
吸边小面板可通过属性和方法分别设置显隐:
1首先是在初始化配置参数中: 2 const readerParam: TextReader.ReaderParam = { 3 isMinibarNeeded: true 4 } 5 6其次是在启动配置参数中: 7 const startParams: TextReader.StartParams = { 8 isMinibarHidden: true, 9 } 10 11再之后就是方法接口: 12 TextReader.showMinibar(); 13 TextReader.hideMinibar(); 14
全屏朗读面板默认启动朗读后就会显示,系统提供了两套接口,可以在start后调用hide就可隐藏全屏朗读面板:
1 TextReader.hidePanel(); 2 TextReader.showPanel(); 3
1 await TextReader.start(readInfoList, this.mCurrentReadInfo.id); 2 TextReader.hidePanel(); 3
三、工具类封装源码共享:
封装ReaderIconView朗读图标,联动管理类的朗读状态,即插即用。
1import { ReadStateCode, TextReaderIcon } from "@kit.SpeechKit"; 2import { TextReaderMgr, TextReaderRegister } from "../mgr/TextReaderMgr"; 3import { common } from "@kit.AbilityKit"; 4 5@Component 6export struct ReaderIconView { 7 private TAG: string = "ReaderIconView"; 8 9 /** 10 * 朗读状态 11 */ 12 @State readState: ReadStateCode = ReadStateCode.WAITING; 13 14 private mTextReaderRegister: TextReaderRegister = { 15 onStateChange: (state: ReadStateCode): void => { 16 this.readState = state; 17 console.log(this.TAG, "mTextReaderRegister onStateChange state: " + state); 18 } 19 } 20 21 aboutToAppear(): void { 22 const context = getContext(this) as common.UIAbilityContext; 23 TextReaderMgr.Ins().initReader(context, null, null, this.mTextReaderRegister); 24 console.log(this.TAG, " aboutToAppear initReader done"); 25 } 26 27 28 build() { 29 TextReaderIcon({ readState: this.readState }) 30 .width("100%") 31 .height("100%") 32 } 33 34} 35
封装单例朗读管理类,用于便捷操作朗读相关接口,封装细节,方便快速调用:
1import { ReadStateCode, TextReader } from "@kit.SpeechKit"; 2 3/** 4 * 初始化配置对象 5 */ 6export class TextReaderInitData { 7 // 全屏面板标题名称 8 panelName: string = ""; 9 // 是否需要吸边小面板 10 isMinibarNeeded: boolean = true; 11 // 是否需要全屏面板 12 isPanelNeeded: boolean = true; 13} 14 15/** 16 * 控制器操作回调 17 */ 18export interface TextReaderCall { 19 onReady: () => void 20 onInitFail: (err: string) => void 21 onFail: (err: string) => void 22} 23 24/** 25 * 监听回调 26 */ 27export interface TextReaderRegister { 28 onStateChange: (state: ReadStateCode) => void 29} 30 31/** 32 * 错误码 33 */ 34export enum TextReaderFail { 35 UnInit = "0", 36 TextReaderInfoNULL = "1" 37} 38 39/** 40 * 文本朗读对象 41 */ 42export class TextReaderInfo { 43 title: string = ""; 44 content: string = ""; 45 author?: string = ""; 46 date?: string = ""; 47} 48 49/** 50 * 文本朗读管理类 51 */ 52export class TextReaderMgr { 53 private TAG: string = "TextReaderMgr"; 54 private static mTextReaderMgr: TextReaderMgr | null = null; 55 private mInit: boolean = false; 56 private mTextReaderCall: TextReaderCall | null = null; 57 private mTextReaderInitData: TextReaderInitData | null = null; 58 private mTextReaderRegister: TextReaderRegister | null = null; 59 60 private mCurrentReadInfo: TextReader.ReadInfo | null = null; 61 62 public static Ins() { 63 if (!TextReaderMgr.mTextReaderMgr) { 64 TextReaderMgr.mTextReaderMgr = new TextReaderMgr(); 65 } 66 return TextReaderMgr.mTextReaderMgr; 67 } 68 69 /** 70 * 设置朗读事件监听 71 */ 72 private setActionListener() { 73 // 监听朗读状态变化 74 TextReader.on('stateChange', (state: TextReader.ReadState) => { 75 let readState: ReadStateCode = ReadStateCode.WAITING; 76 if (this.mCurrentReadInfo?.id === state.id) { 77 readState = state.state; 78 } else { 79 readState = ReadStateCode.WAITING; 80 } 81 this.mTextReaderRegister?.onStateChange(readState); 82 }); 83 84 // 监听加载更多请求 85 TextReader.on('requestMore', (callbackStr) => { 86 console.log(this.TAG, " callbackStr: " + callbackStr); 87 let readInfoList: TextReader.ReadInfo[] = [{ 88 id: '002', 89 title: { 90 text: '水调歌头.明月几时有2', 91 isClickable: true 92 }, 93 author: { 94 text: '宋.苏轼2', 95 isClickable: true 96 }, 97 date: { 98 text: '2025/02/02', 99 isClickable: false 100 }, 101 bodyInfo: '2明月几时有?把酒问青天。不知天上宫阙,今夕是何年?' 102 }]; 103 TextReader.loadMore(readInfoList, true); 104 }) 105 } 106 107 /** 108 * 初始化朗读播放控件 109 */ 110 public async initReader(context: Context, callback?: TextReaderCall | null, data?: TextReaderInitData | null, 111 register?: TextReaderRegister) { 112 this.mTextReaderCall = callback ?? null; 113 this.mTextReaderInitData = data ?? null; 114 this.mTextReaderRegister = register ?? null; 115 116 // 朗读参数配置 117 const readerParam: TextReader.ReaderParam = { 118 isVoiceBrandVisible: data?.panelName == "" ? false : true ?? true, // 显示品牌信息 119 businessBrandInfo: { 120 panelName: data?.panelName == "" ? '朗读' : data?.panelName ?? '朗读', // 面板名称 121 }, 122 isMinibarNeeded: data?.isMinibarNeeded ?? true 123 } 124 125 try { 126 if (context) { 127 // 初始化朗读组件 128 await TextReader.init(context, readerParam); 129 this.mInit = true; // 标记初始化完成 130 this.setActionListener(); // 设置事件监听 131 this.mTextReaderCall?.onReady(); 132 } 133 } catch (err) { 134 // 初始化失败时打印错误信息 135 console.error(this.TAG, [`TextReader failed to init. Code: ${err.code}, message: ${err.message}`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.code.md)); 136 this.mTextReaderCall?.onInitFail(JSON.stringify(err)); 137 } 138 } 139 140 /** 141 * 文本朗读播放接口(不显示字幕全屏面板和吸边小面板,直接朗读文本) 142 * @param content 实时朗读的正文信息(长度10000字符以内) 143 */ 144 public async startContent(context: Context, content: string) { 145 await this.initReader(context); 146 let readInfoList: TextReader.ReadInfo[] = [{ 147 id: '0', 148 title: { 149 text: '', 150 isClickable: true 151 }, 152 bodyInfo: content 153 }]; 154 this.mCurrentReadInfo = readInfoList[0]; 155 await TextReader.start(readInfoList, this.mCurrentReadInfo.id); 156 TextReader.hidePanel(); 157 } 158 159 /** 160 * 启动朗读 161 * @param infoArr 162 */ 163 public async start(infoArr: TextReaderInfo[]) { 164 // 判断当前是否初始化成功过 165 if (!this.mInit) { 166 console.error(this.TAG, "start error ! mInit false !"); 167 this.mTextReaderCall?.onFail(TextReaderFail.UnInit); 168 return; 169 } 170 if (!infoArr) { 171 console.error(this.TAG, "start error ! infoArr null !"); 172 this.mTextReaderCall?.onFail(TextReaderFail.TextReaderInfoNULL); 173 return; 174 } 175 // 朗读启动配置 176 const startParams: TextReader.StartParams = { 177 isMinibarHidden: this.mTextReaderInitData?.isMinibarNeeded ?? true, 178 } 179 // 填充朗读内容 180 let readInfoList: TextReader.ReadInfo[] = []; 181 for (let index = 0; index < infoArr.length; index++) { 182 const info = infoArr[index]; 183 let tempInfo: TextReader.ReadInfo = { 184 id: " " + index, 185 title: { 186 text: info.title, 187 isClickable: true, 188 }, 189 bodyInfo: info.content, 190 date: { 191 text: info.author ?? "", 192 isClickable: true, 193 }, 194 author: { 195 text: info.author ?? "", 196 isClickable: true, 197 } 198 } 199 readInfoList.push(tempInfo); 200 } 201 this.mCurrentReadInfo = readInfoList[0]; 202 // 启动朗读 203 await TextReader.start(readInfoList, this.mCurrentReadInfo.id, startParams); 204 } 205} 206
《【HarmonyOS AI赋能】朗读控件详解》 是转载文章,点击查看原文。