项目源码地址
项目源码已发布到GitCode平台, 方便开发者进行下载和使用。
效果演示
前言
在移动设备上阅读电子书时,翻页效果对用户体验至关重要。一个流畅、自然的翻页动画能够让用户获得接近纸质书籍的阅读体验。本教程将详细介绍如何在HarmonyOS应用中实现三种常见的电子书翻页效果:左右翻页、上下翻页和覆盖翻页。
核心组件介绍
在实现翻页效果之前,我们需要了解以下核心组件:
- Swiper:用于实现左右滑动的容器组件
- List:用于实现上下滑动的列表组件
- LazyForEach:用于按需加载数据的高效渲染方式
- CacheCount:控制缓存数量,优化内存使用
- Stack:用于实现覆盖翻页效果的层叠布局
实现方案概述
我们将实现三种不同的翻页效果,每种效果都有其特定的应用场景和实现方式:
- 左右翻页:模拟传统书籍的横向翻页,适合大多数阅读场景
- 上下翻页:类似网页浏览的纵向滚动,适合长文本阅读
- 覆盖翻页:模拟真实书页翻动效果,提供最接近实体书的阅读体验
数据源准备
在实现翻页效果之前,我们需要准备数据源。在本例中,我们使用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
来实现:
核心实现步骤
- 在
aboutToAppear()
方法中通过pushItem
向后加载数据,addItem
向前加载数据 - 使用
Swiper
组件和LazyForEach
将数据源中的每条数据存放于Text组件中 - 设置
Swiper
的滑动方向为水平方向,实现左右翻页效果 - 在数据源的
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
来实现:
核心实现步骤
- 在
aboutToAppear()
方法中通过pushItem
向后加载数据,addItem
向前加载数据 - 使用
List
组件和LazyForEach
将数据源中的每条数据存放于Text组件中 - 设置
List
的滚动方向为垂直方向,实现上下翻页效果 - 在数据源的
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
组件和动画效果来实现:
核心实现步骤
- 在
Stack
组件中布局三个ReaderPage
,midPage
位于中间可以根据offsetX实时translate自己的位置 - 当offsetX<0时,translate的x为offsetX,midPage向左移动,显现
rightPage
- 当offsetX>0时,translate的x为0,midPage不动,
leftPage
向右滑动 - 将滑动翻页的动画和点击翻页的动画封装在一个闭包中
- 确定翻页时将offsetX设置为screenW或者-screenW,产生覆盖翻页的效果
- 动画结束时offsetX被置为0,leftPage和midPage回归原位
- 根据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;
})
}
}
性能优化建议
在实现电子书翻页效果时,需要注意以下性能优化点:
- 懒加载:使用
LazyForEach
按需加载内容,减少内存占用 - 缓存控制:合理设置
CacheCount
,平衡内存使用和加载性能 - 网络请求优化:在翻页前预加载下一页内容,提高用户体验
- 动画性能:使用硬件加速的动画效果,保证翻页流畅度
- 内存管理:及时释放不需要的资源,避免内存泄漏
总结
本教程详细介绍了HarmonyOS中实现三种电子书翻页效果的方法:左右翻页、上下翻页和覆盖翻页。每种翻页方式都有其特定的应用场景和实现技巧。通过合理使用Swiper
、List
、LazyForEach
、CacheCount
和Stack
等组件,我们可以实现流畅、自然的电子书翻页效果,提升用户的阅读体验。
在实际开发中,可以根据应用的具体需求选择合适的翻页方式,或者像本例一样提供多种翻页方式供用户选择。通过本教程的学习,相信你已经掌握了HarmonyOS电子书翻页效果的实现方法,可以在自己的应用中灵活运用。