在学习了iOS7新出的Text Kit的基础知识后,开始着手编写基于Text Kit的电子书阅读器程序。
首先是搭建程序的基本结构:RootView(导航视图)——BookListView(书本目录表视图)——ReadingView(阅读视图)——URLInteractionView(网页浏览视图)。
其中ReadingView是核心视图,几乎所有的阅读都在该页面中展开。
目前有很多电子书阅读器应用,大多数都有非常棒的翻页效果功能,而且,如果文本非常的长,用户只能通过滚动条来浏览电子书,这样的用户体验不好。所以我打算做成带翻页效果的阅读界面。
明显,翻页效果基于电子书分页,所以首先要进行电子书分页。
其实在iOS提供的sdk中就提供了UIPageViewController这个类来提供动态的翻页效果,但是在配置datasource时,必须静态配置好每一页的数据和总页数等,这种做法略显不够灵。而且我对该类的使用也不够熟悉,加上我想自己尝试去写出翻页效果的实现,最后我放弃了使用UIPageViewController。
(一)电子书分页
开始的分页方案是先计算文本总长度,然后设定一个每页字符数的标准,从而计算出总页数以及得出每页显示的文字及其范围。但是每页字符数的标准很难定下来,于是参考网上的文章进行了改进:http://mobile.51cto.com/iphone-227245.htm
该网页提供的参考代码如下:
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
[super viewDidLoad];
//
totalPages = 0;
currentPage = 0;
//
textLabel.numberOfLines = 0;
//
if (!text) {
// 从文件里加载文本串
[self loadString];
// 计算文本串的大小尺寸
CGSize totalTextSize = [text sizeWithFont:[UIFont systemFontOfSize:FONT_SIZE_MAX]
constrainedToSize:CGSizeMake(textLabel.frame.size.width, CGFLOAT_MAX)
lineBreakMode:UILineBreakModeWordWrap];
// 如果一页就能显示完,直接显示所有文本串即可。
if (totalTextSize.height < textLabel.frame.size.height) {
texttextLabel.text = text;
}
else {
// 计算理想状态下的页面数量和每页所显示的字符数量,只是拿来作为参考值用而已!
NSUInteger textLength = [text length];
referTotalPages = (int)totalTextSize.height/(int)textLabel.frame.size.height+1;
referCharatersPerPage = textLength/referTotalPages;
// 申请最终保存页面NSRange信息的数组缓冲区
int maxPages = referTotalPages;
rangeOfPages = (NSRange *)malloc(referTotalPages*sizeof(NSRange));
memset(rangeOfPages, 0x0, referTotalPages*sizeof(NSRange));
// 页面索引
int page = 0;
for (NSUInteger location = 0; location < textLength; ) {
// 先计算临界点(尺寸刚刚超过UILabel尺寸时的文本串)
NSRange range = NSMakeRange(location, referCharatersPerPage);
// reach end of text ?
NSString *pageText;
CGSize pageTextSize;
while (range.location + range.length < textLength) {
pageText = [text substringWithRange:range];
pageTextSize = [pageText sizeWithFont:[UIFont systemFontOfSize:FONT_SIZE_MAX]
constrainedToSize:CGSizeMake(textLabel.frame.size.width, CGFLOAT_MAX)
lineBreakMode:UILineBreakModeWordWrap];
if (pageTextSize.height > textLabel.frame.size.height) {
break;
}
else {
range.length += referCharatersPerPage;
}
}
if (range.location + range.length >= textLength) {
range.length = textLength - range.location;
}
// 然后一个个缩短字符串的长度,当缩短后的字符串尺寸小于textLabel的尺寸时即为满足
while (range.length > 0) {
pageText = [text substringWithRange:range];
pageTextSize = [pageText sizeWithFont:[UIFont systemFontOfSize:FONT_SIZE_MAX]
constrainedToSize:CGSizeMake(textLabel.frame.size.width, CGFLOAT_MAX)
lineBreakMode:UILineBreakModeWordWrap];
if (pageTextSize.height <= textLabel.frame.size.height) {
range.length = [pageText length];
break;
}
else {
range.length -= 2;
}
}
// 得到一个页面的显示范围
if (page >= maxPages) {
maxPages += 10;
rangeOfPages = (NSRange *)realloc(rangeOfPages, maxPages*sizeof(NSRange));
}
rangeOfPages[page++] = range;
// 更新游标
location += range.length;
}
// 获取最终页面数量
totalPages = page;
// 更新UILabel内容
textLabel.text = [text substringWithRange:rangeOfPages[currentPage]];
}
}
// 显示当前页面进度信息,格式为:"8/100"
pageInfoLabel.text = [NSString stringWithFormat:@"%d/%d", currentPage+1, totalPages];
}
////////////////////////////////////////////////////////////////////////////////////////
// 上一页
- (IBAction)actionPrevious:(id)sender {
if (currentPage > 0) {
currentPage--;
NSRange range = rangeOfPages[currentPage];
NSString *pageText = [text substringWithRange:range];
textLabel.text = pageText;
//
pageInfoLabel.text = [NSString stringWithFormat:@"%d/%d", currentPage+1, totalPages];
}
}
////////////////////////////////////////////////////////////////////////////////////////
// 下一页
- (IBAction)actionNext:(id)sender {
if (currentPage < totalPages-1) {
currentPage++;
NSRange range = rangeOfPages[currentPage];
NSString *pageText = [text substringWithRange:range];
textLabel.text = pageText;
//
pageInfoLabel.text = [NSString stringWithFormat:@"%d/%d", currentPage+1, totalPages];
}
}
这篇文章的分页算法的基本思想就是:先计算文本总尺寸
// 计算文本串的大小尺寸
CGSize totalTextSize = [text sizeWithFont:[UIFont systemFontOfSize:FONT_SIZE_MAX]
constrainedToSize:CGSizeMake(textLabel.frame.size.width, CGFLOAT_MAX)
lineBreakMode:UILineBreakModeWordWrap];
再根据textview的高度计算理想状态下的总页数和每页的字符数:
// 计算理想状态下的页面数量和每页所显示的字符数量,只是拿来作为参考值用而已!
NSUInteger textLength = [text length];
referTotalPages = (int)totalTextSize.height/(int)textLabel.frame.size.height+1;
referCharatersPerPage = textLength/referTotalPages;
接下来再根据referCharatersPerPage产生的文本尺寸结合textview的高度进行动态调整,使每一页的字符刚好铺满整个textview页面,并且将每页的字符范围存储到事先设定好的rangeOfPages指针所指定的内存区域当中。
在下面翻页时只需直接在referCharatersPerPage中取得文本范围并加载就可以了。
的确,这样的分页效果非常好,每一页的文字都刚好布满整个textview,页与页之间的连贯性非常好,而且在翻页时获取指定页的文本范围非常的简单。
但是非常糟糕的是这种分页算法的效率可谓奇低,例如对一篇最后分成4页的文本要几秒,对一篇分成200多页的文本可能要几分钟。另外,为了存储rangeOfPages,需要申请预定的内存空间,这又进一步增大了程序的开销。
尽管如此,该算法的思想还是可取的。
下面是我的分页的代码:
/* 判断是否需要分页和进行分页 */
-(BOOL)paging
{
/* 获取文本内容的string值 */
NSString *text = [bookItem.content string];
/* 获取Settings中设定好的字体(主要是获取字体大小) */
static const CGFloat textScaleFactor = 1.; // 设置文字比例
NSString *textStyle = [curPageView.textView tkd_textStyle]; // 设置文字样式
preferredFont_ = [UIFont tkd_preferredFontWithTextStyle:textStyle
scale:textScaleFactor]; //设置prferredFont(包括样式和大小)
NSLog(@"paging: %@", preferredFont_.fontDescriptor.fontAttributes); // 在控制台中输出字体的属性字典
/* 设定每页的页面尺寸 */
NSUInteger width = (int)self.view.bounds.size.width - 20.0; // 页面的宽度
NSUInteger height = (int)self.view.bounds.size.height - 40.0; // 页面的高度
/* 计算文本串的总大小尺寸 Deprecated in iOS 7.0 */
CGSize totalTextSize = [text sizeWithFont:preferredFont_
constrainedToSize:CGSizeMake(width, CGFLOAT_MAX)
lineBreakMode:NSLineBreakByWordWrapping];
NSLog(@"totalTextSize:w = %f,h = %f", totalTextSize.width, totalTextSize.height);
/* 开始分页 */
if (totalTextSize.height < height) {
/* 如果一页就能显示完,直接显示所有文本 */
totalPages_ = 1; // 设定总页数为1
charsPerPage_ = [text length]; // 设定每页的字符数
textLength_ = [text length]; // 设定文本总长度
return NO; // 不用分页
}
else {
/* 计算理想状态下的页面数量和每页所显示的字符数量,用来作为参考值用 */
textLength_ = [text length]; // 文本的总长度
NSUInteger referTotalPages = (int)totalTextSize.height / (int)height + 1; // 理想状态下的总页数
NSUInteger referCharactersPerPage = textLength_ / referTotalPages; // 理想状态下每页的字符数
// 输出理想状态下的参数信息
NSLog(@"textLength = %d", textLength_);
NSLog(@"referTotalPages = %d", referTotalPages);
NSLog(@"referCharactersPerPage = %d", referCharactersPerPage);
/* 根据referCharactersPerPage和text view的高度开始动态调整每页的字符数 */
// 如果referCharactersPerPage过大,则直接调整至下限值,减少调整的时间
if (referCharactersPerPage > 600) {
referCharactersPerPage = 600;
}
// 获取理想状态下的每页文本的范围和pageText及其尺寸
NSRange range = NSMakeRange(referCharactersPerPage, referCharactersPerPage); // 一般第一页字符数较少,所以取第二页的文本范围作为调整的参考标准
NSString *pageText = [text substringWithRange:range]; // 获取该范围内的文本
NSLog(@"%@", pageText);
CGSize pageTextSize = [pageText sizeWithFont:preferredFont_
constrainedToSize:CGSizeMake(width, CGFLOAT_MAX)
lineBreakMode:NSLineBreakByWordWrapping]; // 获取pageText的尺寸
// 若pageText超出text view的显示范围,则调整referCharactersPerPage
NSLog(@"height = %d", height);
while (pageTextSize.height > height) {
NSLog(@"pageTextSize.height = %f", pageTextSize.height);
referCharactersPerPage -= 2; // 每页字符数减2
range = NSMakeRange(0, referCharactersPerPage); // 重置每页字符的范围
pageText = [text substringWithRange:range]; // 重置pageText
pageTextSize = [pageText sizeWithFont:preferredFont_
constrainedToSize:CGSizeMake(width, CGFLOAT_MAX)
lineBreakMode:NSLineBreakByWordWrapping]; // 获取pageText的尺寸
}
// 根据调整后的referCharactersPerPage设定好charsPerPage_
charsPerPage_ = referCharactersPerPage;
NSLog(@"cpp: %d", charsPerPage_);
// 计算totalPages_
totalPages_ = (int)text.length / charsPerPage_ + 1;
NSLog(@"ttp: %d", totalPages_);
// 计算最后一页的字符数,防止范围溢出
charsOfLastPage_ = textLength_ - (totalPages_ - 1) * charsPerPage_;
NSLog(@"colp: %d", charsOfLastPage_);
// 分页完成
return YES;
}
}
我还是遵照了我最初的思路,在找每页显示的字符数的标准时则参考了网上那篇文章的思想:首先计算理想状态下每页的字符数并计算其尺寸,若其高度大于text view的高度,则减少每页的字符数,直至其高度符合标准,从而在一个text view中显示尽量多而又不超过视图范围的文字。
为了提高分页算法的效率,我做了一些小改进:
// 如果referCharactersPerPage过大,则直接调整至下限值,减少调整的时间
if (referCharactersPerPage > 600) {
referCharactersPerPage = 600;
}
如果每页的字符数过大,则快速调整至下限值。
referCharactersPerPage -= 2; // 每页字符数减2
若每页的文本尺寸大于text view的高度需要调整时,每页的字符数减2,这样递减的调整速度比-1快一倍,最差的预期是在text view中显示少一个文字。这样对文本的显示基本没有影响却换来一倍的效率。
对比之前的算法,由于不需要预先申请存储页面范围的内存空间,所以系统开销减小。更重要的是分页所需的时间大大减小。
使用也是非常简便,而且不需预先申请空间保存各页面的范围:
// set text in curPageView
if (currentPage_ == totalPages_ - 1) {
[curPageView.textView.textStorage setAttributedString:[[bookItem.content attributedSubstringFromRange:NSMakeRange(currentPage_ * charsPerPage_, charsOfLastPage_)] mutableCopy]];
}
else {
[curPageView.textView.textStorage setAttributedString:[[bookItem.content attributedSubstringFromRange:NSMakeRange(currentPage_ * charsPerPage_, charsPerPage_)] mutableCopy]];
}
curPageView.textView.font = preferredFont_;
直接用
NSMakeRange(currentPage_ * charsPerPage_, charsOfLastPage_)]
存取即可。
另一方面可以设定一个私有变量preferredFont_便于在用户调整字体大小时重置text view中的字体。
不足之处:
1.对于近80000字符数的文本的加载可能也需要3秒左右时间加载,明显不够快,在此我的进一步改进的设想是将首次加载文本时时计算好的charsPerPage和对应的电子书名保存起来,在下次加载时直接从保存好的数据中加载charsPerPage,这样非首次加载文本就可以免去分页计算的时间。但是我还没想好用哪种方法保存数据,有待改进。
2.最大的不足是由于限定了每页的字符数,所以难免会出现每页的显示会出现参差不齐,例如每页显示的文本高度不同,页与页之间的连贯性不够好,等等。
3.sizeWithFont:constrainedToSize:lineBreakMode:方法已经被iOS 7.0建议Deprecated
/* 计算文本串的总大小尺寸 Deprecated in iOS 7.0 */
CGSize totalTextSize = [text sizeWithFont:preferredFont_
constrainedToSize:CGSizeMake(width, CGFLOAT_MAX)
lineBreakMode:NSLineBreakByWordWrapping];
可以用iOS7新出的方法来代替:
- (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options attributes:(NSDictionary *)attributes context:(NSStringDrawingContext *)context NS_AVAILABLE_IOS(7_0);
即通过计算字体高度和字符行距之和来得出文本的大小尺寸。这一点要改进。
(二)字体调整
若用户在Settings中调整字体时,text view中的字体要作出相应的变化。
其实也非常简单,首先在消息中心注册消息接受者:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(preferredContentSizeChanged:) name:UIContentSizeCategoryDidChangeNotification object:nil]; // 当不同类别的字体大小发生变化时发送消息给self
实现方法:
// 当消息中心收到用户在settings中调整字体大小的消息
-(void)preferredContentSizeChanged:(NSNotification *)noti
{
static const CGFloat textScaleFactor = 1.; // 设置文字显示比例
NSString *textStyle = [curPageView.textView tkd_textStyle]; // 设置文字样式
preferredFont_ = [UIFont tkd_preferredFontWithTextStyle:textStyle
scale:textScaleFactor]; // 设置preferredFont_(包括样式和大小)
NSLog(@"%@", preferredFont_.fontDescriptor.fontAttributes);
curPageView.textView.font = preferredFont_; // 设置text view中的字体
}
由于preferredFont_是私有变量,可以在整个.m文件中实现,所以在以上的实现方法中可以设定preferredFont_为调整后的字体。
由于在翻页时要加入新页面addPageView(下面会讲),所以可以在加入新页面后通过preferredFont_设定字体:
addPageView.textView.font = preferredFont_;