鴻蒙開發-閱讀器正文頁面實現
鴻蒙開發-閱讀器正文頁面實現
記錄開發一個小說閱讀應用的過程
實現點擊書籍,讀取該書籍的文件內容,然后顯示該書籍正文內容,滑動進行翻頁。
實現邏輯
- 在書架頁面,獲取書籍列表,為每一項添加點擊事件,進行路由帶參跳轉,參數為書籍路徑或書籍URL,跳轉到正文頁面。
- 進入正文頁面后,設置閱讀頁面,文字大小、字體樣式、行間距、段間距等。
- 根據書籍路徑或URL,獲取文件內容,根據章節拆分文件內容。
- 獲取每章的內容,按字拆分,根據顯示界面寬度、文字大小、是否換行等,得到每行顯示的內容,用數組進行存儲。
- 根據顯示界面高度和每行文字的高度(文字大小×行間距)得到每頁顯示的行數,使用
slice得到當前頁、上一頁和下一頁的內容。 - 將當前頁的內容繪制在顯示區域內,上一頁繪制在當前頁的左邊,下一頁繪制在右邊,使用
offsetX變量控制滑動時的偏移量。
代碼實現
- 定義數據類
// 定義書籍類Book
class Book{
bookUrl?:string
bookName?:string
}
// 定義章節內容chapterContent
class chapterContent{
Number?:number
content?:string
title?:string
}
- 書架頁面
struct bookshelf {
@State bookList:Book[] = []
aboutToAppear(){
this.getBookList()
}
getBookList(){
// 獲取書籍列表,bookList在這里賦值
// 例如
this.bookList.push(
new Book()
)
}
build(){
Column(){
ForEach(this.bookList, (item: Book) => {
ListItem() {
bookListView({ book: item }) // 自定義書籍列表組件
}
.onClick(() => {
console.info('???打開書籍')
router.pushUrl({
url: 'pages/view/reader/readerPage', // 進入閱讀正文頁面
params: {
// 傳參,如果bookUrl為空,則使用默認值,目錄'/data/storage/el2/base/haps/entry/files'為電腦文件復制到模擬機中的存儲路徑。
bookUrl: item.bookUrl ? item.bookUrl : '/data/storage/el2/base/haps/entry/files/xxxx.TXT',
}
})
})
})
}
}
}
- 閱讀正文頁面
// API 12 13 編譯通過
// 路由傳參需要規范數據類型
class params {
bookUrl: string = ''
}
struct readerPage {
// 應該對bookUrl的值進行檢驗(是否存在,是否合法等等),但偷懶
@State bookUrl: string = (router.getParams() as params).bookUrl
// 或者這樣
// @State bookUrl:string = ''
// @State Params: params = router.getParams() as params
// aboutToAppear(){
// this.bookUrl = this.Params.bookUrl
// }
//*********************************//
// 以下看心情設定
@State currentChapterContent: number = 0 // 當前章節編號
@State lineSpacing: number = 1.8 // 行間距(行高相對于字體大小的倍數)
@State currentFontSize: number = 20 // 當前字體大小
@State paragraphSpacing: number = -2 // 段落間距
// ……
build(){
Column(){
// 自定義組件繪制正文
TxtContentDisplayModel({
bookUrl: this.bookUrl,
currentChapterContent: this.currentChapterContent,
lineSpacing: this.lineSpacing,
currentFontSize: this.currentFontSize,
paragraphSpacing: this.paragraphSpacing,
})
}
}
}
- 正文繪制
- 首先根據書籍路徑或URL讀取文件內容,并將讀取到的內容按照章節拆分
遍歷文件內容的每一行(遇到換行為止),使用正則表達式匹配章節名稱,匹配成功說明該行內容是章節名,則創建新的章節內容對象(chapterContent),設置該對象的章節名和章節編號,將該對象放入章節列表數組中;匹配失敗說明是章節內容,則將該行與章節列表數組中最后一個成員的content拼接作為章節內容。返回章節列表數組。
static async readFile(readFileUrl: string) {
let chapterNumber = 0
// const chapters: chaptersItem[] = [];
const chapters: chapterContent[] = [];
console.info('???readFileUrl:' + readFileUrl)
//const regex = /===第(.*?)章 (.*?)===/g;
// 使用正則表達式匹配章節名稱
const regex =
/^[=|<]{0,4}((?:序章|楔子|番外|第\s{0,4})([\d〇零一二兩三四五六七八九十百千萬壹貳叁肆伍陸柒捌玖拾佰仟]+?\s{0,4})(?:章|回(?!合)|話(?!說)|節(?!課)|卷|篇(?!張)))(.{0,30})/g;
await fs.readLines(readFileUrl, options).then((readerIterator: fs.ReaderIterator) => {
for (let it = readerIterator.next();!it.done; it = readerIterator.next()) {
const match = regex.exec(it.value);
if (match) {
const chapterTitleNumber = match[1]; // 內部章節
const chapterTitle = match[3];
chapterNumber++
chapters.push(new chapterContent(chapterNumber, chapterTitle, chapterTitleNumber, ''))
} else {
if (chapters.length > 0) {
chapters[chapters.length - 1].content += it.value
}
}
}
}).catch((err: BusinessError) => {
console.error("???readLines failed with error message: " + err.message + ", error code: " + err.code);
});
return chapters
}
- 根據當前章節編號(初始編號為0)獲取章節內容,按字分割,得到每一行顯示的內容。
遍歷章節內容(類型為string)(通過索引得到每個字),將遍歷到的內容和\n匹配,匹配失敗說明不是換行,則檢查是否小于顯示區域,小于則將遍歷到的內容拼接到當前行中(使用當前行 currentLine 存儲每行顯示的內容),大于則將當前行用變量chaptersItemLines存儲起來,并把遍歷到的內容賦值給當前行,檢查結束后索引加1;匹配成功說明是換行,則將當前行存儲起來,索引加2,并把當前行置空。得到當前章節內容CurrentChaptersContent
// 通過章節編號獲取章節內容
GetCurrentChapterByNumber(Number: number): string {
return this.chapters[Number].content
}
// 章節內容按字分割
splitText(context: CanvasRenderingContext2D) {
// let content: string = this.textContent
let content: string = this.GetCurrentChapterByNumber(this.currentChapterContent)
//
let chaptersItemLines: string[] = []
let currentLine: string = ''
let pattern = /\n/
let i = 0
// console.info(`???當前章節內容長度為:` + content[i])
while (i < content.length) {
let temp = content[i] + content[i+1]
// console.info(`??? ${i} == ${temp}`)
if (!pattern.test(temp)) {
// 檢查是否小于顯示區域
if (context.measureText(currentLine + ' ' + content[i]).width <
Number((this.screenWidth - 10).toFixed(0))) {
currentLine += content[i];
} else {
chaptersItemLines.push(currentLine);
currentLine = content[i];
}
i++
} else {
// console.info(`??? ||| ${temp == '\r\n'}`)
chaptersItemLines.push(currentLine);
i = i + 2 // 換行存在 \r\n
currentLine = '';
}
// console.info(`??? push ${content[i]}`)
}
if (pattern.test(chaptersItemLines[chaptersItemLines.length-1])) {
chaptersItemLines.splice(chaptersItemLines.length - 1, 1)
}
this.CurrentChaptersContent = chaptersItemLines
this.drawPage();
}
- 繪制文本
計算顯示區域內能顯示多少行(每頁行數=顯示高度/每行高度)(每行高度=文字大小*行間距),使用slice函數處理當前章節內容CurrentChaptersContent,得到每頁顯示的內容,在預定區域繪制文本。上一頁和下一頁與當前頁距離一個顯示區域的寬度,使用offsetX控制滑動顯示。
// 繪制文本
drawPage() {
this.context.font = `${this.currentFontSize}vp`
// 計算屏幕上可以顯示的行數 = 屏幕高度 / (字體大小 * 行間距) 向下取整
// console.info(`??? ${Math.floor((this.screenHeight - 150) / (this.currentFontSize * this.lineSpacing))}`)
// this.linesPerPage = Math.floor((this.screenHeight - 150) / (this.currentFontSize * this.lineSpacing))
// 感覺使用顯示區域高度會好一些
console.info(`??? ${Math.floor(this.context.height / (this.currentFontSize * this.lineSpacing))}`)
this.linesPerPage = Math.floor(this.context.height / (this.currentFontSize * this.lineSpacing))
if (this.context) {
// 初始化畫布
this.context.clearRect(0, 0, this.screenWidth, this.screenHeight);
this.context.font = `${this.currentFontSize}vp`;
this.context.fillStyle = '#000000';
// 計算當前頁行數和內容
const start = this.currentPage * this.linesPerPage;
const end = start + this.linesPerPage
const currentPageLines = this.CurrentChaptersContent.slice(start, end);
// console.info(`??? start: ${start} end: ${end}`)
// currentPageLines.forEach((line, index) => {
// console.info(`???第${index}行:${line}`)
// })
// 繪制文本
currentPageLines.forEach((line, index) => {
this.context.fillText(line, 10 + this.offsetX, (index + 1) * this.currentFontSize * this.lineSpacing);
})
const preStart = start - this.linesPerPage
const nextEnd = end + this.linesPerPage
// 上一頁內容
const prePageLines = this.CurrentChaptersContent.slice(preStart, start - 1);
prePageLines.forEach((line, index) => {
this.context.fillText(line, 10 - this.screenWidth + this.offsetX,
(index + 1) * this.currentFontSize * this.lineSpacing);
})
// 下一頁內容
const nextPageLines = this.CurrentChaptersContent.slice(end, nextEnd);
nextPageLines.forEach((line, index) => {
this.context.fillText(line, 10 + this.screenWidth + this.offsetX,
(index + 1) * this.currentFontSize * this.lineSpacing);
})
}
}
- 使用
Canvas組件繪制內容,為組件設置滑動事件
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#FEFEFE')
.onReady(() => {
//繪制填充類文本
// this.drawPage()
this.context.font = `${this.currentFontSize}vp sans-serif`;
this.splitText(this.context)
})
.gesture(
PanGesture({ direction: PanDirection.Left | PanDirection.Right })
.onActionUpdate((Event) => {
// console.info(`???滑動中`)
// console.info(`???${JSON.stringify(Event)}`)
this.offsetX = Event.offsetX
this.drawPage()
})
.onActionEnd((Event) => {
// console.info(`???滑動結束`)
// console.info(`???${JSON.stringify(Event)}`)
console.info(`???currentPage: ${this.currentPage}`)
if (Event.offsetX > 100 && this.currentPage == 0) {
if (this.currentChapterContent > 0) {
this.currentPage = 0
this.currentChapterContent--
this.splitText(this.context)
} else {
showMessage('沒有上一頁了') // 自定義組件,用于彈出提示,可以用日志輸出代替
}
} else if (Event.offsetX > 100 && this.currentPage > 0) {
this.currentPage--
console.info('??? 上一頁')
} else if (Event.offsetX < -100 && this.currentPage < this.GetTotalPages()) {
this.currentPage++
console.info('??? 下一頁')
} else if (Event.offsetX < -100 && this.currentPage == this.GetTotalPages()) {
if (this.currentChapterContent < this.chapters.length) {
this.currentPage = 0
this.currentChapterContent++
this.splitText(this.context)
} else {
showMessage('沒有下一頁了')
}
}
this.offsetX = 0
this.drawPage()
})
)
運行結果
可以添加動畫,避免翻頁過程過于生硬,但偷懶
需要注意的
- 由于預覽器不支持讀取文件,需要使用模擬器。
將電腦文件拖到模擬機上,會復制文件到目錄/data/storage/el2/base/haps/entry/files/下。
- 日志輸出標記
由于運行模擬機時會輸出大量日志,不方便查看自己寫的的輸出,可以使用console.info('??? xxxxx') ,用???對日志輸出內容進行標記,方便查看。

浙公網安備 33010602011771號