閱讀服務使用示例(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
英文地址,請注明出處:http://en.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 在釘釘小程序中的引入與基礎配置。本章將…