阅读服务使用示例(HarmonyOS Reader Kit)

阅读服务使用示例(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,别想着自己造轮子。
  • 只支持真机,模拟器别浪费时间。
  • 只在大陆能用,港澳台/海外暂时别想。
  • 真遇到坑别慌,文档看不懂就多试试代码,踩踩坑总能通。有更骚的用法欢迎留言交流!

官方文档/社区(有空多翻翻)

  • Reader Kit官方文档
  • HarmonyOS开发者社区

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/88963.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/88963.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ASP.NET Core Web API 实现 JWT 身份验证

在ASP.NET Core WebApi中使用标识框架&#xff08;Identity)-CSDN博客 因为一般需要和标识框架一起使用,建议先查看标识框架用法 一.为什么需要JWT 我们的系统需要实现认证,即服务端需要知道登录进来的客户端的身份,管理员有管理员的权限,普通用户有普通用户的权限. 但服务…

优化Cereal宏 一行声明序列化函数

Cereal序列化库中宏递归展开的优化方案及技术解析 未优化&#xff1a;参考nlohmann json设计Cereal宏 一行声明序列化函数 宏实现 #include <cereal/cereal.hpp>// 强制二次展开 #define CEREAL_EXPAND( x ) x// 获取宏参数的数量&#xff0c;对应的CEREAL_PASTEn宏NAME…

14-C#的弹出的窗口输入与输出

C#的弹出的窗口输入与输出 1.文件名输入 string fileName Interaction.InputBox("输入保存的文件名", "保存");2.弹窗信息输出 MessageBox.Show("请选择轮询!", "Error", MessageBoxButtons.OK);catch (Exception ex){MessageBox.S…

多模态大语言模型arxiv论文略读(141)

Mini-InternVL: A Flexible-Transfer Pocket Multimodal Model with 5% Parameters and 90% Performance ➡️ 论文标题&#xff1a;Mini-InternVL: A Flexible-Transfer Pocket Multimodal Model with 5% Parameters and 90% Performance ➡️ 论文作者&#xff1a;Zhangwei …

VScode使用usb转网口远程开发rk3588

我使用的是鲁班猫的板&#xff0c;只有一个网口&#xff0c;需要接雷达&#xff0c;因此另外弄了一个usb转网口来连接电脑开发。 在使用vscode或MobaXterm连接板子时&#xff0c;使用主机名与用户名来连接&#xff1a; ssh catlubancat rk那边就直接插入usb转网口以及网线&a…

AUTOSAR图解==>AUTOSAR_AP_EXP_SOVD

AUTOSAR服务导向车辆诊断详解 面向现代化车辆架构的诊断方案 目录 1. 引言 1.1 ASAM SOVD简介1.2 SOVD产生的动机 2. SOVD参考架构 2.1 SOVD网关2.2 诊断管理器2.3 SOVD到UDS转换2.4 后端连接 3. SOVD用例 3.1 SOVD和UDS的共同用例3.2 SOVD特定用例 3.2.1 访问权限3.2.2 软件更…

第八讲:STL简介

1. 什么是STL STL(standard template libaray-标准模板库)&#xff1a;是C标准库的重要组成部分&#xff0c;不仅是一个可复的 组件库&#xff0c;而且是一个包罗数据结构与算法的软件框架。 2. STL的版本 a. 原始版本 Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本…

高弹性、高可靠!腾讯云 TDMQ RabbitMQ Serverless 版全新发布

导语 2025年6月起&#xff0c;腾讯云 TDMQ RabbitMQ 版正式推出 Serverless 版本&#xff0c;该版本基于自研的存算分离架构&#xff0c;兼容 AMQP 0-9-1 协议和开源 RabbitMQ 的各个组件与概念&#xff0c;且能够规避开源版本固有的不抗消息堆积、脑裂等稳定性缺陷&#xff0…

Linux 内存调优之 BPF 分析用户态小内存分配

写在前面 博文内容为 使用 BPF 工具跟踪 Linux 用户态小内存分配(brk,sbrk)理解不足小伙伴帮忙指正 😃,生活加油我看远山,远山悲悯 持续分享技术干货,感兴趣小伙伴可以关注下 _ brk 内存分配简单概述 一般来说,应用程序的数据存放于堆内存中,堆内存通过brk(2)系统调用进…

心理测评app心理测试系统框架设计

一、逻辑分析 用户管理逻辑 新用户注册&#xff1a;需要收集用户的基本信息&#xff0c;如用户名、密码、邮箱等&#xff0c;并且要对输入信息进行合法性校验&#xff0c;确保信息完整且符合格式要求。同时&#xff0c;为每个新用户生成唯一的标识符&#xff0c;方便后续数据管…

配置有nvlink的H20A800使用pytorch报错

背景 装有nvlink的h20机器上配置好驱动和cuda之后使用pytorch报错 A800机器同样 (pytorch2.4) rootxx-dev-H20:~# python Python 3.12.0 | packaged by Anaconda, Inc. | (main, Oct 2 2023, 17:29:18) [GCC 11.2.0] on linux Type “help”, “copyright”, “credits” or …

sql的语句执行过程

第一步&#xff1a;客户端把语句发给服务器端执行 当我们在客户端执行SQL语句时&#xff0c;客户端会把这条SQL语句发送给服务器端&#xff0c;让服务器端的进程来处理这语句。也就是说&#xff0c;Oracle 客户端是不会做任何的操作&#xff0c;他的主要任务就是把客户端产生的…

深度学习-分类

深度学习-分类方式 &#xff08;重点&#xff09;一、按数据类型与处理逻辑分类1. 序列数据&#xff08;时序/顺序相关&#xff09;2. 网格状数据&#xff08;空间相关&#xff09;3. 图结构数据&#xff08;非欧几里得结构&#xff09;4. 其他特殊类型数据 &#xff08;重点&a…

C语言---常见的字符函数和字符串函数介绍

目录 前言 1 字符分类函数 2 字符转换函数 3 strlen的使用和模拟实现 3.1 strlen的模拟实现 4 strcpy的使用和模拟实现 4.1 strcpy的模拟实现 5 strcat的使用和模拟实现 5.1 strcat的模拟实现 6 strcmp的使用和模拟实现 6.1 strcmp的模拟实现 7 strncpy函数的使用…

Minio入门+适配器模式(实战教程)

一、安装Minio 1.1 拉取镜像 docker pull minio/minio docker images 1.2创建挂载目录 1.2.1 创建数据目录 mkdir -p /docker-minio/data 1.2.2 创建配置文件目录 mkdir -p /docker-minio/config 1.2.3 设置权限 chmod -R 777 /docker-minio/data /docker-minio/config …

LLaMA-Factory 对 omnisql 进行 ppo dpo grpo nl2sql任务 实现难度 时间 全面对比

在LLaMA-Factory框架下&#xff0c;针对omnisql任务&#xff08;自然语言到SQL生成&#xff09;应用PPO、DPO、GRPO三种算法的实现难度、时间及全面对比如下&#xff1a; 一、实现难度对比 1. PPO&#xff08;近端策略优化&#xff09; 难度&#xff1a;★★☆☆☆&#xff…

Kingbase 数据库中的 sys_guid() 函数报错

解决 Kingbase 数据库中的 sys_guid() 函数报错问题 问题背景 Kingbase 数据库在迁移或使用过程中&#xff0c;可能会遇到 select sys_guid() 函数报错 , 提示函数不存在的情况&#xff0c;这通常是由于以下几种原因造成的&#xff1a; 函数未正确安装或未启用函数参数不符合…

零基础RT-thread第五节:电容按键(2)

上一章的电容按键完全使用的HAL库的代码&#xff0c;并没有使用线程。这里尝试使用线程来控制电容按键。 依旧是 F767 本来以为会很容易实现&#xff0c;没想到尝试了很久&#xff0c;电容按键一直没有反应。 static rt_uint32_t measure_charge_time(void) {// 步骤1: 放电 …

华为云Flexus+DeepSeek征文|单机部署 与 CCE 高可用部署下 Dify 性能实测

引言 在当今的 AI 应用开发领域&#xff0c;选择合适的部署方式对于应用的性能表现、资源利用和成本控制至关重要。华为云为开发者提供了多样化的部署选择&#xff0c;其中基于单机 Flexus 实例的基础版部署和基于 CCE 容器的高可用版部署是两种常见的方式。本文将深入对比这两…

钉钉小程序框架:Pinia 状态管理与持久化存储封装

上一篇文章完成了 Pinia 在钉钉小程序中的引入与基础配置 文章地址&#xff1a;钉钉小程序框架引入 Pinia 状态管理-CSDN博客 本文将深入探讨如何通过Pinia 结合持久化存储 实现用户状态 在上一章节中&#xff0c;我们已经完成了 Pinia 在钉钉小程序中的引入与基础配置。本章将…