HarmonyOS NEXT 小说阅读器应用系列教程之电子书翻页效果实现

0 阅读6分钟

项目源码地址

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

gitcode.com/qq_33681891…

效果演示

前言

在移动设备上阅读电子书时,翻页效果对用户体验至关重要。一个流畅、自然的翻页动画能够让用户获得接近纸质书籍的阅读体验。本教程将详细介绍如何在HarmonyOS应用中实现三种常见的电子书翻页效果:左右翻页、上下翻页和覆盖翻页。

核心组件介绍

在实现翻页效果之前,我们需要了解以下核心组件:

  1. Swiper:用于实现左右滑动的容器组件
  2. List:用于实现上下滑动的列表组件
  3. LazyForEach:用于按需加载数据的高效渲染方式
  4. CacheCount:控制缓存数量,优化内存使用
  5. Stack:用于实现覆盖翻页效果的层叠布局

实现方案概述

我们将实现三种不同的翻页效果,每种效果都有其特定的应用场景和实现方式:

  1. 左右翻页:模拟传统书籍的横向翻页,适合大多数阅读场景
  2. 上下翻页:类似网页浏览的纵向滚动,适合长文本阅读
  3. 覆盖翻页:模拟真实书页翻动效果,提供最接近实体书的阅读体验

数据源准备

在实现翻页效果之前,我们需要准备数据源。在本例中,我们使用BasicDataSource类来管理数据:

// 初始化播放文章列表
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];
  }
}

左右翻页实现

左右翻页是最常见的电子书翻页方式,我们使用Swiper组件结合LazyForEach来实现:

核心实现步骤

  1. aboutToAppear()方法中通过pushItem向后加载数据,addItem向前加载数据
  2. 使用Swiper组件和LazyForEach将数据源中的每条数据存放于Text组件中
  3. 设置Swiper的滑动方向为水平方向,实现左右翻页效果
  4. 在数据源的getData方法中处理网络加载逻辑

代码实现

@Component
export struct LeftRightPlipPage {
  @Link isMenuViewVisible: boolean;
  @Link isCommentVisible: boolean;
  @Link currentPageNum: number;
  private swiperController: SwiperController = new SwiperController();
  private data: BasicDataSource = new BasicDataSource([]);
  private screenW: number = px2vp(display.getDefaultDisplaySync().width);
  @Prop bgColor: string;
  @Prop isbgImage: boolean;
  @Prop textSize: number;
  // 播放文章列表
  @Link readInfoList: TextReader.ReadInfo[];
  @Link selectedReadInfo: TextReader.ReadInfo;

  aboutToAppear(): void {
    /**
     * 请求网络数据之后可以通过this.data.addItem插入到数据源的开头
     * 请求网络数据之后可以通过this.data.pushItem插入到数据源的末尾
     */
    for (let i = CONFIGURATION.PAGEFLIPPAGESTART; i <= CONFIGURATION.PAGEFLIPPAGEEND; i++) {
      this.data.pushItem(STRINGCONFIGURATION.PAGEFLIPRESOURCE + i.toString());
    }
  }

  build() {
    Stack() {
      // 实现Swiper组件的左右翻页效果
      Swiper(this.swiperController) {
        LazyForEach(this.data, (item: string) => {
          Text(item)
            .width('100%')
            .height('100%')
            .fontSize(this.textSize)
            .padding(15)
            .backgroundColor(this.bgColor)
        }, item => item)
      }
      .index(this.currentPageNum - 1)
      .loop(false)
      .indicator(false)
      .onChange((index: number) => {
        this.currentPageNum = index + 1;
        this.selectedReadInfo = this.readInfoList[this.currentPageNum - 1];
      })
    }
    .width('100%')
    .height('100%')
  }
}

上下翻页实现

上下翻页适合长文本阅读,我们使用List组件结合LazyForEach来实现:

核心实现步骤

  1. aboutToAppear()方法中通过pushItem向后加载数据,addItem向前加载数据
  2. 使用List组件和LazyForEach将数据源中的每条数据存放于Text组件中
  3. 设置List的滚动方向为垂直方向,实现上下翻页效果
  4. 在数据源的getData方法中处理网络加载逻辑

代码实现

@Component
export struct UpDownFlipPage {
  private data: BasicDataSource = new BasicDataSource([]);
  @Link isMenuViewVisible: boolean;
  @Link isCommentVisible: boolean;
  @Link @Watch('updatePage')currentPageNum: number;
  @Prop bgColor: string;
  @Prop isbgImage: boolean;
  @Prop textSize: number;
  // 播放文章列表
  @Link readInfoList: TextReader.ReadInfo[];
  @Link selectedReadInfo: TextReader.ReadInfo;
  @State pageIndex: number = 0;
  private scroller: ListScroller = new ListScroller();

  aboutToAppear(): void {
    /**
     * 请求网络数据之后可以通过this.data.addItem插入到数据源的开头
     * 请求网络数据之后可以通过this.data.pushItem插入到数据源的末尾
     */
    for (let i = CONFIGURATION.PAGEFLIPPAGESTART; i <= CONFIGURATION.PAGEFLIPPAGEEND; i++) {
      this.data.pushItem(STRINGCONFIGURATION.PAGEFLIPRESOURCE + i.toString());
    }
  }

  build() {
    Stack() {
      // 实现List组件的上下翻页效果
      List({ scroller: this.scroller }) {
        LazyForEach(this.data, (item: string) => {
          ListItem() {
            Text(item)
              .width('100%')
              .height('100%')
              .fontSize(this.textSize)
              .padding(15)
              .backgroundColor(this.bgColor)
          }
        }, item => item)
      }
      .onScrollIndex((firstIndex: number, lastIndex: number) => {
        this.pageIndex = firstIndex;
        this.currentPageNum = this.pageIndex + 1;
        this.selectedReadInfo = this.readInfoList[this.currentPageNum - 1];
      })
    }
    .width('100%')
    .height('100%')
  }
}

覆盖翻页实现

覆盖翻页提供最接近实体书的阅读体验,我们使用Stack组件和动画效果来实现:

核心实现步骤

  1. Stack组件中布局三个ReaderPagemidPage位于中间可以根据offsetX实时translate自己的位置
  2. 当offsetX<0时,translate的x为offsetX,midPage向左移动,显现rightPage
  3. 当offsetX>0时,translate的x为0,midPage不动,leftPage向右滑动
  4. 将滑动翻页的动画和点击翻页的动画封装在一个闭包中
  5. 确定翻页时将offsetX设置为screenW或者-screenW,产生覆盖翻页的效果
  6. 动画结束时offsetX被置为0,leftPage和midPage回归原位
  7. 根据currentPageNum加载三个content相应的内容

代码实现

@Component
export struct CoverFlipPage {
  @State leftPageContent: string = "";
  @State midPageContent: string = "";
  @State rightPageContent: string = "";
  @State offsetX: number = 0;
  @Link isMenuViewVisible: boolean;
  @Link isCommentVisible: boolean;
  @Link @Watch('updatePage')currentPageNum: number;
  private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.Left | PanDirection.Right });
  private screenW: number = px2vp(display.getDefaultDisplaySync().width);
  @Prop bgColor: string;
  @Prop isbgImage: boolean;
  @Prop textSize: number;
  // 播放文章列表
  @Link readInfoList: TextReader.ReadInfo[];
  @Link selectedReadInfo: TextReader.ReadInfo;

  aboutToAppear(): void {
    this.updatePage();
  }

  updatePage() {
    // 根据当前页码更新三个页面的内容
    if (this.currentPageNum > 1) {
      this.leftPageContent = this.readInfoList[this.currentPageNum - 2].content;
    }
    this.midPageContent = this.readInfoList[this.currentPageNum - 1].content;
    if (this.currentPageNum < this.readInfoList.length) {
      this.rightPageContent = this.readInfoList[this.currentPageNum].content;
    }
  }

  build() {
    Stack() {
      // 左页面
      Text(this.leftPageContent)
        .width('100%')
        .height('100%')
        .fontSize(this.textSize)
        .padding(15)
        .backgroundColor(this.bgColor)
        .translate({ x: this.offsetX > 0 ? this.offsetX - this.screenW : -this.screenW })

      // 中间页面
      Text(this.midPageContent)
        .width('100%')
        .height('100%')
        .fontSize(this.textSize)
        .padding(15)
        .backgroundColor(this.bgColor)
        .translate({ x: this.offsetX < 0 ? this.offsetX : 0 })

      // 右页面
      Text(this.rightPageContent)
        .width('100%')
        .height('100%')
        .fontSize(this.textSize)
        .padding(15)
        .backgroundColor(this.bgColor)
    }
    .width('100%')
    .height('100%')
    .gesture(
      PanGesture(this.panOption)
        .onActionStart(() => {
          // 开始滑动时的处理
        })
        .onActionUpdate((event: GestureEvent) => {
          // 滑动过程中更新offsetX
          this.offsetX = event.offsetX;
        })
        .onActionEnd(() => {
          // 滑动结束时的处理
          if (this.offsetX < -this.screenW / 6 && this.currentPageNum < this.readInfoList.length) {
            // 向左翻页动画
            animateTo({ duration: 200 }, () => {
              this.offsetX = -this.screenW;
            });
            this.currentPageNum += 1;
            this.selectedReadInfo = this.readInfoList[this.currentPageNum - 1];
          } else if (this.offsetX > this.screenW / 6 && this.currentPageNum > 1) {
            // 向右翻页动画
            animateTo({ duration: 200 }, () => {
              this.offsetX = this.screenW;
            });
            this.currentPageNum -= 1;
            this.selectedReadInfo = this.readInfoList[this.currentPageNum - 1];
          } else {
            // 回弹动画
            animateTo({ duration: 100 }, () => {
              this.offsetX = 0;
            });
          }
        })
    )
  }
}

翻页方式切换实现

在实际应用中,我们需要提供一种机制让用户可以切换不同的翻页方式。以下是实现翻页方式切换的代码:

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(() => {
      this.isMenuViewVisible = false;
      this.filledName = '';
      this.isVisible = false;
    })
  }
}

性能优化建议

在实现电子书翻页效果时,需要注意以下性能优化点:

  1. 懒加载:使用LazyForEach按需加载内容,减少内存占用
  2. 缓存控制:合理设置CacheCount,平衡内存使用和加载性能
  3. 网络请求优化:在翻页前预加载下一页内容,提高用户体验
  4. 动画性能:使用硬件加速的动画效果,保证翻页流畅度
  5. 内存管理:及时释放不需要的资源,避免内存泄漏

总结

本教程详细介绍了HarmonyOS中实现三种电子书翻页效果的方法:左右翻页、上下翻页和覆盖翻页。每种翻页方式都有其特定的应用场景和实现技巧。通过合理使用SwiperListLazyForEachCacheCountStack等组件,我们可以实现流畅、自然的电子书翻页效果,提升用户的阅读体验。

在实际开发中,可以根据应用的具体需求选择合适的翻页方式,或者像本例一样提供多种翻页方式供用户选择。通过本教程的学习,相信你已经掌握了HarmonyOS电子书翻页效果的实现方法,可以在自己的应用中灵活运用。