对电子书阅读器来说,提示当前阅读进度是一项很自然的功能,习惯用电脑的人都常看到进度条 (progress bar) 和滚动条 (scroll bar),如右图是 Textus 中使用的右侧滚动条。
然而在实际实现中,进度计算是一件伤脑筋的事情,比如 Textus 的实现其实很简单:在打开文件时读入整个文件,然后整个交给 Core Text 去排版,将排版后的结果分解为行 (CTLineRef) 记录下来,并将所有行总的高度设置为整个文本视图的高度,这样,每当滚动视图 (NSScrollView) 移动到某个位置时,重绘函数 (-drawRect:) 被调用到,我们根据该位置来判断应该绘制从第几行到第几行的内容,再调用 Core Text 把这些行画出来。
这么做看似很简单直接,结果也很容易保证正确,带来的问题是,每次用户修改设置 (比如调整字体大小、窗口尺寸) 时,就得把整个文件重新排版一遍,即使此时我们只需要看到当前一页的内容。为什么这种方法这么低效,我还一直使用它呢?因为这个实现严格依赖滚动视图给出的位置来判断当前阅读进度,所以总的高度估计必须非常精确,不然随便滚动一下就可能出现错位,而一次算给出整个高度的方法最准确,不容易出错。
在 iTextus,也就是 iPad 版本的文本阅读器中,我打算换一种方法,主要的原因是:
- 大家在手持设备中都不喜欢用进度条,因为频繁滑动比较烦人
- 分页方式实现起来比较简单高效,而 iPad 的性能有限
其实从文件载入到显示出来,90% 的时间花费在排版上 (对于 Core Text 程序,就是 CTFramesetterCreateWithAttributedString 这一步),剩下 5% 用来读取文件,5% 用来绘制排版结果。所以在 iPad 上我们尽可能的减少排版时间,最简单的方法就是把这个时间均摊到每页上,这样每页的排版时间就几乎可以忽略不计了。另一方面,这样也能减少程序的内存占用,在内存紧张的时候可以简单地回收几个页面的排版数据,而不必清空所有的。
既然采用分页方式,进度显示就很灵活了,我们先看看常见的 iPhone 上电子书阅读器是怎么做的。
Stanza 在全屏阅读状态时,除了页面底部用不同颜色显示已读和未读进度比例之外,没有任何其他的提示,这样看起来非常简洁。
触碰页面中部之后 Stanza 会出现一个更详细的界面,包括上方的导航栏提供了书名和作者,中间提示了章节、页数和进度的百分比,下方工具栏还提供了直接通过拖拉跳转页面的功能。
GoodReader 在全屏阅读时干脆什么都不显示,触碰页面中部之后的界面和 Stanza 差不多,不同之处是把页面和跳转进度条放到了左边。
Eucalyptus 采用的是模仿真实书籍的方式,全屏时在页面上方显示书名和页码。
在触碰页面中部之后,Eucalyptus 显示的界面和 Stanza 差不多,更简单一点,中间没有放置任何 HUD 控件,页码放在了下方,而且干脆也不提供任何设置功能了。
Classics 的方式更有趣一些:它没有单独的信息界面,导航栏是一直保持在页面上部的,书名在导航栏上,然而它改写了导航栏,在上面通过颜色区分显示阅读过和尚未阅读的进度比。下方则用浅色显示章节名称和页码。
其他几个阅读器主要是考虑 iPhone 界面空间非常宝贵,导航栏和状态栏没必要随时保持在页面上,把它们在用户要求时单独显示出来反而不容易干扰阅读,而 Classics 这种做法也是可以理解的,因为它复用了导航栏,算是节省了一些空间,这样实现也简单一点。
此外,Stanza 和 GoodReader 都支持横屏显示,而 Eucalyptus 和 Classics 都不支持。
目前我在 iTextus 中实现的是类似 Stanza 的进度提示方式,如下图所示,因为这是最容易实现的,至于最后是否应该选择这样的方式,我还没有决定,也欢迎你的意见。
与进度显示紧密相关的是进度的估计,因为我们既然不能一次排版完所有内容,就必须有个合适的方法估计当前阅读内容的总页数,目前我采用的一个简单的方法如下:
- 因为我们一次读取整个文件的内容,所以一开始我们就知道当前文件的总长度 (字符数量)
- 但是每页可能有不同数量的字符 (比如有的页面段落、换行较多,有的则较密集),可是我们假设平均数量是一个稳定值
- 总的估计页数 = 文件总的字符数量 / 当前页面平均字符数得到
- 一开始只排版了一个页面,那当前的页面平均字符数就是第一页的字符数,此后每排版一个新的页面,我们可以让这个平均值更精确一点
经过实际使用的尝试,对于较长的文件 (比如文本长度 100KB 以上的),这个页数估计是非常精确的,就算偶尔出现震荡也不会超过一两页,不会影响用户对进度的体验。
前不久 Instapaper 的开发者 Marco 提出了一个新的想法:根据时间跟踪和估计进度,大致构思如下:
- 从用户打开一个新的文件开始,记录用户阅读该文件的总时间
- 用户的平均阅读速度 = 该文件总阅读时间 / 当前已阅读的页数 (或者字数?)
- 根据总的页数或者字数和用户的平均阅读速度推算要读完这本书要花费的总时间
- 可以在界面中显示从现在开始,如果不间断,将在什么时刻读完这本书
这毫无疑问是一个比较有意思的想法,其实游戏和播放器都经常采用这样的方式 (时间记录和完成时间估计),但用在书籍、文章阅读时会起到积极的作用吗?会不会有些副作用?不惯怎么说,应该是值得尝试的。




9 Comments