项目源码地址

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

https://gitcode.com/qq_33681891/NovelReader

效果演示

前言

在移动设备上阅读电子书时,沉浸式的阅读体验对用户至关重要。一个好的阅读应用不仅需要提供流畅的翻页效果,还需要创造一个不受干扰的阅读环境。本教程将详细介绍如何在HarmonyOS应用中实现沉浸式阅读体验,包括全屏模式、状态栏控制、事件订阅等功能。

沉浸式阅读体验的核心要素

一个优秀的沉浸式阅读体验应包含以下核心要素:

  1. 全屏模式:隐藏系统状态栏和导航栏,最大化阅读区域
  2. 自适应界面:根据用户操作智能显示或隐藏菜单
  3. 事件响应:处理系统返回事件,提供平滑的退出体验
  4. 个性化设置:允许用户调整字体大小、背景颜色等阅读参数
  5. 状态保存:记住用户的阅读位置和偏好设置

实现全屏沉浸式模式

在HarmonyOS中,我们可以通过windowAPI来实现全屏沉浸式模式:

setSystemBarHidden() {
  window.getLastWindow(this.context).then((data: window.Window) => {
    let windowClass = data;
    // 设置沉浸式全屏
    windowClass.setWindowLayoutFullScreen(true)
      .then(() => {
        // 设置导航栏,状态栏不可见
        windowClass.setWindowSystemBarEnable([]);
        this.registerEmitter(windowClass);
      })
  });
}

这段代码首先获取当前窗口,然后设置窗口为全屏模式,并隐藏系统状态栏和导航栏。这样用户就能获得最大的阅读区域,不受系统UI的干扰。

处理系统返回事件

在沉浸式模式下,我们需要正确处理系统返回事件,确保用户可以平滑地退出阅读界面。HarmonyOS提供了事件订阅机制来实现这一功能:

/*
 * 添加事件订阅
 */
registerEmitter(windowClass: window.Window) {
  // 定义返回主页时发送的事件id
  let innerEvent: emitter.InnerEvent = { eventId: 2 };
  emitter.on(innerEvent, (data: emitter.EventData) => {
    // 收到返回事件,显示状态栏和导航栏,退出全屏模式,再返回主页
    if (data?.data?.backPressed) {
      windowClass.setWindowSystemBarEnable(['status', 'navigation'])
        .then(() => {
          if (this.popPage) {
            this.popPage();
          } else {
            // 未传入返回接口时给出弹框提示
            promptAction.showToast({
              message: $r('app.string.pageflip_back_error_message'),
              duration: 1000
            })
          }
        });
    }
  })
}

/*
 * 取消事件订阅
 */
deleteEmitter() {
  emitter.off(1);
}

在这段代码中,我们订阅了系统返回事件。当用户点击返回按钮时,我们首先恢复状态栏和导航栏的显示,然后退出全屏模式,最后返回上一页面。这样可以确保用户体验的连贯性。

生命周期管理

在组件的生命周期中,我们需要在适当的时机设置全屏模式和注册事件监听:

aboutToAppear(): void {
  this.setSystemBarHidden();
  this.initResourceData();
}

aboutToDisappear(): void {
  this.deleteEmitter();
}

在组件即将出现时,我们设置全屏模式并初始化数据;在组件即将消失时,我们取消事件订阅,释放资源。

实现自适应菜单

为了提供更好的沉浸式体验,我们可以实现自适应菜单,根据用户操作智能显示或隐藏:

build() {
  /**
   * 创建一个Stack组件,上下菜单通过zIndex在阅读页面之上。
   * 通过底部点击的按钮名来确定翻页方式,创建翻页组件。
   */
  Stack() {
    // 根据选择的翻页方式显示对应组件
    if (this.buttonClickedName === STRINGCONFIGURATION.LEFTRIGHTFLIPPAGENAME) {
      LeftRightPlipPage({
        isMenuViewVisible: this.isMenuViewVisible,
        isCommentVisible: this.isCommentVisible,
        currentPageNum: this.currentPageNum,
        bgColor: this.bgColor,
        isbgImage: this.isbgImage,
        textSize: this.textSize,
        readInfoList: this.readInfoList,
        selectedReadInfo: this.selectedReadInfo
      });
    } else if (this.buttonClickedName === STRINGCONFIGURATION.UPDOWNFLIPPAGENAME) {
      UpDownFlipPage({
        isMenuViewVisible: this.isMenuViewVisible,
        isCommentVisible: this.isCommentVisible,
        currentPageNum: this.currentPageNum,
        bgColor: this.bgColor,
        isbgImage: this.isbgImage,
        textSize: this.textSize,
        readInfoList: this.readInfoList,
        selectedReadInfo: this.selectedReadInfo
      });
    } else {
      CoverFlipPage({
        isMenuViewVisible: this.isMenuViewVisible,
        isCommentVisible: this.isCommentVisible,
        currentPageNum: this.currentPageNum,
        bgColor: this.bgColor,
        isbgImage: this.isbgImage,
        textSize: this.textSize,
        readInfoList: this.readInfoList,
        selectedReadInfo: this.selectedReadInfo
      });
    }
    
    // 菜单视图
    Column() {
      BottomView({
        isMenuViewVisible: this.isMenuViewVisible,
        buttonClickedName: this.buttonClickedName,
        filledName: this.filledName,
        isVisible: this.isVisible,
        isCommentVisible: this.isCommentVisible,
        bgColor: this.bgColor,
        isbgImage: this.isbgImage,
        textSize: this.textSize,
        readInfoList: this.readInfoList,
        selectedReadInfo: this.selectedReadInfo,
        currentPageNum: this.currentPageNum
      })
        .zIndex(CONFIGURATION.FLIPPAGEZINDEX)
    }
    .height($r('app.string.pageflip_full_size'))
    .justifyContent(FlexAlign.End)
    .onClick(() => {
      /**
       * 弹出上下菜单视图时,由于Column中间无组件,
       * 点击事件会被下一层的LeftRightPlipPage或UpDownFlipPage或CoverFlipPage的点击事件取代。
       */
      this.isMenuViewVisible = false;
      this.filledName = '';
      this.isVisible = false;
    })
  }
}

在这段代码中,我们使用Stack组件将阅读内容和菜单视图叠加在一起。菜单视图通过zIndex属性置于阅读内容之上,并且可以通过点击事件控制其显示或隐藏。

个性化阅读设置

为了提供更好的阅读体验,我们可以允许用户调整阅读参数,如字体大小、背景颜色等:

@Component
export struct PageFlipComponent {
  // ... 其他属性
  
  // 背景颜色
  @State bgColor: string = '#FFEFEFEF';
  // 文字字体大小
  @State textSize: number = 20;
  // 是否显示阅读背景
  @State isbgImage: boolean = false;
  // 播放文章列表
  @State readInfoList: TextReader.ReadInfo[] = [];
  @State selectedReadInfo: TextReader.ReadInfo = this.readInfoList[0];
  
  // ... 其他方法
}

这些状态变量可以通过菜单中的控件进行调整,从而实现个性化的阅读设置。

阅读位置保存与恢复

为了提供连续的阅读体验,我们需要保存和恢复用户的阅读位置:

// 初始化播放文章列表
initResourceData() {
  const context: Context = getContext(this);
  // 读取string.json中文章的数据
  try {
    let str = '';
    for(let i = CONFIGURATION.PAGEFLIPPAGESTART; i <= CONFIGURATION.PAGEFLIPPAGEEND; i++) {
      str = context.resourceManager.getStringByNameSync(STRINGCONFIGURATION.PAGEINFO + i.toString());
      // 将数据存入列表
      this.readInfoList.push(textReaderInfo(String(i), str));
    }
  } catch (error) {
    let code = (error as BusinessError).code;
    let message = (error as BusinessError).message;
    logger.error(`callback getStringByName failed, error code: ${code}, message: ${message}.`);
  }
  if(this.currentPageNum) {
    this.selectedReadInfo = this.readInfoList[this.currentPageNum - CONFIGURATION.PAGEFLIPPAGECOUNT];
  }
}

在这段代码中,我们根据currentPageNum恢复用户的阅读位置。在实际应用中,可以将currentPageNum保存到持久化存储中,以便在用户下次打开应用时恢复阅读位置。

错误处理与日志记录

在开发过程中,合理的错误处理和日志记录可以帮助我们快速定位和解决问题:

try {
  let str = '';
  for(let i = CONFIGURATION.PAGEFLIPPAGESTART; i <= CONFIGURATION.PAGEFLIPPAGEEND; i++) {
    str = context.resourceManager.getStringByNameSync(STRINGCONFIGURATION.PAGEINFO + i.toString());
    // 将数据存入列表
    this.readInfoList.push(textReaderInfo(String(i), str));
  }
} catch (error) {
  let code = (error as BusinessError).code;
  let message = (error as BusinessError).message;
  logger.error(`callback getStringByName failed, error code: ${code}, message: ${message}.`);
}

在这段代码中,我们使用try-catch块捕获可能的异常,并使用logger记录错误信息,方便后续调试和问题排查。

性能优化建议

在实现沉浸式阅读体验时,需要注意以下性能优化点:

  1. 减少不必要的重绘:只在必要时更新UI,避免频繁重绘
  2. 延迟加载:非关键内容可以延迟加载,优先保证主要阅读内容的显示
  3. 资源释放:在组件销毁时及时释放资源,避免内存泄漏
  4. 事件节流:对于频繁触发的事件(如滚动、缩放)进行节流处理
  5. 异步处理:耗时操作放在异步线程中处理,避免阻塞主线程

适配不同设备

为了提供一致的用户体验,我们需要适配不同尺寸和方向的设备:

private screenW: number = px2vp(display.getDefaultDisplaySync().width);

通过获取设备的实际宽度,我们可以根据不同设备的尺寸调整UI布局和交互行为,确保在各种设备上都能提供良好的阅读体验。

总结

本教程详细介绍了如何在HarmonyOS应用中实现沉浸式阅读体验,包括全屏模式、状态栏控制、事件订阅、自适应菜单、个性化设置等功能。通过这些技术,我们可以为用户提供一个专注、舒适的阅读环境,提升用户的阅读体验。


全栈若城
1 声望2 粉丝