H5小说阅读器左右翻页研究

H5小说阅读器左右翻页研究

H5小说阅读器左右翻页研究


1.背景

之前做了手 Q 阅读阅读页功能的开发,用 H5 实现了一个简单的的小说阅读器,其中左右翻页这个功能相对复杂,线上采用了相对简单 css3 column 多栏布局的方式实现,项目完成后又做了相应的调研和其他方案的尝试,这里主要总结了纯文本的小说阅读器左右翻页功能实现的几种方案。


2.分析
通常都是拿到书籍的一章内容进行渲染,切换章节时替换为新的内容,如果不考虑左右翻页这个功能,只需要把书籍的内容渲染在页面上,设置好相应的样式,上下滑动页面即可阅读。但是为提高用户体验,实现左右翻页是一个必不可少的功能。如下是 QQ 阅读客户端左右翻页效果:

H5小说阅读器左右翻页研究 H5小说阅读器左右翻页研究

要实现类似于左右翻页的效果,首先就是要实现内容的分页,然后展示在多个容器元素上,最后为容器元素切换时加上动画效果。整个过程和印刷书一样,需要将文章根据纸张印刷区的大小,去划分到每一页的文本内容;我们的目的也是将文本内容根据屏幕绘制区的大小去计算出每一屏该展示的内容。
计算时需要考虑很多影响分页数据的因素,如不同尺寸的手机屏幕、文本字体的大小等等。


3.实现

CSS3 column 多栏布局方案

多栏布局可以将内容分隔成多列进行展示,类似于报纸的栏目排版,这里可以利用多栏布局来模拟分页效果。

H5小说阅读器左右翻页研究

css3 column 多栏布局的兼容性相对较好,IE 10+ 以及其它现代浏览器都支持,使用时还是建议加上个浏览器前缀。

H5小说阅读器左右翻页研究

实现多栏布局主要用到如下属性:
column-width: 每栏的宽度,设定后会根据容器宽度自动分列(真实宽度受容器尺寸影响);column-count: 分栏个数;column: column-width 和 column-count 的简写,俩值转成同一属性,谁小用谁,为了计算准确一般都写上 column: 100px 1;column-gap: 每栏之间的间隔,默认 1em;


H5小说阅读器左右翻页研究

更多其他属性可以看看 MDN 的介绍 :https://developer.mozilla.org/en-US/docs/Web/CSS/columns


根据上文的介绍,为容器设置不同的分栏宽度,可自动实现其子元素内容的拆分;当分栏宽度与分栏间隙之和刚好为一页宽度时,就模拟出了分页效果。如下图:

H5小说阅读器左右翻页研究

H5小说阅读器左右翻页研究

接下来可通过 JS 的 touch 或点击事件,控制容器的 translateX 属性,每次将容器移动一个分栏宽度与分栏间隙之和的距离,即可实现上一页或下一页内容展示在视口中,同时增加 animation 属性可以在切换分页的时候设置动画效果。

H5小说阅读器左右翻页研究



章节切换处理
为提高用户的阅读体验,减少切换章节时的等待时间,加载完当前章节内容后,需要对下一章或上一章内容进行预加载,缓存在内容中,切换章节时,只需要用下一章的内容替换成上一章对应的内容,重新渲染即可,这里需要注意两点:
1、预加载的时机,可首次加载两章,每阅读完一章,拉取一次。如首次加载1、2章,读完第1章,拉取第3章,根据用户阅读进度去预加载;
2、需要注意缓存内容过多造成内存使用过高影响性能,可通过一定的策略去触发清除无用的缓存内容。

往下一章切换时,待内容渲染成分页平铺后,只需要将容器元素的 translateX 值置零,会就展示第一页内容;比较特殊的情况是向上一章翻页时,需要展示上一章的最后一页,由于是通过浏览器自动分页,该如何准确的移动到最后一页呢?
已知分栏的宽度,只需要计算出当前内容拆分成多少页,就可以任意指定展示到具体哪一页的内容;经过试验发现通过元素的 scrollWidth 属性能得到内容总宽度,它不仅包括可视区域的宽度,还包括隐藏区域的宽度;利用容器总宽度和分栏的宽度可以计算出总页数,这样既可解决向上一章翻页时展示最后一页内容的问题。

H5小说阅读器左右翻页研究

这种方案的实现相对容易,并且上下滑动模式下通过 column 属性直接适配成左右翻页,实现自动分页效果,不需要复杂计算,但是也存在着不少问题:
1.一次性需要将所有内容全部排列并渲染,内容过多时会影响页面性能;
2.分页不可人为控制,不方便扩展(在某一页后面增加广告页);

3.翻页只能支持滑动效果,并不能支持“覆盖”、“仿真翻页”效果。


如果我们自己要去实现分页该怎么做呢?理想情况是在页面渲染之前就拿到分页数据,翻页时取对应页数的数据进行渲染;
思路:计算出在内容区域内每一行可以最多渲染多少文字,再通过行高、段间距计算出每一页可以渲染多少行,这样就可以计算出整章的分页数据;以行为单位去计算的好处是当一个段落文字跨页时,可以直接把这段文字分成两部分划分到对应的分页数据里。如何计算每一行可以渲染多少文字是个大问题。

经过调研发现 canvas  的 API  有个 measureText方法,它可以返回一段文本的尺寸,包括宽度、高度等。

H5小说阅读器左右翻页研究


Canvas 绘制方案
首先需要知道 canvas 在绘制文本的时候是不能自动换行的,只能一行一行绘制;根据上面的思路计算得到每一行的文本也正好符合 canvas 的绘制特性。

拿到章节数据后对每个段落文本进行遍历,逐字增加去判断当前截取的这段文字宽度是否超出绘制区宽度,直到这段文字的宽度大于了绘制区的宽度,那么这段文字就是这一行能渲染的内容,同时将这行文字存下来,然后进行下一行的判断,处理完一个段落内容后再对下一个段落进行相同的处理(文末附有代码)。

H5小说阅读器左右翻页研究

处理完每段内容后都进行一下行数的计算,通过当前设置的行高、段间距等能计算出一页能最多渲染多少行,这样就能计算出整张内容的分页数据。

H5小说阅读器左右翻页研究

最终可得到的数据结构如下,同时还可以对数据做自定义处理,例如 isStart 表示是否是一个段落内容的第一行。

H5小说阅读器左右翻页研究

然后通过 canvas 绘制对应的页面,在翻页处理上仅需同时维持2~3页内容,加上相应的动画方案即可,不用渲染出全部分页。

H5小说阅读器左右翻页研究

这种方式可对数据自定义处理,同时翻页方式可扩展,但是计算相对复杂。

Canvas 解析 + HTML渲染方案
在分页数据解析方面和上一个方案一致,只是在存储分页数据和渲染时有区别。
通过 canvas 解析后,能够得到按行整合的每一页数据,然后再对每一行的数据按段进行合并,这样也能够得到按段整合的每一页的数据,如下两种整合方式的结构:

H5小说阅读器左右翻页研究

这样可以将每段的内容直接通过 p 标签渲染。

H5小说阅读器左右翻页研究

但是需要注意的是,canvas 上下文设置的字体、字号、行高、字间距等都需要和 HTML 标签设置保持一致,否则会出现误差,如 HTML 标签字间距过大时,刚好计算出一行的内容渲染在 HTML 标签中时会出现换行的情况。

4.总结
第一种方案主要是基于 CSS3 分栏实现的自动分页,后两种是通过 Canvas 解析数据进行分页处理,目前还在不断优化阶段,可以作为一种思路来参考,实现过程还有很多细节需要处理,这里不展开说,如果大家有其他的方案和建议,希望不吝赐教,一起学习!

附:

canvas 解析分页数据的部分代码思路:


chapterData(content) {  // 已经解析过的内容按行加入数组  var hasResolved = [];  var hasResolvedByP = [];  // 已解析行数  var hasResolvedLength = 0;  // 上一页最后的段落下标  var current = this.curPage.end.pIndex;  // 是否是每屏的第一行  var firstLine = true;  var lineHeight = 20;  var spacing = 30;  // 当前段落索引 < 总段落长度  while (current < content.length) {    // 当前段落    var curParagraph = content[current];    // 解析当前段落按行加入数组    var resolving = [];    // 带有行间距的数据结构    var withLineHeightResolving = []
// 每行的起始位置文字的索引 var start = 0; if (firstLine) { // 上一页最后的段落的文字偏移量 start = this.curPage.end.offset; firstLine = false; } // 计算每行的文字宽度,超过了行宽就重新解析下一行 for (var i = start; i < curParagraph.length; i++) { if (this.ctx.measureText(curParagraph.substring(start, i)).width > (window.innerWidth - 50)) { resolving.push(curParagraph.substring(start, i)); if (start === 0) { withLineHeightResolving.push({ text: curParagraph.substring(start, i), // 每一段的第一行 isStart: true }); } else { withLineHeightResolving.push({ text: curParagraph.substring(start, i) }); } start = i; } } // 最后一行没有满一行时,全部加入 if (start < i) { resolving.push(curParagraph.substring(start, i)); // 当前段只有一行 if (start === 0) { withLineHeightResolving.push({ text: curParagraph.substring(start, i), isStart: true }); } else { withLineHeightResolving.push({ text: curParagraph.substring(start, i) }); } } // 记录当前页的最后一个字的索引值 var offset = 0; // 当前段解析完成后,计算是否已经满了一屏, 没有满屏则解析下一段落 // 总行高 + 总段间距 > 绘制区高 则超出一屏 if ((hasResolvedLength + resolving.length)*lineHeight + (current - this.curPage.end.pIndex)*spacing) > (window.innerHeight - 64 - 20)) { // 找出最后一段的字偏移量 for (var j = 0; j < resolving.length && j + hasResolvedLength < totalRow; j++) { offset += resolving[j].length; hasResolved.push(withLineHeightResolving[j]); } // 记录最后一段的信息 this.curPage.end.pIndex = current; this.curPage.end.offset = offset; // 按段加入 hasResolvedByP.push(curParagraph.substring(0, offset))
// 一屏的内容 this.chapter.pdata[this.chapter.pcount] = hasResolved; this.chapter.pdata2[this.chapter.pcount] = hasResolvedByP; // 下一屏开始前要重置 hasResolved = []; hasResolvedByP = []; hasResolvedLength = 0; firstLine = true; this.chapter.pcount++; } else { // 将当前段落的分行数组合并到页面的数组里 hasResolved = hasResolved.concat(withLineHeightResolving); hasResolvedLength += resolving.length hasResolvedByP.push(resolving.join('')) // 继续绘制下一个段落 current++; } } /* 检查是否正常跳出满屏---如果没有 表示到达书尾,直接加入 */ if (hasResolvedLength) { this.chapter.pdata[this.chapter.pcount] = hasResolved; this.chapter.pdata2[this.chapter.pcount] = hasResolvedByP; }}


H5小说阅读器左右翻页研究


本文作者:关小峰

转载请向阅文体验设计微信公众号(id:YUX_design)获取授权,并注明作者、出处和链接。

欢迎大家关注我们的站酷以及知乎账号:阅文体验设计YUX


本篇文章来源于微信公众号: 阅文体验设计YUX

UI/UX

腾讯CoDesign|用 Figma 插件上传设计稿 无须注册也能查看标注

2021-4-6 18:07:50

UI/UX

设计研究院 Vol.5 | B-Metric,企业产品体验度量极简指南

2021-4-7 9:58:00

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索