学习例程6.2:HarmonyOS 鸿蒙Next 一个带语音的故事板(优化并添加注释)

学习例程6.2:HarmonyOS 鸿蒙Next 一个带语音的故事板(优化并添加注释) 对《学习例程6.1》的代码优化,并添加注释。

// 导入媒体模块,用于音频播放
import { media } from "@kit.MediaKit";
// 导入Ability模块,用于获取上下文
import { common } from "@kit.AbilityKit";

// 音频播放服务类,封装音频播放逻辑
class AudioPlayerService {
  private avPlayer: media.AVPlayer | null = null; // AVPlayer实例,用于播放音频

  // 播放音频文件
  async play(soundRaw: string, onComplete: () => void, onError: (error: string) => void) {
    try {
      // 如果已有AVPlayer实例,先释放资源
      if (this.avPlayer) {
        this.avPlayer.release();
      }

      // 创建AVPlayer实例
      this.avPlayer = await media.createAVPlayer();
      // 监听状态变化
      this.avPlayer.on('stateChange', async (state: string) => {
        switch (state) {
          case 'initialized': // 初始化完成
            this.avPlayer!.prepare(); // 准备播放
            break;
          case 'prepared': // 准备完成
            this.avPlayer!.play(); // 开始播放
            break;
          case 'completed': // 播放完成
            onComplete(); // 执行完成回调
            this.avPlayer!.release(); // 释放资源
            this.avPlayer = null; // 清空实例
            break;
          case 'error': // 播放错误
            onError('音频播放失败'); // 执行错误回调
            break;
        }
      });

      // 获取上下文并加载音频文件
      let context = getContext(this) as common.UIAbilityContext;
      let fileDescriptor = await context.resourceManager.getRawFd(soundRaw);
      let avFileDescriptor: media.AVFileDescriptor = {
        fd: fileDescriptor.fd, // 文件描述符
        offset: fileDescriptor.offset, // 文件偏移量
        length: fileDescriptor.length, // 文件长度
      };
      this.avPlayer.fdSrc = avFileDescriptor; // 设置音频源
    } catch (error) {
      onError(`音频加载失败: ${error}`); // 捕获并处理错误
    }
  }
}

// 角色类,表示故事中的角色
@ObservedV2 // 标记为可观察类,用于UI更新
export class Role {
  public name: string; // 角色名称
  public introduce: string; // 角色介绍
  public headPicture: Resource | string; // 角色头像

  constructor(
    name: string,
    introduce: string,
    headPicture: Resource | string
  ) {
    this.name = name;
    this.introduce = introduce;
    this.headPicture = headPicture;
  }
}

// 故事板单元类,表示故事中的一个单元(如一段对话)
@ObservedV2 // 标记为可观察类,用于UI更新
export class StoryBoardUnit {
  public name: string; // 单元名称
  public role: Role; // 关联的角色
  public words: string; // 对话内容
  public sound: string; // 音频文件路径
  public picture: Resource | string; // 单元图片

  constructor(
    name: string,
    role: Role,
    words: string,
    sound: string,
    picture: Resource | string = ""
  ) {
    this.name = name;
    this.role = role;
    this.words = words;
    this.sound = sound;
    this.picture = picture;
  }
}

// 故事板帧类,表示故事中的一个帧(包含多个单元)
@ObservedV2 // 标记为可观察类,用于UI更新
export class StoryBoardFrame {
  public units: StoryBoardUnit[]; // 包含的故事单元
  public backPicture: Resource | string; // 背景图片

  constructor(
    units: StoryBoardUnit[] = [],
    backPicture: Resource | string = ""
  ) {
    this.units = units;
    this.backPicture = backPicture;
  }
}

// 逐字显示组件,用于逐字显示对话内容
@ComponentV2 // 标记为组件
export struct ViewTxtShow {
  @Param Words: string = ""; // 接收外部传入的对话内容
  @Local private wordList: string[] = []; // 将对话内容拆分为字符数组
  @Local private wordOpacity: number[] = []; // 每个字符的透明度

  // 组件加载时调用
  aboutToAppear(): void {
    this.updateWordList(); // 初始化字符数组
    this.animateText(); // 开始逐字显示动画
  }

  // 将对话内容拆分为字符数组
  private updateWordList() {
    this.wordList = Array.from(this.Words); // 将字符串转换为字符数组
    this.wordOpacity = new Array(this.wordList.length).fill(0.3); // 初始化透明度数组
  }

  // 逐字显示动画
  private animateText(idx: number = 0) {
    if (idx >= this.wordList.length) {
      return; // 如果所有字符都已显示,结束动画
    }

    setTimeout(() => {
      this.wordOpacity[idx] = 1; // 设置当前字符的透明度为1(完全显示)
      this.animateText(idx + 1); // 递归显示下一个字符
    }, 100); // 每100毫秒显示一个字符
  }

  // 构建UI
  build() {
    Grid() {
      ForEach(this.wordList, (char: string, idx: number) => {
        GridItem() {
          Text(this.Words[idx]) // 显示字符
            .opacity(this.wordOpacity[idx]) // 设置透明度
            .animation({ curve: Curve.Ease }) // 添加动画效果
            .fontColor('white') // 设置字体颜色
        }
      }, (item: string, idx: number) => idx.toString()); // 使用索引作为键值
    }
    .direction(Direction.Auto) // 设置布局方向
    .width("100%"); // 设置宽度
  }
}

// 故事板主组件,用于展示故事板内容
@ComponentV2 // 标记为组件
export struct ViewStoryBoard {
  @Local private nowStoryFrame: StoryBoardFrame | undefined = undefined; // 当前故事帧
  @Local private nowStoryUnit: StoryBoardUnit | undefined = undefined; // 当前故事单元
  @Local private headOpacity: number = 0.1; // 角色头像的透明度
  private audioPlayer: AudioPlayerService = new AudioPlayerService(); // 音频播放服务实例

  // 组件加载时调用
  aboutToAppear(): void {
    this.initializeStory(); // 初始化故事内容
  }

  // 初始化故事内容
  private initializeStory() {
    this.nowStoryFrame = new StoryBoardFrame(
      [
        new StoryBoardUnit(
          "曹操说",
          new Role("曹操", "魏国丞相", $r("app.media.caocaoHeadRight")),
          "呵呵!吾不笑别人,单笑周瑜无谋,诸葛亮少智。\n若是吾用兵之时,预先在这里伏下一军,如之奈何?",
          "caocao_01.mp3"
        ),
        new StoryBoardUnit(
          "赵云说",
          new Role("赵云", "蜀国大将", $r("app.media.zhaoyun_head")),
          "我赵子龙奉军师将令,在此等候多时了!",
          "zhaoyun_01.mp3"
        ),
      ],
      $r("app.media.huarongdao") // 设置背景图片
    );

    this.playUnit(0); // 开始播放第一个单元
  }

  // 播放指定索引的故事单元
  private async playUnit(idx: number) {
    if (!this.nowStoryFrame || idx >= this.nowStoryFrame.units.length) {
      return; // 如果索引无效,直接返回
    }

    this.nowStoryUnit = this.nowStoryFrame.units[idx]; // 设置当前单元
    this.headOpacity = 0.1; // 初始化头像透明度

    if (this.nowStoryUnit.sound) {
      // 播放音频
      await this.audioPlayer.play(
        this.nowStoryUnit.sound,
        () => this.playUnit(idx + 1), // 播放完成后播放下一个单元
        (error) => console.error(error) // 错误处理
      );
    }

    this.headOpacity = 1; // 显示角色头像
  }

  // 构建UI
  build() {
    Stack() {
      Image(this.nowStoryFrame?.backPicture) // 显示背景图片
        .opacity(1);

      Stack() {
        Row()
          .width('100%')
          .height(80)
          .backgroundColor('black')
          .opacity(0.8);

        Row() {
          Column() {
            Image(this.nowStoryUnit?.role?.headPicture) // 显示角色头像
              .width("20%")
              .backgroundColor("#aaaaaa")
              .borderRadius('50%')
              .animation({ curve: Curve.Ease })
              .opacity(this.headOpacity);
            Text(this.nowStoryUnit?.role?.name) // 显示角色名称
              .fontColor("#aabbcc")
              .fontSize(10);
          }

          if (this.nowStoryUnit?.words) {
            ViewTxtShow({ Words: this.nowStoryUnit.words }) // 显示对话内容
              .width('80%')
              .margin(5);
          }
        }
      }
      .width('100%')
      .alignContent(Alignment.Start);
    }
    .width('100%')
    .height('300')
    .alignContent(Alignment.BottomStart);
  }
}

// 入口组件
@ComponentV2 // 标记为组件
@Entry // 标记为入口组件
export struct TestStoryBoard {
  build() {
    Column() {
      ViewStoryBoard(); // 加载故事板主组件
    }
  }
}

优化/注释 by DeepSeek


更多关于学习例程6.2:HarmonyOS 鸿蒙Next 一个带语音的故事板(优化并添加注释)的实战教程也可以访问 https://www.itying.com/category-93-b0.html

1 回复

更多关于学习例程6.2:HarmonyOS 鸿蒙Next 一个带语音的故事板(优化并添加注释)的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


HarmonyOS鸿蒙Next的带语音故事板例程6.2主要涉及ArkUI框架的使用,通过声明式UI和状态管理实现交互式界面。故事板功能通过@State@Prop装饰器管理状态,@Builder用于构建可复用的UI组件。语音功能通过@ohos.multimedia.audio模块实现,包括音频播放和语音识别。代码结构分为视图层和逻辑层,视图层使用ArkTS编写,逻辑层处理数据和业务逻辑。注释部分解释了关键代码的作用,如状态管理、UI构建和音频处理流程。优化部分包括性能提升和代码简洁性改进。

回到顶部