HarmonyOS PageFlip — 三种阅读器翻页效果的实现解析
项目地址:https://gitcode.com/HarmonyOS_Samples/PageFlip 技术栈:HarmonyOS 5.0.5 / ArkTS / ArkGraphics 2D
项目简介
PageFlip 是华为 HarmonyOS 官方示例项目,展示了如何在阅读器场景中实现三种经典的翻页效果:上下翻页、左右覆盖翻页和仿真翻页。这个项目不仅是一个效果演示,更是一份完整的 HarmonyOS 动画与图形编程实践指南。
对于正在开发阅读类、漫画类或文档浏览类应用的开发者来说,PageFlip 提供了可以直接复用的核心逻辑。三种翻页模式覆盖了绝大多数阅读场景的用户习惯:上下滑动适合长文本连续阅读,左右覆盖适合章节切换,仿真翻页则还原了实体书的翻阅体验。
项目基于 HarmonyOS 5.0.5 Release,使用 ArkTS 语言开发,核心仿真翻页效果通过 ArkGraphics 2D 的 @ohos.graphics.drawing 接口实现。
功能概览
| 翻页模式 | 交互方式 | 技术实现 |
|---|---|---|
| 仿真翻页 | 手指滑动 / 点击左右侧 | ArkGraphics 2D + NodeContainer 自定义绘制 |
| 左右覆盖翻页 | 左右滑动 / 点击翻页 | animateTo 显式动画 + 组件位移 |
| 上下翻页 | 上下滑动 | List 组件 + LazyForEach 懒加载 |
进入应用后默认展示仿真翻页效果。点击屏幕中部区域弹出底部选项栏,可在三种模式间切换。
真机运行效果
仿真翻页模式(默认)

底部设置菜单

三种翻页模式可通过底部菜单快速切换,菜单采用 bindSheet 实现,从屏幕底部弹出,背景透明,不影响阅读内容的连续性。
工程结构
entry/src/main/ets/
├── common/
│ └── Constants.ets // 公共常量与枚举定义
├── entryability/
│ └── EntryAbility.ets // 应用入口 Ability
├── pages/
│ └── Index.ets // 首页:三种翻页模式的容器
├── view/
│ ├── BottomView.ets // 底部翻页类型选择弹窗
│ ├── CoverFlipPage.ets // 左右覆盖翻页实现
│ ├── EmulationFlipPage.ets // 仿真翻页实现(核心)
│ ├── ReaderPage.ets // 单页阅读内容组件
│ └── UpDownFlipPage.ets // 上下翻页实现
└── viewmodel/
├── BasicDataSource.ets // List 数据源管理
└── PageNodeController.ets // 节点控制器与绘制逻辑工程采用标准的 HarmonyOS 模块化结构。pages/Index.ets 作为根容器,通过状态变量 buttonClickedIndex 切换三种翻页组件的显示。view/ 目录下的三个 *FlipPage 分别独立实现各自的翻页逻辑,互不耦合。
核心实现解析
一、上下翻页:List 组件的极简实现
上下翻页是最基础的翻页模式,实现也最为简洁。核心思路是利用 ArkUI 的 List 组件原生支持的垂直滑动能力:
// entry/src/main/ets/view/UpDownFlipPage.ets
List({ initialIndex: this.currentPageNum - 1, scroller: this.scroller }) {
LazyForEach(this.data, (item: string) => {
ListItem() {
Text($r(item))
.fontSize($r('app.integer.flip_page_text_font_size'))
.lineHeight($r('app.integer.flip_page_text_line_height'))
.padding({ left: 26, right: 20 })
.fontColor($r('app.color.text_font_color'))
}
}, (item: string, index: number) => index + JSON.stringify(item))
}
.height($r('app.string.page_flip_full_size'))
.scrollBar(BarState.Off)
.cachedCount(Constants.PAGE_FLIP_CACHE_COUNT)
.onScrollIndex((firstIndex: number) => {
this.currentPageNum = firstIndex + 1;
})关键点:
LazyForEach实现懒加载,只渲染可视区域内的页面,内存占用低cachedCount预加载相邻页面,保证滑动流畅onScrollIndex回调同步当前页码到父组件- 数据源
BasicDataSource实现了IDataSource接口,支持数据变更监听
这种实现方式的优势在于零自定义绘制,完全依赖 ArkUI 框架的原生能力,代码量少、稳定性高。
真机效果 - 上下翻页模式:

上下翻页模式采用标准的列表滚动交互,用户可以通过上下滑动浏览内容,体验类似于常见的长文本阅读应用。LazyForEach 确保只有当前可视区域的页面被渲染,内存占用极低。
二、左右覆盖翻页:显式动画驱动
覆盖翻页模拟了卡片堆叠的切换效果:当前页面向左或向右滑出,露出下方的下一页。实现上通过 animateTo 显式动画控制组件的 translate 偏移:
// entry/src/main/ets/view/CoverFlipPage.ets
private pageAnimateTo(isClick: boolean, isLeft?: boolean) {
this.getUIContext().animateTo({
duration: Constants.PAGE_FLIP_TO_AST_DURATION,
curve: Curve.EaseOut,
onFinish: () => {
// 动画结束后更新页码
if (this.offsetX > Constants.PAGE_FLIP_RIGHT_FLIP_OFFSETX &&
this.currentPageNum !== Constants.PAGE_FLIP_PAGE_START) {
this.currentPageNum -= 1;
} else if (this.offsetX < Constants.PAGE_FLIP_LEFT_FLIP_OFFSETX &&
this.currentPageNum !== Constants.PAGE_FLIP_PAGE_END) {
this.currentPageNum += 1;
}
this.offsetX = Constants.PAGE_FLIP_ZERO;
this.simulatePageContent();
this.isAnimating = false;
}
}, () => {
if (isClick) {
this.offsetX = isLeft ? this.screenW : -this.screenW;
} else {
// 根据滑动方向判断翻页或回弹
if (!this.isPageForward && !this.isGestureForward) {
this.offsetX = -this.screenW;
} else if (this.isPageForward && this.isGestureForward) {
this.offsetX = this.screenW;
} else {
this.offsetX = Constants.PAGE_FLIP_ZERO;
}
}
});
}关键点:
- 使用
Stack堆叠三个ReaderPage组件:左页、中页(当前页)、右页 - 中页通过
translate({ x: this.offsetX })实现水平位移 PanGesture监听滑动手势,实时更新offsetX- 手势结束时调用
pageAnimateTo,使用Curve.EaseOut实现自然的减速效果 - 页面边缘添加
shadow属性,增强层叠视觉层次
覆盖翻页的核心难点在于手势方向的判断:需要比较手势起始位置与结束位置,结合当前页码判断是否越界,并在越界时给出提示。
真机效果 - 覆盖翻页模式:

覆盖翻页模式下,当前页面会像卡片一样向左或向右滑出,露出下方的下一页内容。页面边缘的阴影效果增强了层叠的视觉层次感。
三、仿真翻页:ArkGraphics 2D 的自定义绘制(核心难点)
仿真翻页是整个项目最复杂的部分。它需要在手指滑动时实时计算书页的卷曲形状,并通过 2D 绘制 API 渲染出逼真的翻页效果。
3.1 架构设计
┌─────────────────────────────────────────┐
│ Stack 容器 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 右页 │ │ 中页 │ │ 左页 │ │
│ │(下一页) │ │(当前页) │ │(上一页) │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ NodeContainer │ │
│ │ (自定义绘制层:翻页动画) │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘中页和左页通过 translate 定位,而翻页过程中的动态卷曲效果则通过 NodeContainer + 自定义 RenderNode 绘制在上层。
3.2 触摸点与几何计算
仿真翻页的核心是根据手指位置计算书页卷曲的几何形状。代码中定义了多个关键点:
// entry/src/main/ets/viewmodel/PageNodeController.ets
class MyPoint {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
// 关键点定义
let pointA: MyPoint = new MyPoint(-1, -1); // 手指触摸点
let pointF: MyPoint = new MyPoint(0, 0); // 页面固定角(右上角或右下角)
let pointG: MyPoint = new MyPoint(0, 0); // AF 中点
let pointE: MyPoint = new MyPoint(0, 0); // 贝塞尔曲线控制点
// ... 更多辅助点当手指在屏幕上滑动时,系统根据触摸点 A 和固定角 F 的位置,通过几何公式计算出书页卷曲边缘的所有控制点:
function calcPointsXY(): void {
pointG.x = (pointA.x + pointF.x) / 2;
pointG.y = (pointA.y + pointF.y) / 2;
pointE.x = pointG.x - (pointF.y - pointG.y) * (pointF.y - pointG.y) / (pointF.x - pointG.x);
pointE.y = pointF.y;
pointH.x = pointF.x;
pointH.y = pointG.y - (pointF.x - pointG.x) * (pointF.x - pointG.x) / (pointF.y - pointG.y);
// 计算交点 B、K
pointB = getIntersectionPoint(pointA, pointE, pointC, pointJ);
pointK = getIntersectionPoint(pointA, pointH, pointC, pointJ);
}这些计算涉及中点公式、贝塞尔曲线控制点计算和线段交点求解,本质上是在模拟纸张弯曲时的物理形态。
3.3 绘制流程
自定义绘制在 RectRenderNode.draw() 中完成,分为四个步骤:
// entry/src/main/ets/viewmodel/PageNodeController.ets
draw(context: DrawContext): void {
const canvas = context.canvas;
init(); // 1. 初始化数据
drawPathBShadow(canvas); // 2. 绘制下一页的阴影
drawPathC(canvas); // 3. 绘制翻起页的背面
getPathA(); // 4. 计算当前页裁剪区域
drawPathAContent(canvas); // 5. 绘制当前页内容
}步骤详解:
- 初始化数据:根据手指位置计算所有几何点坐标
- 绘制下一页阴影:使用线性渐变模拟翻页时下方页面的阴影效果
- 绘制翻起页背面:通过矩阵变换实现页面的镜像翻转,模拟纸张背面的内容
- 绘制当前页内容:使用
clipPath裁剪出当前可见区域,再绘制页面截图
3.4 截图与像素操作
仿真翻页需要获取页面内容的像素数据,代码中使用了 getComponentSnapshot API:
// entry/src/main/ets/view/EmulationFlipPage.ets
this.pagePixelMap = this.getUIContext().getComponentSnapshot().getSync(this.snapPageId);截图后的 PixelMap 被存入 AppStorage,供 PageNodeController 在绘制时读取:
// 在 drawPathC 中绘制背面内容
let pagePixelMap: image.PixelMap = AppStorage.get('pagePixelMap') as image.PixelMap;
let verts: Array<number> = [0, 0, viewWidth, 0, 0, viewHeight, viewWidth, viewHeight];
canvas.drawPixelMapMesh(pagePixelMap, 1, 1, verts, 0, null, 0);3.5 自动翻页动画
当用户松开手指后,翻页动画需要自动完成。代码通过 setInterval 实现逐帧更新:
private setTimer(xDiff: number, yDiff: number, drawNode: () => void) {
this.timeID = setInterval((xDiff: number, yDiff: number, drawNode: () => void) => {
let x = AppStorage.get('positionX') as number + xDiff;
let y = AppStorage.get('positionY') as number + yDiff;
// 终止条件判断
if (x >= (AppStorage.get('windowWidth') as number) - 1 || y >= (AppStorage.get('windowHeight') as number) || y <= 0) {
this.finishLastGesture();
} else {
AppStorage.setOrCreate('positionX', x);
AppStorage.setOrCreate('positionY', y);
drawNode();
}
}, Constants.TIMER_DURATION, xDiff, yDiff, drawNode);
}每 8.3ms 更新一次触摸点位置,触发重新绘制,形成流畅的翻页动画。
真机效果 - 仿真翻页滑动过程:

上图展示了从右向左滑动翻页后的结果页面。仿真翻页通过实时计算书页卷曲的几何形状,配合阴影渐变和背面内容镜像,还原了实体书翻阅的真实体验。
运行效果
项目提供了完整的中文和英文演示截图。以下是真机运行截图对比:
| 仿真翻页 | 覆盖翻页 | 上下翻页 |
|---|---|---|
![]() | ![]() | ![]() |
| 手指滑动时书页实时卷曲 | 当前页面向左/右滑出 | 上下滑动浏览 |
| 支持点击左右侧快速翻页 | 支持手势和点击翻页 | 类似长文本阅读 |
| 点击中部弹出设置选项 | 动画流畅自然 | 内存占用最低 |
交互演示: 点击屏幕中部区域可唤出底部设置菜单,在三种翻页模式间即时切换。

技术要点总结
1. 三种翻页方案的选择策略
| 方案 | 实现复杂度 | 性能 | 适用场景 |
|---|---|---|---|
| List 上下翻页 | 低 | 高 | 长文本、连续阅读 |
| animateTo 覆盖翻页 | 中 | 高 | 章节切换、卡片浏览 |
| ArkGraphics 2D 仿真翻页 | 高 | 中 | 电子书、漫画、追求真实感 |
2. 状态管理
项目使用 AppStorage 作为全局状态存储,在仿真翻页中传递触摸点坐标、绘制状态等跨组件数据:
AppStorage.setOrCreate('positionX', this.positionX);
AppStorage.setOrCreate('positionY', this.positionY);
AppStorage.setOrCreate('drawState', DrawState.DS_MOVING);3. 性能优化
LazyForEach懒加载避免一次性渲染全部页面cachedCount控制预加载数量NodeContainer的clearNodes及时释放绘制资源pagePixelMap.release()避免内存泄漏
总结
PageFlip 项目展示了 HarmonyOS 在阅读场景下的三种翻页实现方案,从简单的 List 组件到复杂的 2D 自定义绘制,覆盖了不同复杂度需求的开发场景。
学习建议:
- 初学者:从
UpDownFlipPage入手,理解 List + LazyForEach 的基础用法 - 进阶开发者:研究
CoverFlipPage的animateTo动画和手势处理逻辑 - 高级开发者:深入
EmulationFlipPage和PageNodeController,掌握 ArkGraphics 2D 的自定义绘制、几何计算和像素操作
这个项目不仅是一个示例,更是一份HarmonyOS 动画与图形编程的实战教材。无论是开发阅读器、漫画 App 还是任何需要页面切换效果的应用,都能从中找到可直接复用的思路和代码。
环境要求
- HarmonyOS 5.0.5 Release 及以上
- DevEco Studio 5.0.5 Release 及以上
- 支持设备:华为手机、平板