准备

首先分页和绘制文本所用的NSAttributedString必须是一样的,包含相同的字体,段落格式,其中字体必须支持要绘制的文本语言,否则会在绘制时使用默认的字体代替

绘制文本其实就是在绘图,所以能够缓存更多纯文本外的信息,就可以提升效率,我们用来显示文本的容器有:

1.使用系统控件UITextView,UILabel,甚至是UIButton,需要配合TextKit使用

2.使用CATextLayer,但它有自己的段落格式,适合少量文本

3.使用UIView,重写drawRect方法,在上下文中直接绘制,通过setNeedsDisplay调用

4.使用CALayer,重写drawInContext,同UIView

5.使用UIImageView,把绘制结果保存成图片显示

分页

分页主要是取得每页的文本NSRange,文本长度较小时,使用TextKit

TextKit

(1)初始化NSTextStorage,NSLayoutManager,NSTextContainer

(2)计算总文本容器高度与单页文本容器高度

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

两者相除结果加1即为总页数

(3)在一个while循环中创建文本容器并添加到layoutManager,然后调用

1
- (NSRange)glyphRangeForTextContainer:(NSTextContainer *)container;

获取每页文本范围

(4)保存结果

追求效率的话,就必须使用CoreText

CoreText

(1)转换NSAttributedString为CFAttributedStringRef

(2)创建CTFrameSetterRef

(3)进入循环,首先添加路径,然后创建CTFrame,用文字填满Frame,然后获取页面内文字范围,然后保存结果

(4)释放CTFrameSetterRef

 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
CFAttributedStringRef cfAttStr = (__bridge CFAttributedStringRef)str;
<span style="color: #339966;">//直接桥接,引用计数不变</span>
 CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(cfAttStr);
 int textPos = 0;
 _totalPage = 0;
 _currentPage = 0;
 NSUInteger strLength = [str length];
 while (textPos < strLength)
 {
 CGPathRef path = CGPathCreateWithRect(textFrame, NULL);
<span style="color: #339966;">//设置路径</span>
 CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(textPos, 0), path, NULL);
<span style="color: #339966;">//生成frame</span>
 CFRange frameRange = CTFrameGetVisibleStringRange(frame);
 NSRange ra = NSMakeRange(frameRange.location, frameRange.length);
 [_pagingResult addObject:NSStringFromRange(ra)];
<span style="color: #339966;">//获取范围并转换为NSRange,然后以NSString形式保存</span>
 textPos += frameRange.length;
<span style="color: #339966;">//移动当前文本位置</span>
 CFRelease(frame);
 CGPathRelease(path);
 _totalPage++;
<span style="color: #339966;">//释放路径和frame,页数加1</span>
 }
 CFRelease(framesetter);
<span style="color: #339966;">//释放frameSetter
</span>

这里还要提一下NSString的计算文本矩形框的方法

NSStringDrawing

1
2
3
- (CGRect)boundingRectWithSize:(CGSize)size 
                       options:(NSStringDrawingOptions)options 
                       context:(NSStringDrawingContext *)context;

它计算出的高度与使用CoreText计算出的高度是相同的,但应该仅限用于不分页的情况,因为网上的一些方法就是使用它先计算总高度,然后除以单页高度,然后结果加1,用这个结果对整个文本长度分段,当然,还有一些修正以适应页面,然后所有页面的文字长度都是相同的,的确这样效率非常高,但这个方法是错误的。为什么呢?对于文本中,某些片段内如果有大量的换行,此时换行是作为一个字符长度计算的,可想而知,绘制时有发生文字溢出页面的情况,所不推荐使用它来分页。

绘制

(1)最简单的方法

直接将NSAttributedString的子字符串赋值给控件的属性,比如UITextView的attributedString和CATextLayer的string,让控件自己绘制

(2)在上下文中绘制

1.使用CoreText在上下文中绘制,我们需要做的与上文中使用CoreText相同,只要增加一个在上下文中绘制的函数

1
CTFrameDraw(frame, context);

2.使用NSStringDrawing,我们使用NSAttributedString,直接绘制即可,如果是NSString,需配置字典

1
[str drawInRect:textFrame];

最后,如果是重写drawRect或者drawInContext,需要调用UIView或者CALayer的setNeedsDisplay在下一个绘图周期绘制,我采用的方法是直接赋值给CALayer的contents,这样会有动画效果,代码如下

1
2
3
4
5
6
7
8
9
-(void)setNewStr:(NSAttributedString *)str
{
 UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0);
 [st drawInRect:textFrame];
 CGImageRef image = (CGBitmapContextCreateImage(UIGraphicsGetCurrentContext()));
 UIGraphicsEndImageContext();
 self.contents = (__bridge id)image;
 CGImageRelease(image);
}

这里需要注意的一点是,如果使用单一字体,比如默认中文字体来绘制中英文,CoreText的分页结果不会在使用NSStringDrawing时超出屏幕,但使用系统字体时,行高按较大的默认英文字体,绘制内容会超出屏幕,也就是混用字体时,最好用CoreText绘图。