<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      鴻蒙開發-閱讀器正文頁面實現

      鴻蒙開發-閱讀器正文頁面實現

      記錄開發一個小說閱讀應用的過程

      實現點擊書籍,讀取該書籍的文件內容,然后顯示該書籍正文內容,滑動進行翻頁。


      實現邏輯

      1. 在書架頁面,獲取書籍列表,為每一項添加點擊事件,進行路由帶參跳轉,參數為書籍路徑或書籍URL,跳轉到正文頁面。
      2. 進入正文頁面后,設置閱讀頁面,文字大小、字體樣式、行間距、段間距等。
      3. 根據書籍路徑或URL,獲取文件內容,根據章節拆分文件內容。
      4. 獲取每章的內容,按字拆分,根據顯示界面寬度、文字大小、是否換行等,得到每行顯示的內容,用數組進行存儲。
      5. 根據顯示界面高度和每行文字的高度(文字大小×行間距)得到每頁顯示的行數,使用slice得到當前頁、上一頁和下一頁的內容。
      6. 將當前頁的內容繪制在顯示區域內,上一頁繪制在當前頁的左邊,下一頁繪制在右邊,使用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,
              	})
              }
          }
          
         
      }
      
      
      • 正文繪制
      1. 首先根據書籍路徑或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
        }
      
      1. 根據當前章節編號(初始編號為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();
        }
      
      
      1. 繪制文本

      計算顯示區域內能顯示多少行(每頁行數=顯示高度/每行高度)(每行高度=文字大小*行間距),使用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);
            })
          }
      
        }
      
      
      1. 使用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') ,用???對日志輸出內容進行標記,方便查看。

      posted @ 2025-01-03 10:26  末雨摸魚  閱讀(245)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 老熟女高潮一区二区三区| 精品无人乱码一区二区三区的优势 | 亚洲午夜精品国产电影在线观看 | 久久精品国产再热青青青| 色橹橹欧美在线观看视频高清| 午夜福利理论片高清在线| 亚洲天堂激情av在线| 四虎影视一区二区精品| 日韩有码av中文字幕| 崇仁县| 久久国产精品精品国产色| 久久天天躁夜夜躁狠狠85| 亚洲综合网国产精品一区| 青草国产超碰人人添人人碱| 国产成人精品永久免费视频| 亚洲中文字幕日韩精品| 日韩激情一区二区三区| 日韩亚av无码一区二区三区| 国产精品久久久久久久专区| 石原莉奈日韩一区二区三区| 另类图片亚洲人妻中文无码| 亚洲一区二区中文av| 免费无码肉片在线观看| 亚洲综合一区国产精品| 日韩一区日韩二区日韩三区| 天美传媒一区二区| 国产精品久久久福利| 在线看av一区二区三区| 狠狠色噜噜狠狠狠狠蜜桃 | 久久月本道色综合久久| 亚洲国内精品一区二区| 上栗县| 国产精品一区二区三区三级| 色综合久久一区二区三区| 精品熟女少妇av免费久久| 亚洲av无码牛牛影视在线二区 | 国产又黄又爽又不遮挡视频| 亚洲AV无码国产永久播放蜜芽| 日韩在线视频线观看一区| 日韩卡1卡2卡三卡免费网站| 亚洲线精品一区二区三八戒|