首页 > 其他分享 >鸿蒙开发-阅读器正文页面实现

鸿蒙开发-阅读器正文页面实现

时间:2025-01-03 10:31:30浏览次数:1  
标签:章节 info console 鸿蒙 内容 context 阅读器 const 页面

鸿蒙开发-阅读器正文页面实现

记录开发一个小说阅读应用的过程

实现点击书籍,读取该书籍的文件内容,然后显示该书籍正文内容,滑动进行翻页。


实现逻辑

  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') ,用???对日志输出内容进行标记,方便查看。

标签:章节,info,console,鸿蒙,内容,context,阅读器,const,页面
From: https://www.cnblogs.com/ldh-blog/p/18649452

相关文章

  • 使用js写一个方法遍历输出页面中的所有元素
    在JavaScript中,你可以使用递归函数来遍历DOM树并输出所有元素。以下是一个简单的示例:functiontraverseAndLog(element){console.log(element);varchildren=element.children;for(vari=0;i<children.length;i++){traverseAndLog(children[i......
  • 「Mac畅玩鸿蒙与硬件52」UI互动应用篇29 - 模拟火车票查询系统
    本篇教程将实现一个模拟火车票查询系统,通过输入条件筛选车次信息,并展示动态筛选结果,学习事件处理、状态管理和界面展示的综合开发技巧。关键词条件筛选动态数据展示状态管理UI交互查询系统一、功能说明模拟火车票查询系统包含以下功能:用户输入查询条件:支持输入出发......
  • WPF通过外部资源文件为主页面控件编写样式
    1.增加外部样式文件,添加资源词典(WPF)文件创建公共样式文件GlobalStyles.xaml 编写样式文件,以<style>标记开头,TargetType属性为控件类型,如按钮“Button”,单选按钮“RadioButton”等,x:Key属性自定义命名,控件在引用属性的时候需将Style属性设置为{StaticResourcekey属性}......
  • 鸿蒙 NEXT 开发中,使用公共事件进行进程间通信
    大家好,我是V哥,在鸿蒙NEXT开发中,使用公共事件进行进程间通信(IPC)是一种常见的做法。下面我将提供一个完整的业务代码示例,并解释逻辑关系,以便即使是初学者也能按照步骤进行实操,废话不多说,直接上干货。业务场景假设我们有两个应用,应用A和应用B。应用A需要在电量低时通知应用B执......
  • 有度即时通参加广东鸿蒙生态伙伴论坛,共同推动鸿蒙生态建设
    近日,HDD·广东鸿蒙生态伙伴论坛在广州隆重举行。此次论坛由华为技术有限公司主办,广东软件行业协会协办,汇聚了众多业内精英和技术专家,共同探讨鸿蒙生态的最新进展与创新应用。有度即时通作为华为鸿蒙生态的重要合作伙伴参与此次盛会,与众多伙伴共同推进鸿蒙生态建设。    ......
  • 支持多种阅读格式!一个跨平台的电子书阅读器!
    大家好,我是Java陈序员。随着智能设备的流行,传统的纸质阅读渐渐被电子书阅读取代,不知道大家平时是怎么看电子书的呢?今天,给大家介绍一个跨平台的开源电子书阅读器!关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。项目介绍koodo-re......
  • 在移动端如何解决长时间按住页面出现闪退的问题?
    在移动端前端开发中,解决长时间按住页面出现闪退的问题,可以采取以下措施:禁用长按弹出的上下文菜单:通过CSS属性-webkit-touch-callout来禁用长按后出现的上下文菜单,这可以防止因长按导致的意外行为。例如,为元素添加样式-webkit-touch-callout:none;。优化事件处理:确保页面中的事......
  • Vue 2.0 学习(九、SPA-单页面应用 与 前端路由)
    文章目录一、前后端架构1.服务器生成页面2.前后端分离3.单页面应用-SPA二、路由1.服务器端路由2.前端路由3、结语一、前后端架构常见的前后端架构有服务器生成页面、前后端分离、单页面应用三种,它们有着各自的特点,可以根据实际需求选择不同的前后端架构......
  • 用户访问页面白屏了,原因是啥如何排查?
    以下是用户访问页面出现白屏问题的可能原因及排查方法:1.网络问题原因:用户的网络连接不稳定、速度慢或者网络中断,可能导致页面资源无法正常下载,从而出现白屏现象。排查方法:让用户检查自己的网络连接状态,尝试刷新页面或访问其他网站,以确定是否是网络问题。可以使用网络......
  • 如何解决页面请求接口大规模并发问题
    前端优化策略缓存数据浏览器缓存:合理利用浏览器缓存机制,通过设置正确的缓存头来减少不必要的接口请求。例如,对于那些不经常变化的数据(如网站的logo、样式文件等),可以设置较长时间的缓存。可以在服务器端返回数据时,在响应头中设置Cache-Control和Expires字段。如Cache-Cont......