HarmonyOS NEXT 小说阅读器应用系列教程之底部菜单栏实现教程

0 阅读4分钟

项目源码地址

项目源码已发布到GitCode平台, 方便开发者进行下载和使用。

gitcode.com/qq_33681891…

效果演示

前言

在移动应用开发中,底部菜单栏是用户交互的重要组成部分,尤其对于阅读类应用来说,一个功能完善、交互友好的底部菜单栏能够极大提升用户体验。本教程将详细讲解如何在HarmonyOS应用中实现一个功能丰富的阅读应用底部菜单栏,包括翻页方式选择、界面设置等功能。

技术要点

本教程涉及以下HarmonyOS开发技术点:

  • ArkUI组件化开发
  • 状态管理与数据同步
  • 动画效果实现
  • 事件处理与交互
  • 条件渲染与可见性控制

底部菜单栏结构设计

我们的底部菜单栏主要包含以下几个部分:

  1. 底部图标导航栏(阅读列表、自由阅读、发现、亮度调节、设置)
  2. 翻页方式选择面板(左右翻页、上下翻页、覆盖翻页)
  3. 阅读设置面板(文本朗读、字体大小、背景颜色、背景图片)
  4. 评论区入口

代码实现

1. 组件结构与状态定义

首先,我们需要定义组件的基本结构和状态变量:

@Component
export struct BottomView {
  // @Link装饰器:父子双向同步。@Link装饰的变量与其父组件中的数据源共享相同的值。
  @Link filledName: string;                // 当前选中的底部图标名称
  @Link buttonClickedName: string;         // 点击按钮的名称,用来判断是否已被点击
  @Link isVisible: boolean;                // 用来判断翻页方式视图是否显示
  @Link isCommentVisible: boolean;         // 用来判断评论视图是否显示
  @Link isMenuViewVisible: boolean;        // 用来判断上下菜单视图是否显示
  @State buttonNameList: Array<string> = [STRINGCONFIGURATION.LEFTRIGHTFLIPPAGENAME, 
                                         STRINGCONFIGURATION.UPDOWNFLIPPAGENAME, 
                                         STRINGCONFIGURATION.COVERFLIPPAGENAME]
  @Link bgColor: string;                   // 背景颜色
  @Link textSize: number;                  // 文本大小
  @State isTextReader: boolean = false;    // 文本朗读状态
  @State isTextSize: boolean = false;      // 字体大小状态
  @State isbgColor: boolean = false;       // 背景颜色状态
  @Link isbgImage: boolean;                // 背景图片状态
  @Link currentPageNum: number;            // 当前页码
  // 播放文章列表
  @Prop readInfoList: TextReader.ReadInfo[] = [];
  @Prop selectedReadInfo: TextReader.ReadInfo = this.readInfoList[0];
  // 用于显示当前页的按钮状态
  @State isInit: boolean = false;          // 初始化状态
}

这里使用了HarmonyOS的几种状态装饰器:

  • @Link:实现父子组件间的双向数据同步
  • @State:管理组件内部状态
  • @Prop:接收父组件传递的只读数据

2. 底部图标栏实现

底部图标栏是用户最常交互的区域,包含了多个功能入口:

Row() {
  Image(this.filledName === STRINGCONFIGURATION.PAGEFLIPVIEWLIST ? 
        $r('app.media.flippage_view_list_filled') : $r('app.media.flippage_view_list'))
    .width($r('app.string.pageflip_bottomview_row_image_width'))
    .height($r('app.string.pageflip_bottomview_row_image_height'))
    .objectFit(ImageFit.Contain)
    .padding($r('app.integer.flippage_padding_small'))
    .onClick(() => {
      this.clickAnimate(STRINGCONFIGURATION.PAGEFLIPVIEWLIST);
    })
  // 其他图标(自由阅读、发现、亮度、设置)的实现类似
  // ...
  Image(this.filledName === STRINGCONFIGURATION.PAGEFLIPSETTINGS ? 
        $r('app.media.flippage_settings_filled') : $r('app.media.flippage_settings'))
    .width($r('app.string.pageflip_bottomview_row_image_width'))
    .height($r('app.string.pageflip_bottomview_row_image_height'))
    .objectFit(ImageFit.Contain)
    .padding($r('app.integer.flippage_padding_small'))
    .onClick(() => {
      animateTo({
        duration: CONFIGURATION.PAGEFLIPTOASTDURATION,
        curve: Curve.Linear,
      }, () => {
        if (this.filledName === STRINGCONFIGURATION.PAGEFLIPSETTINGS) {
          this.filledName = '';
          this.isVisible = false;
          this.isCommentVisible = true;
        } else {
          this.filledName = STRINGCONFIGURATION.PAGEFLIPSETTINGS;
          this.isVisible = true;
          this.isCommentVisible = false;
        }
      });
    })
    .id('setting')
}

这里的关键点:

  1. 使用条件渲染切换图标的填充/未填充状态
  2. 为每个图标添加点击事件处理
  3. 使用动画效果增强交互体验

3. 翻页方式选择面板

翻页方式选择面板允许用户选择不同的翻页模式:

Flex({ justifyContent: FlexAlign.SpaceBetween }) {
  ForEach(this.buttonNameList, (item: string) => {
    Button(item, { type: ButtonType.Capsule })
      .backgroundColor($r('app.color.pageflip_button_backgroundcolor'))
      .fontColor(this.buttonClickedName === item ? 
                $r('app.color.pageflip_button_click_fontcolor') : 
                $r('app.color.pageflip_button_fontcolor'))
      .margin({ left: $r('app.integer.flippage_margin_small'),
                right: $r('app.integer.flippage_margin_small') })
      .borderWidth(CONFIGURATION.PAGEFLIPBORDERWIDTH)
      .onClick(() => {
        if (this.buttonClickedName !== item) {
          this.buttonClickedName = item;
          this.isMenuViewVisible = false;
          this.filledName = '';
          this.isVisible = false;
        }
      })
  }, (item: string) => item)
}
.margin({ top: $r('app.integer.flippage_margin_small'), 
          bottom: $r('app.integer.flippage_margin_small') })
.visibility(this.isVisible ? Visibility.Visible : Visibility.None)

这里的关键点:

  1. 使用ForEach循环渲染按钮列表
  2. 根据选中状态动态改变按钮文字颜色
  3. 通过visibility属性控制面板的显示和隐藏

4. 阅读设置面板

阅读设置面板提供了文本朗读、字体大小、背景颜色等功能:

Flex({ justifyContent: FlexAlign.SpaceBetween }) {
  Button($r('app.string.pageflip_button_text_reader'), { type: ButtonType.Capsule })
    .backgroundColor($r('app.color.pageflip_button_backgroundcolor'))
    .fontColor(this.isTextReader ? 
              $r('app.color.pageflip_button_click_fontcolor') : 
              $r('app.color.pageflip_button_fontcolor'))
    .margin({ left: $r('app.integer.flippage_margin_small'),
              right: $r('app.integer.flippage_margin_small') })
    .borderWidth(CONFIGURATION.PAGEFLIPBORDERWIDTH)
    .onClick( () => {
      this.setEventListener();
      if(!this.isTextReader) {
        // 朗读控件起播,拉起播放器面板并开始播放
        TextReader.showPanel();
        TextReader.start(this.readInfoList, this.selectedReadInfo?.id).then(() => {
          logger.info('TextReader succeeded in starting');
        }).catch((e: BusinessError) => {
          logger.error(`TextReader failed to start. Code: ${e.code}, message: ${e.message}`);
        })
      }
      else {
        // 朗读控件停止朗读,执行播放面板的关闭
        TextReader.stop().then(() => {
          logger.info(`item TextReader succeeded in stopping.`);
        }).catch((e: BusinessError) => {
          logger.error(`TextReader failed to stop. Code: ${e.code}, message: ${e.message}`);
        })
        TextReader.hidePanel();
      }
      this.isTextReader = !this.isTextReader;
      emitter.emit({ eventId: 0, priority: 0 }, {
        data: {
          isTextReader: this.isTextReader
        }
      })
      this.isMenuViewVisible = false;
      this.filledName = '';
      this.isVisible = false;
    })
    .id('textReading')
  
  // 字体大小和背景颜色按钮实现类似
  // ...
}
.margin({ top: $r('app.integer.flippage_margin_small'), 
          bottom: $r('app.integer.flippage_margin_small') })
.visibility(this.isVisible ? Visibility.Visible : Visibility.None)

这里的关键点:

  1. 文本朗读功能的实现,包括启动和停止朗读
  2. 使用emitter发送事件通知其他组件状态变化
  3. 使用Promise处理异步操作

5. 动画效果实现

为了增强用户体验,我们为菜单切换添加了动画效果:

clickAnimate(name: string) {
  animateTo({
    duration: CONFIGURATION.PAGEFLIPTOASTDURATION,
    curve: Curve.Linear,
  }, () => {
    if (this.filledName === name) {
      this.filledName = '';
      this.isCommentVisible = true;
    } else {
      promptAction.showToast({
        message: $r('app.string.pageflip_default_toast'),
        duration: CONFIGURATION.PAGEFLIPTOASTDURATION
      });
      this.filledName = name;
      this.isVisible = false;
      this.isCommentVisible = false;
    }
  })
}

这里的关键点:

  1. 使用animateTo实现平滑的状态过渡
  2. 根据当前状态决定动画后的界面展示
  3. 使用promptAction.showToast提供用户反馈

文本朗读功能实现

文本朗读是阅读应用的重要功能,我们使用HarmonyOS的TextReader组件实现:

1. 初始化朗读控件

async init() {
  // 设置朗读参数
  const readerParam: TextReader.ReaderParam = {
    isVoiceBrandVisible: true,
    businessBrandInfo: {
      panelName: STRINGCONFIGURATION.XIAOYIREADING
    }
  }
  try{
    // 初始化朗读控件
    await TextReader.init(getContext(this), readerParam);
    this.isInit = true;
  } catch (err) {
    logger.error(`TextReader failed to init. Code: ${err.code}, message: ${err.message}`);
  }
}

2. 设置事件监听

setEventListener(){
  TextReader.on('stop', () => {
    this.isTextReader = false;
  });

  TextReader.on('stateChange', (state: TextReader.ReadState) => {
    // 当前正在播放的文章播放完成
    if(state.state === CONFIGURATION.COMPLETED) {
      // 当朗读的为最后一页
      if(Number(state.id) === this.readInfoList.length) {
        this.currentPageNum = Number(state.id);
      } else {
        this.currentPageNum = Number(state.id) + 1;
      }
    }
  });

  TextReader.on('eventPanel', (pe: TextReader.PanelEvent) => {
    // 点击上一条按钮
    if(pe.click === 'BPC_03') {
      this.currentPageNum = Number(pe.id);
    // 点击下一条按钮
    } else if(pe.click === 'BPC_04') {
      this.currentPageNum = Number(pe.id);
    }
  });
}

最佳实践与注意事项

  1. 状态管理:合理使用@Link@State@Prop装饰器管理组件状态
  2. 条件渲染:使用三元表达式和visibility属性控制UI元素的显示和隐藏
  3. 事件处理:为UI元素添加点击事件处理函数,实现交互逻辑
  4. 动画效果:使用animateTo实现平滑的状态过渡
  5. 资源引用:使用$r引用应用资源,便于国际化和主题切换
  6. 错误处理:使用try-catch和Promise的catch方法处理可能的错误

总结

通过本教程,我们学习了如何在HarmonyOS应用中实现一个功能完善的阅读应用底部菜单栏。这个菜单栏不仅提供了基本的导航功能,还集成了翻页方式选择、文本朗读、字体大小调整、背景颜色设置等高级功能,大大提升了用户的阅读体验。