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

 

posted on 2018-03-13 17:56  Nikogls_Hu  阅读(1807)  评论(0编辑  收藏  举报