阅读服务使用示例(HarmonyOS Reader Kit)
2025-06-28 13:16:08
109次阅读
0个评论
阅读服务使用示例(HarmonyOS Reader Kit)
Reader Kit到底能干啥?
第一次搞电子书阅读器,真以为就是“读txt显示出来”这么简单,结果各种格式、排版、翻页动效、目录跳转……全是坑。还好有Reader Kit,救了我一命。写这篇纯粹是给后来人留点“血泪史”,也给自己留个念想。
都能干啥?
- txt、epub、mobi、azw、azw3这些格式都能整,书名、作者、封面、目录、正文全能拿。
- txt、富文本(html+css)排版,仿真翻页、横滑分页,排版快照啥都有。
- 阅读页组件(ReadPageComponent)负责内容显示、翻页交互、动效,进度感知也有。
用下来觉得爽的点
- 多格式解析,啥书都能读,信息都能薅。
- 富文本排版快,支持自定义字体,W3C标准,兼容性不用愁。
- 翻页动效真香,OpenGL加持,翻着翻着都想多看两页。
几个绕不开的名词
- ReadPageComponent:阅读页UI组件,负责内容显示、翻页交互、进度感知。
- BookParser:电子书解析引擎,啥格式都能啃。
- spine(书脊):不是你的后背,是书的阅读顺序,每个SpineItem是一个内容节点。
有啥坑/限制
- 只能读本地文件,别想着在线书,云书架啥的暂时别想。
- DRM保护的书,别折腾了,打不开。
- 只认txt、epub、mobi、azw、azw3,pdf啥的别拿来试。
- 排版和交互必须用ReadPageComponent,自己造轮子多半踩坑。
- 只支持HarmonyOS NEXT 5.0.4+的真机,模拟器别浪费时间。
- 只在大陆能用,港澳台/海外暂时别想。
怎么拿到书名和封面?
有时候产品就想在书架上秀个封面和书名,其实Reader Kit拿这些信息很顺手。
我一般这么写(别忘了spineIndex,写错了封面就是一片空白,别问我怎么知道的):
// 这段代码抄过去基本能用,别忘了路径和权限
import { bookParser } from '@kit.ReaderKit';
import { image } from '@kit.ImageKit';
let path: string = './download/ebook/abc.epub';
let bookParserHandler: bookParser.BookParserHandler = await bookParser.getDefaultHandler(path);
let bookInfo: bookParser.BookInfo = bookParserHandler.getBookInfo();
let bookTitle = bookInfo.bookTitle;
let buffer: ArrayBuffer = bookParserHandler.getResourceContent(-1, bookInfo.bookCoverImage);
let imageSource: image.ImageSource = image.createImageSource(buffer);
let bookCover: image.PixelMap = await imageSource.createPixelMap();
imageSource.release();
怎么搞目录和跳转?
目录、章节跳转这些功能,产品肯定要。Reader Kit的目录解析和跳转还挺顺。
我一般这么写(目录缩进别乱写,catalogLevel用错,目录就成了“楼梯”):
// 目录渲染和跳转,抄过去能跑,记得调试下缩进
import { bookParser } from '@kit.ReaderKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
@State catalogItemList: bookParser.CatalogItem[] = [];
private defaultHandler: bookParser.BookParserHandler | null = null;
private async getCatalogItemList(){
let path: string = './download/ebook/abc.epub';
this.defaultHandler = await bookParser.getDefaultHandler(path);
this.catalogItemList = this.defaultHandler.getCatalogList() || [];
}
// 渲染目录
build() {
Column() {
List() {
ForEach(this.catalogItemList, (item: bookParser.CatalogItem) => {
ListItem() {
// ...章节名、点击跳转等...
}
})
}
}
}
跳转章节:
private async jumpToCatalogItem(catalogItem: bookParser.CatalogItem){
const domPos = await this.getDomPos(catalogItem);
const resourceIndex = this.getResourceItemByCatalog(catalogItem).index;
// 用domPos和resourceIndex跳转
}
private async getDomPos(catalogItem: bookParser.CatalogItem): Promise<string> {
const domPos: string = this.defaultHandler?.getDomPosByCatalogHref(catalogItem.href || '') || '';
return domPos;
}
private getResourceItemByCatalog(catalogItem: bookParser.CatalogItem): bookParser.SpineItem {
let resourceFile = catalogItem.resourceFile || '';
let spineList: bookParser.SpineItem[] = this.defaultHandler?.getSpineList() || [];
let resourceItemArr = spineList.filter(item => item.href === resourceFile);
if (resourceItemArr.length > 0) {
hilog.info(0x0000, 'testTag', 'getResourceItemByCatalog get resource ', resourceItemArr[0]);
return resourceItemArr[0];
} else if (spineList.length > 0) {
hilog.info(0x0000, 'testTag', 'getResourceItemByCatalog get resource in resourceList', spineList[0]);
return spineList[0];
} else {
hilog.info(0x0000, 'testTag', 'getResourceItemByCatalog get resource in escape');
return {
idRef: '',
index: 0,
href: '',
properties: ''
};
}
}
阅读器怎么搭起来?
ReadPageComponent才是“灵魂”,翻页、进度、交互全靠它。
第一次做阅读器,没等页面渲染完就隐藏loading,结果用户一进来先黑屏。记得加好加载状态!
我一般这么写:
// 下面这段直接抄,记得把路径和参数换成自己的
import { bookParser, ReadPageComponent, readerCore } from '@kit.ReaderKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { display } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
private readerComponentController: readerCore.ReaderComponentController = new readerCore.ReaderComponentController();
private readerSetting: readerCore.ReaderSetting = {
fontName: '系统字体',
fontPath: '',
fontSize: 18,
fontColor: '#000000',
fontWeight: 400,
lineHeight: 1.9,
nightMode: false,
themeColor: 'rgba(248, 249, 250, 1)',
themeBgImg: '',
flipMode: '0',
scaledDensity: display.getDefaultDisplaySync().scaledDensity > 0 ? display.getDefaultDisplaySync().scaledDensity : 1,
viewPortWidth: 370,
viewPortHeight: 800,
};
private bookParserHandler: bookParser.BookParserHandler | null = null;
@State isLoading: boolean = true;
// 构建阅读组件
build() {
Stack() {
ReadPageComponent({
controller: this.readerComponentController,
readerCallback: (err: BusinessError, data: readerCore.ReaderComponentController) => {
this.readerComponentController = data;
}
})
Row() {
Text('加载中...')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
.visibility(this.isLoading ? Visibility.Visible : Visibility.None)
}.width('100%').height('100%')
}
// 打开书籍到指定进度
aboutToAppear(): void {
let filePath: string = 'xxx/download/ebook/abc.epub';
let spineIndex: number = 0;
let domPos: string = '';
this.registerListener();
this.startPlay(filePath, spineIndex, domPos);
}
private registerListener(): void {
this.readerComponentController.on('pageShow', (data: readerCore.PageDataInfo): void => {
hilog.error(0x0000, 'testTag', 'pageshow: data is: ' + JSON.stringify(data));
if (data.state === readerCore.PageState.PAGE_ON_SHOW) {
this.isLoading = false;
}
});
}
private async startPlay(filePath: string, spineIndex: number, domPos: string) {
try {
let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
let initPromise = this.readerComponentController.init(context);
let bookParserHandler = bookParser.getDefaultHandler(filePath);
let result: [bookParser.BookParserHandler, void] = await Promise.all([bookParserHandler, initPromise]);
this.bookParserHandler = result[0];
this.readerComponentController.setPageConfig(this.readerSetting);
this.readerComponentController.registerBookParser(this.bookParserHandler);
this.readerComponentController.startPlay(spineIndex || 0, domPos);
} catch (err) {
hilog.error(0x0000, 'testTag', 'startPlay: err: ' + JSON.stringify(err));
}
}
aboutToDisappear(): void {
this.readerComponentController.off('pageShow');
this.readerComponentController.releaseBook();
}
想换字体?这样搞
有时候产品一句“能不能换个字体”,开发就得支持自定义字体。字体文件可以放在resources/rawfile/fonts或沙箱路径下,别忘了注册resourceRequest回调,否则字体加载不出来。
我一般这么写:
// 字体换起来,路径别写错,回调别忘了
import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
let filePath: string = 'fonts/SourceHanSerifCN-VF.ttf';
// let filePath: string = this.getUIContext().getHostContext()!.filesDir + 'fonts/SourceHanSerifCN-VF.ttf';
this.readerSetting.fontName = '思源宋体';
this.readerSetting.fontPath = filePath;
this.readerComponentController.setPageConfig(this.readerSetting);
aboutToAppear(): void {
this.readerComponentController.on('resourceRequest', this.resourceRequest);
}
aboutToDisappear(): void {
this.readerComponentController.off('resourceRequest');
}
private isFont(filePath: string): boolean {
let options = [".ttf", ".woff2", ".otf"];
let path = filePath.toLowerCase();
return options.some(ext => path.indexOf(ext) !== -1);
}
private resourceRequest: bookParser.CallbackRes<string, ArrayBuffer> = (filePath: string): ArrayBuffer => {
if(filePath.length === 0){
return new ArrayBuffer(0);
}
let resourcePath = filePath;
if(this.isFont(filePath)){
resourcePath = 'fonts/' + resourcePath;
}
try {
let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
let value: Uint8Array = context.resourceManager.getRawFileContentSync(resourcePath);
return value.buffer as ArrayBuffer;
} catch (error) {
return this.loadFileFromPath(resourcePath);
}
}
private loadFileFromPath(filePath: string): ArrayBuffer {
try {
let stats = fs.statSync(filePath);
let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
let buffer = new ArrayBuffer(stats.size);
fs.readSync(file.fd, buffer);
fs.closeSync(file);
return buffer;
} catch (err) {
return new ArrayBuffer(0);
}
}
背景也能随心换
想让阅读器更有个性?可以自定义背景色和背景图片。设置浅色背景时,记得关掉夜间模式,字体颜色也要适配,否则白底白字啥都看不见。
我一般这么写:
// 背景换起来,图片和颜色都能玩,别忘了适配字体色
import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
this.readerSetting.themeColor = '#FFFFFF';
this.readerSetting.themeBgImg = '';
this.readerSetting.nightMode = false;
this.readerSetting.fontColor = '#000000';
this.readerSetting.themeBgImg = 'white_sky_first.jpg';
this.readerSetting.themeColor = '#FFFFFF';
this.readerSetting.nightMode = false;
this.readerSetting.fontColor = '#000000';
this.readerComponentController.setPageConfig(this.readerSetting);
aboutToAppear(): void {
this.readerComponentController.on('resourceRequest', this.resourceRequest);
}
aboutToDisappear(): void {
this.readerComponentController.off('resourceRequest');
}
private resourceRequest: bookParser.CallbackRes<string, ArrayBuffer> = (filePath: string): ArrayBuffer => {
if(filePath.length === 0){
return new ArrayBuffer(0);
}
try {
let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
let value: Uint8Array = context.resourceManager.getRawFileContentSync(filePath);
return value.buffer as ArrayBuffer;
} catch (error) {
return this.loadFileFromPath(filePath);
}
}
private loadFileFromPath(filePath: string): ArrayBuffer {
try {
let stats = fs.statSync(filePath);
let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
let buffer = new ArrayBuffer(stats.size);
fs.readSync(file.fd, buffer);
fs.closeSync(file);
return buffer;
} catch (err) {
return new ArrayBuffer(0);
}
}
深色/浅色切换别忘了
现在用户都喜欢深色/浅色自由切换,Reader Kit也能动态适配。监听colorMode变化,切换主题时记得同步字体色和背景色。
我一般这么写:
// 主题切换,别忘了字体色和背景色一起改
import { Configuration, UIAbility } from '@kit.AbilityKit';
import { ConfigurationConstant } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onConfigurationUpdate(newConfig: Configuration): void {
AppStorage.setOrCreate('colorMode', newConfig.colorMode);
}
}
@StorageLink('colorMode') @Watch('colorModeChange') colorMode: ConfigurationConstant.ColorMode =
ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET;
colorModeChange() {
if (this.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK) {
this.readerSetting.nightMode = true;
this.readerSetting.fontColor = '#ffffff';
this.readerSetting.themeColor = '#202224';
} else {
this.readerSetting.nightMode = false;
this.readerSetting.fontColor = '#000000';
this.readerSetting.themeColor = '#FFFFFF';
}
this.readerComponentController.setPageConfig(this.readerSetting);
}
踩过的坑和小建议
- 只能读本地书,别想着直接读网盘/云书。
- DRM加密的书,别折腾了,打不开。
- 只认那几种格式,别拿pdf来试。
- 排版和交互必须用ReadPageComponent,别想着自己造轮子。
- 只支持真机,模拟器别浪费时间。
- 只在大陆能用,港澳台/海外暂时别想。
- 真遇到坑别慌,文档看不懂就多试试代码,踩踩坑总能通。有更骚的用法欢迎留言交流!
官方文档/社区(有空多翻翻)
00
- 0回答
- 0粉丝
- 0关注
相关话题
- PDF Kit 使用示例(HarmonyOS)
- HarmonyOS ——Telephony Kit(蜂窝通信服务)教程
- HarmonyOS ——Telephony Kit(蜂窝通信服务)教程
- 元服务--环境与示例
- 元服务环境与示例
- 鸿蒙应用开发:WebSocket 使用示例
- 鸿蒙术语使用示例(开发者吐槽笔记)
- 【HarmonyOS Next开发】Calendar Kit日历管理
- 基础控件——新闻示例
- 【 HarmonyOS 5 入门系列 】鸿蒙HarmonyOS示例项目讲解
- 【 HarmonyOS 5 入门系列 】鸿蒙HarmonyOS示例项目讲解
- HarmonyOS NEXT 小说阅读器应用系列教程之沉浸式阅读体验开发教程
- 端侧OCR文字识别实现 -- Core Vision Kit ##HarmonyOS SDK AI##
- [HarmonyOS NEXT 实战案例:新闻阅读应用] 基础篇 - 水平分割布局构建新闻阅读界面
- (九五)HarmonyOS Design 在阅读领域的应用