【HarmonyOS AI赋能】朗读控件详解

【HarmonyOS AI赋能】朗读控件详解

一、前言

鸿蒙系统提供了系统级别的朗读控件,来实现对文本进行朗读的业务需求。不需要复杂的SDK接入和集成,就可实现商业级别的朗读效果。

朗读控件分为听筒组件朗读控制器 ,以及朗读面板 三部分组成。

朗读面板又分为吸边小面板全屏朗读面板

需要注意的是,仅支持中国境内(不包含中国香港、中国澳门、中国台湾)提供服务。并且实时朗读的正文信息长度10000字符 以内。

二、如何使用朗读控件?

以下代码为上图所示的DEMO源码,可直接新建工程后,贴到index.ets类中,启自动签名后,启动查看效果。下面为大家详细拆解如何使用。

typescript 复制代码
// 导入语音朗读相关的组件和类型
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

提供的听筒控件,可以同步朗读状态,如上动态图所示,有现成的朗读效果,如果业务需要使用,可以用。或者直接跳过也可以,控件参数比较简单,如下代码所示:

typescript 复制代码
 TextReaderIcon({ readState: this.readState })
        .width(32)
        .height(32)
        .onClick(async () => {
					// do something...
        })

readState 需要通过朗读控制器TextReader去监听,当前的朗读状态,然后设置给朗读控件,就可以实现朗读控件的动态效果。

typescript 复制代码
    TextReader.on('stateChange', (state: TextReader.ReadState) => {
			
    });

并且根据DEMO代码可发现,听筒控件的点击事件,触发了朗读控制器对象的开启操作。

综上所述,我们可以不使用话筒控件,直接使用朗读控制器,调用其接口实现文本朗读的效果。

(2)朗读控制器TextReader

TextReader是整个朗读操作逻辑的核心操作对象,系统接口提供了该单例对象。使用之前需要先初始化:

typescript 复制代码
    // 朗读参数配置
    const readerParam: TextReader.ReaderParam = {
      isVoiceBrandVisible: true, // 显示品牌信息
      businessBrandInfo: {
        panelName:  '朗读', // 面板名称
      },
      isMinibarNeeded: true
    }
    await TextReader.init(context, readerParam);

然后再进行常规的启动,暂停(pause),销毁暂停(stop)【ps: 我现在对系统接口,这种类似双暂停的命名很无语 = =。猛地看起来,两个暂停,傻傻分不清楚。但是目前stop后者,多用于整个生命周期回收重置的调用处理。】:

typescript 复制代码
 // 朗读启动配置
    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接口:

typescript 复制代码
    TextReader.on('stateChange', (state: TextReader.ReadState) => {

    });

(3)朗读面板

关于朗读面板,我理解是通过子窗口来实现,吸边小面板和全屏面板的效果。因为文档中有强调使用朗读控件初始化前,需要使用windowManager进行舞台窗口对象的注入(
WindowManager.setWindowStage(windowStage);):

typescript 复制代码
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;
      }
    });
  }

}

但是在实际使用中我发现,即使不调用该注入方法。初始化也不会报错。目前已提交工单,后续结论同步到该文章。

朗读面板的操作逻辑很简单,分别提供了显示和隐藏两个面板(吸边小面板和全屏面板)的属性或者接口,来控制显隐。

吸边小面板可通过属性和方法分别设置显隐:

typescript 复制代码
首先是在初始化配置参数中:
    const readerParam: TextReader.ReaderParam = {
      isMinibarNeeded:  true
    }
    
其次是在启动配置参数中:
    const startParams: TextReader.StartParams = {
      isMinibarHidden:  true,
    }
    
再之后就是方法接口:
    TextReader.showMinibar();
    TextReader.hideMinibar();

全屏朗读面板默认启动朗读后就会显示,系统提供了两套接口,可以在start后调用hide就可隐藏全屏朗读面板:

typescript 复制代码
    TextReader.hidePanel();
    TextReader.showPanel();
typescript 复制代码
    await TextReader.start(readInfoList, this.mCurrentReadInfo.id);
    TextReader.hidePanel();

三、工具类封装源码共享:

封装ReaderIconView朗读图标,联动管理类的朗读状态,即插即用。

typescript 复制代码
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%")
  }

}

封装单例朗读管理类,用于便捷操作朗读相关接口,封装细节,方便快速调用:

typescript 复制代码
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);
  }
}
相关推荐
音视频牛哥17 分钟前
鸿蒙NEXT如何接入GB28181平台?SmartMediaKit 设备接入集成实践
华为·harmonyos·鸿蒙next gb28181·鸿蒙gb28181设备对接·鸿蒙next对接gb28181·鸿蒙gb28181实时回传·鸿蒙next 28181对接
俊哥V1 小时前
每日 AI 研究简报 · 2026-05-15
人工智能·ai
沉浸式学习ing1 小时前
B站视频怎么快速总结?AI自动生成要点+思维导图+逐字稿
人工智能·ai·自然语言处理·音视频·语音识别·notion
KKei16381 小时前
Flutter for OpenHarmony 学习视频播放器技术文章
学习·flutter·华为·音视频·harmonyos
条tiao条2 小时前
鸿蒙 ArkTS 实战进阶:组件复用三剑客与状态管理一篇通
华为·harmonyos
MatrixOrigin2 小时前
什么是AI Native的组织,它该具备什么样的特点
人工智能·ai·opc
KKei16382 小时前
Flutter for OpenHarmony 健身计划与运动打卡APP
flutter·华为·harmonyos
HwJack203 小时前
HarmonyOS APP开发中userAuthIcon 统一认证控件的原理与实战破局
华为·harmonyos
KKei16383 小时前
Flutter for OpenHarmony 在线考试与自测系统APP技术文章
flutter·华为·harmonyos
踏着七彩祥云的小丑3 小时前
AI——Dify常见报错与排查
人工智能·ai