现在,我们已经获得所有的文本块和格式标签(如同前面见到的<font>标签)。需要做的仅仅是遍历文本块数组然后构建NSAttributedString .
在方法体中加入以下代码:
for (NSTextCheckingResult* b in chunks) { NSArray* parts = [[markup substringWithRange:b.range] componentsSeparatedByString:@"<"]; //1 CTFontRef fontRef = CTFontCreateWithName( (CFStringRef)self.font, 24.0f, NULL); //apply the current text style //2 NSDictionary* attrs=[NSDictionary dictionaryWithObjectsAndKeys: (id)self.color.CGColor, kCTForegroundColorAttributeName, (id)fontRef, kCTFontAttributeName, (id)self.strokeColor.CGColor,kCTStrokeColorAttributeName, (id)[NSNumber numberWithFloat: self.strokeWidth], (NSString *)kCTStrokeWidthAttributeName, nil]; [aString appendAttributedString:[[[NSAttributedString alloc] initWithString:[parts objectAtIndex:0] attributes:attrs] autorelease]]; CFRelease(fontRef); //handle new formatting tag //3 if ([parts count]>1) { NSString* tag = (NSString*)[parts objectAtIndex:1]; if ([tag hasPrefix:@"font"]) { //stroke color NSRegularExpression* scolorRegex = [[[NSRegularExpression alloc] initWithPattern:@"(?<=strokeColor=\")\\w+" options:0 error:NULL] autorelease]; [scolorRegex enumerateMatchesInString:tag options:0 range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){ if ([[tag substringWithRange:match.range] isEqualToString:@"none"]) { self.strokeWidth = 0.0; } else { self.strokeWidth = -3.0; SEL colorSel = NSSelectorFromString([NSString stringWithFormat: @"%@Color", [tag substringWithRange:match.range]]); self.strokeColor = [UIColor performSelector:colorSel]; } }]; //color NSRegularExpression* colorRegex = [[[NSRegularExpression alloc] initWithPattern:@"(?<=color=\")\\w+" options:0 error:NULL] autorelease]; [colorRegex enumerateMatchesInString:tag options:0 range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){ SEL colorSel = NSSelectorFromString([NSString stringWithFormat: @"%@Color", [tag substringWithRange:match.range]]); self.color = [UIColor performSelector:colorSel]; }]; //face NSRegularExpression* faceRegex = [[[NSRegularExpression alloc] initWithPattern:@"(?<=face=\")[^\"]+" options:0 error:NULL] autorelease]; [faceRegex enumerateMatchesInString:tag options:0 range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){ self.font = [tag substringWithRange:match.range]; }]; } //end of font parsing } } return (NSAttributedString*)aString; |
代码稍有点多,但不要担心,我会逐句进行讲解。
注: 关于代码中正则式的具体含义,可以理解为“查找color="之后的字符串(由普通字符数字、字母和下划线组成,不包括标点符号),一直到右尖括号”。更多请参考苹果的“NSRegularExpression类参考”。
译者注:正则式“(?<=color=\")\\w+”可以分为两部分,第一部分“(?<=color=\")”使用了零宽度正向回查(非捕获组)模板“(?<=pattern)”,这部分匹配结果不捕获,第二部分“\\w+”匹配1或多个普通字符(数字、字母和下划线),这部分匹配结果会被捕获。
嗨,我们已经完成了一半的工作了!现在attrStringFromMarkup:方法会对markup进行分割,形成NSAttributedString,并准备传递给CoreText。
打开CTView.m,在 @implementation前面加入:
#import "MarkupParser.h" |
将定义attString对象的语句替换为:
the line whereattString is defined - replace it with the following code:
MarkupParser* p = [[[MarkupParser alloc] init] autorelease]; NSAttributedString* attString = [p attrStringFromMarkup: @"Hello <font color=\"red\">core text <font color=\"blue\">world!"]; |
这里,我们创建了一个新的解析器对象,并传入一个使用了标记语法的字符串给attrStringFromMarkup:方法,已得到一个NSAttributeString字符串。
运行程序。很酷吧?仅仅50行的代码,没有计算文本位置、没有硬编码文本样式,仅仅用一个简单文本文件就可以编辑出杂志的内容!
只要你愿意,这个简单解析器可以无限制地扩展下去。
我们可以显示文本,这是一个好的开端。但作为杂志而言,我们希望能够分栏显示——使用Core Text很容易做到这点。
在这样做之前,首先让我们将长文本加载进应用程序,以便我们有足够的文本显示成多行。
点击File\New\New File,选择iOS\Other\Empty,点击 Next。将文件命名为 test.txt, 然后点击Save。
然后编辑文件内容,如 这个文件所示。
打开CTView.m ,找到创建 MarkupParser 和 NSAttributedString 的两句代码,删除它们。我们将在drawRect:方法之外加载text文件,这个功能不应该由drawRect:方法来实现。将attString修改为实例变量和类的属性。
打开 CoreTextMagazineViewController.m,删除所有内容,加入以下内容:
#import "CoreTextMagazineViewController.h" #import "CTView.h" #import "MarkupParser.h" @implementation CoreTextMagazineViewController - (void)viewDidLoad { [super viewDidLoad]; NSString *path = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"txt"]; NSString* text = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL]; MarkupParser* p = [[[MarkupParser alloc] init] autorelease]; NSAttributedString* attString = [p attrStringFromMarkup: text]; [(CTView*)self.view setAttString: attString]; } @end |
当应用程序的视图一被加载,会读取test.txt文件的内容,将其转换为NSAttributedString,然后设置到CTView的attString属性。当然,我们需要为CTView增加相应的属性。
在CTView.h中定义 3 个实例变量:
float frameXOffset; float frameYOffset; NSAttributedString* attString; |
在CTView.h 和 CTView.m 中定义 attString属性:
//CTView.h @property (retain, nonatomic) NSAttributedString* attString; //CTView.m //just below @implementation ... @synthesize attString; //at the bottom of the file -(void)dealloc { self.attString = nil; [super dealloc]; } |
运行程序,文本文件的内容被显示到了屏幕上。
如何将这些文本分栏?CoreText提供了一个便利函数 - CTFrameGetVisibleStringRange。该函数能够告诉你在一个固定的框内能够放下多少文字。基本思路是——创建栏,判断它适合放入多少文字,如果放不下——创建新的栏,以此类推(这里的“栏”就是一个CTFrame实例,栏其实就是一个矩形框)。
首先我们应当有栏,然后是页,然后是整本杂志,因此我们让CTView的继承了UIScrollView,以便获得分页和滚动的能力。
打开CTView.h ,将 @interface 一行改为:
@interface CTView : UIScrollView<UIScrollViewDelegate> { |
现在,CTView已经继承了UIScrollView。我们要让它能够分页。
我们已经在drawRect:方法中创建了framesetter和frame。当存在分栏且样式各不同的情况下,更好的方法是让这个动作只需进行一次。因此我们将创建一个新的类CTColumnView,它仅仅负责渲染指定的CoreText文本,在我们的CTView类中,我们只需创建一次CTColumnView并将它们加到subViews中。
简单而言:CTView负责滚动、分页和创建栏;而CTColumnView将实际上负责将文本内容渲染在屏幕上。
点击File\New\New File, 选择 iOS\Cocoa Touch\Objective-C class, 然后点击Next。选择 UIView 作为父类,点击 Next, 将新类命名为 CTColumnView.m, 然后点击Save. 这是CTColumnView类代码:
//inside CTColumnView.h #import <UIKit/UIKit.h> #import <CoreText/CoreText.h> @interface CTColumnView : UIView { id ctFrame; } -(void)setCTFrame:(id)f; @end //inside CTColumnView.m #import "CTColumnView.h" @implementation CTColumnView -(void)setCTFrame: (id) f { ctFrame = f; } -(void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); // Flip the coordinate system CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM(context, 1.0, -1.0); CTFrameDraw((CTFrameRef)ctFrame, context); } @end |
这个类仅仅负责渲染一个CTFrame。在这本杂志中,我们应当为每个栏单独创建一个实例。
首先,要在CTView中加入一个属性,用于保存所有CTFrame,同时声明一个buildFrames方法,用于创建所有栏:
//CTView.h - at the top #import "CTColumnView.h" //CTView.h - as an ivar NSMutableArray* frames; //CTView.h - declare property @property (retain, nonatomic) NSMutableArray* frames; //CTView.h - in method declarations - (void)buildFrames; //CTView.m - just below @implementation @synthesize frames; //CTView.m - inside dealloc self.frames = nil; |
在buildFrames 方法中创建CTFrame并将它们添加到frames数组。
- (void)buildFrames { frameXOffset = 20; //1 frameYOffset = 20; self.pagingEnabled = YES; self.delegate = self; self.frames = [NSMutableArray array]; CGMutablePathRef path = CGPathCreateMutable(); //2 CGRect textFrame = CGRectInset(self.bounds, frameXOffset, frameYOffset); CGPathAddRect(path, NULL, textFrame ); CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString); int textPos = 0; //3 int columnIndex = 0; while (textPos < [attString length]) { //4 CGPoint colOffset = CGPointMake( (columnIndex+1)*frameXOffset + columnIndex*(textFrame.size.width/2), 20 ); CGRect colRect = CGRectMake(0, 0 , textFrame.size.width/2-10, textFrame.size.height-40); CGMutablePathRef path = CGPathCreateMutable(); CGPathAddRect(path, NULL, colRect); //use the column path CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(textPos, 0), path, NULL); CFRange frameRange=CTFrameGetVisibleStringRange(frame); //5 //create an empty column view CTColumnView* content = [[[CTColumnView alloc] initWithFrame: CGRectMake(0, 0, self.contentSize.width, self.contentSize.height)] autorelease]; content.backgroundColor = [UIColor clearColor]; content.frame = CGRectMake(colOffset.x, colOffset.y, colRect.size.width, colRect.size.height) ; //set the column view contents and add it as subview [content setCTFrame:(id)frame]; //6 [self.frames addObject: (id)frame]; [self addSubview: content]; //prepare for next frame textPos += frameRange.length; //CFRelease(frame); CFRelease(path); columnIndex++; } //set the total width of the scroll view int totalPages = (columnIndex+1) / 2; //7 self.contentSize = CGSizeMake(totalPages*self.bounds.size.width, textFrame.size.height); } |
先来看看代码。
现在,当Coret Text准备妥当后调用buildFrames方法。在CoreTextMagazineViewController.m的viewDidLoad:方法最后加入代码:
[(CTView *)[self view] buildFrames]; |
在此之前,将CTView.m的drawRect:方法删除。现在,我们通过CTColumnView来渲染文字,因此不需要在CTView的drawRect:方法中做任何额外的工作。
点击Run,iPad屏幕显示如下图所示!左右滑动以进行翻页……简直是酷极了!