最近为了作业的课设,做了一个app,主要是浏览、下载和阅读小说,谈到阅读,就要谈一谈排版、字体、分页。当然,除了分页的部分,其他的使用textview都可以很好地解决,这里总结一下这么一个比较简单的分页方法,针对从txt文本获取的NSString进行分页。

Text Kit

1.NSLayoutManager

管理文本排版、字体

2.NSTextStorage

存储文本

3.NSTextContainer

文本容器

详细的介绍可以参考apple开发者文档,主要是首先按照给定的字体和排版求出整个文本占据的最小矩形框的高度,然后除以单页文本的矩形框高度,结果加一来补全最后可能缺失的页数。

工程使用默认的singleview application,txt文档用的是utf-8编码的一本小说,大小约500kB

下面是代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
@interface ViewController ()

@property(nonatomic,strong) NSLayoutManager *textLayout;

@property(nonatomic,strong) UITextView *textView;

@property(nonatomic,strong) NSString *aText;

@end

//模拟器使用iPhone5

//屏幕分辨率640X1136

//点数为320X568

//textview的frame设为320X568

//textcontainer的size设置为310X560(可以打印一下textview你会发现它的textcontainer的size的宽度与textview的宽度默认相等,高度总是小8个点)

- (void)viewDidLoad {
  [super viewDidLoad];
  // Do any additional setup after loading the view, typically from a nib.
  //如果是使用gbk编码的文档,要使用gbk解码
  // NSStringEncoding gbk = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000);

  //加载文本,打印长度
  NSString *path = [[NSBundle mainBundle] pathForResource:@"11877" ofType:@"txt"];
  _aText = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
  NSLog(@"%d",_aText.length);

  //设置文本属性,这里只改变一下字体大小
  NSAttributedString *textString =  [[NSAttributedString alloc] initWithString:_aText attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:14]}];

  //使用 NSString的方法计算文本高度,这里做个对比
  CGRect rect = [textString boundingRectWithSize:CGSizeMake(310, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin context:nil];

  NSLog(@"%f",rect.size.height);
  
  //将文本存入textStorage
  NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:textString];
  _textLayout = [[NSLayoutManager alloc] init];
  //为textStorage添加一个NSLayoutManager
  [textStorage addLayoutManager:_textLayout];

  //接下来计算分页数
  NSTextContainer *atextContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(310, MAXFLOAT)];
  [_textLayout addTextContainer:atextContainer];
  //上面代码先创建一个文本容器,宽度为310,高度为最大单精度浮点数,然后将它添加到textLayout,不添加的话,是无法计算下一步的矩形框高度

  //计算整个本文长度的字形在所给的文本容器中,所占据的矩形框的高度,然后将该文本容器移除
  float textViewHeight1 = [_textLayout boundingRectForGlyphRange:NSMakeRange(0, _aText.length) inTextContainer:atextContainer].size.height;
  [_textLayout removeTextContainerAtIndex:0];
  NSLog(@"%f",textViewHeight1);

  //总高度向上取整,可选,不知道会不会对性能造成影响
  textViewHeight1 = ceilf(textViewHeight1);
  NSLog(@"%f",textViewHeight1);

  //重复刚才的工作,这次创建一个供单个页面使用的文本容器,然后添加到textLayout
  NSTextContainer *atextContainer1 = [[NSTextContainer alloc] initWithSize:CGSizeMake(310, 560)];
  [_textLayout addTextContainer:atextContainer1];

  //计算整个本文长度的字形在所给的单页文本容器中,所占据的矩形框的高度,注意需要把文本容器填满,你可以设置一个较大的数值,或者直接使用整个文本的长度,然后将该文本容器移除
  float textViewHeight2 = [_textLayout boundingRectForGlyphRange:NSMakeRange(0, _aText.length) inTextContainer:atextContainer1].size.height;
  [_textLayout removeTextContainerAtIndex:0];
  NSLog(@"%f",textViewHeight2);

  //单页高度向下取整,可选,但不要向上取整,不然偏差会很多,特别是分页数较多时   
  textViewHeight2=floorf(textViewHeight2);
  NSLog(@"%f",textViewHeight2);

  //计算分页数,加1补全可能缺失的最后一页,也可以不取整时,直接除后加一
  int count = textViewHeight1/textViewHeight2+1;
  NSLog(@"%d",count);
  int i=0;
  while (i<count) {
    NSTextContainer *atextContainer3 = [[NSTextContainer alloc] initWithSize:CGSizeMake(310, 560)];
    [_textLayout addTextContainer:atextContainer3];
    i++;
    //这里还有一个方法用于保存分页结果
    //我们把结果存储到一个数组中,最后写入文件,第二次加载时就不用再次分页了
    NSString *ran = NSStringFromRange([textLayout glyphRangeForTextContainer:atextContainer3]);[_pageArray addObject:ran];
  }

  // 注意现在textLayout的textContainers 数组为空,我们添加count个文本容器,每次添加,可以想象成在整个文本所占据的矩形框上,以单页的高度为单位不断下移
  NSTextContainer *atextContainer3 = [_textLayout.textContainers lastObject];

  //获取处理后的textLayout.textContainers中的最后一个文本容器,看看是否分页成功
   _textView = [[UITextView alloc] initWithFrame:self.view.bounds textContainer:atextContainer3];
  [self.view addSubview:_textView];
  NSLog(@"%@",_textView.textContainer);
}
    
- (void)didReceiveMemoryWarning{
  [super didReceiveMemoryWarning];
  // Dispose of any resources that can be recreated.
  float textViewHeight = [_textLayout usedRectForTextContainer:_textView.textContainer].size.height;
  NSLog(@"%f",textViewHeight);
  // 在模拟器页面按住shift+command+m可以发送内存警告,调用此方法,获取信息
}
    
-(BOOL)prefersStatusBarHidden
{
  //全屏模式
  return YES;
}

以下是打印的结果

1
2
3
4
5
6
7
8
 2015-04-26 14:51:29.912 pageProject[3089:91446] 194658(文本长度)
 2015-04-26 14:51:30.292 pageProject[3089:91446] 211558.859375(NSString方法计算的高度)
 2015-04-26 14:51:30.723 pageProject[3089:91446] 215751.343750(整个文本的高度)
 2015-04-26 14:51:30.723 pageProject[3089:91446] 215752.000000(向上取整的高度)
 2015-04-26 14:51:30.724 pageProject[3089:91446] 551.165955(单个页面中文本的高度,小于560)
 2015-04-26 14:51:30.725 pageProject[3089:91446] 551.000000(向下取整的高度)
 2015-04-26 14:51:30.725 pageProject[3089:91446] 392(总页数)
 2015-04-26 14:51:30.784 pageProject[3089:91446] <UITextView: 0x7db5ec00; frame = (0 0; 320 568); text = ★☆★☆★☆轻小说文库(Www.WenKu8.COM...; clipsToBounds = YES; gestureRecognizers = <NSArray: 0x7bfc4df0>; layer = <CALayer: 0x7bfa2b10>; contentOffset: {0, 0}; contentSize: {320, 560}>

可以看到最后的contentSize属性的大小为320X560,它是文本的textcontainer的最大尺寸

1
 2015-04-26 14:51:30.784 pageProject[3089:91446] <NSTextContainer: 0x7bfc4860 size = (310.000000,560.000000); widthTracksTextView = NO; heightTracksTextView = NO>; exclusionPaths = 0x0; lineBreakMode = 0

这是文本容器的信息

1
2
 2015-04-26 14:51:53.762 pageProject[3089:91446] Received memory warning.
 2015-04-26 14:51:53.762 pageProject[3089:91446] 233.827957

收到内存警告后的信息,可以看到实际页面中文字占据的矩形高度小于560,最后一面没有填满

总结

1.没啥总结,从打印的信息可以看到,按这么分页,500k左右的文本分页时间不长,但是对于较大的文本如7.9m的,不会崩溃,只是要花一点时间来卡住。总的来说,最耗时的在于全文本计算矩形和创建与分页数相同个数的文本容器,而对于这种500k左右或以下的文档,耗时影响不大,使用菊花图和预加载的话,分页和创建容器都很快;

2.文本容器不会被完全填满,所以如果不计算单个文本容器内的文本高度而直接使用文本容器高度的话,分页数量会不够;

3.didReceiveMemoryWarning中使用的usedRectForTextContainer方法只能计算已经显示在textview中的文本容器内的文本占据的高度,就是说文本容器不加载进视图时,是用不了的,只会返回0.0000

4.计算文本高度的关键函数

1
-  (CGRect)boundingRectForGlyphRange:(<NSRange)glyphRange inTextContainer:(NSTextContainer *)container;

layoutManager只能对已经添加到它的文本容器数组内的文本容器使用,直接用的话,还是返回0.0000,所以每次算完后,都会移除掉文本容器,因为文本容器内的文本是连续的,如果上一个容器高度太大,占据了整个文本,下一个就是空白页面了;

5.如果要做成阅读器,可以在计算分页前放菊花图,同时先创建数个文本容器,获取textLayout的容器数组的copy,接着加载小说的首几页,同时在后台完成分页。注意不要在获取copy后立刻启用后台线程添加文本容器,多线程下会造成对同一个数组操作而报错。翻页时,可以先把textview从父视图移除,重新alloc一个textview,然后添加,效果挺好,没卡顿,and~textview的创建必须在主线程下,不能放到后台,否则报错。

6.可以明显看到的是使用 NSString方法计算的高度和真实高度相差很大,如果直接用它去分页,单页文本内容会过多,然后显示不全,或者做个奇葩的单页滚动的textview来显示(手势会发生冲突),之前尝试过使用它,步骤如下:

  • 用nsstring的方法算出高度,除以view.frame的高度,获得count;
  • 用文本的length除以count,获得单页的字符串长度;
  • 写个循环,把substringWithRange获得的文本子字符串全部加入数组;
  • 覆盖uiview的drawrect方法,使用相同的attribute直接绘制子字符串;

问题是几乎所有页面都显示不完全,偶尔几页会显示完整,也许是直接拆分nsstring,没处理好换行和其他控制字符,目前不造怎么破, 还是Text Kit 大法好;

7.用wordpress写起来比markdown感觉方便多了~~~有空的话,我会尝试其他的函数,再写一些。