电子书分页,翻页效果,字体大小调整和基本功能实现

在学习了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_;

 

转载于:https://www.cnblogs.com/nkls/p/8559712.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值