你好,游客 登录 注册 搜索
背景:
阅读新闻

用Core Text创建简单杂志应用(2) - kmyhy的专栏

[日期:2013-04-07] 来源:  作者: [字体: ]

现在,我们已经获得所有的文本块和格式标签(如同前面见到的<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;

代码稍有点多,但不要担心,我会逐句进行讲解。

  1. 在对文本块进行遍历的过程中,我们把文本块根据“<”符号分为前后两段。前半段parts[0]作为文本内容,后半段作为标签内容用于格式化后面的文本。
  2. 接下来,创建一个dictionary用于存放格式化属性——通过这种方式我们可以将格式属性传递给NSAttributedString。注意NSAttributedString中key的名称——苹果将它们定义成望名生意的常量(详见 Core Text String Attributes Reference)。appendAttributedString: 方法调用则将新的文本块和格式属性添加到最终的NSAttributedString。
  3. 最后,检查在文本块后面是否有标签出现,如果有并且是font开头,开始对每种可能的标签属性进行解析。对于face属性,将字体名赋给self.font即可;对于color,我们需要通过正则式colorRegex从<font color="red">中取出单词red,然后创建一个名为redColor的选择器,然后在UIColor上进行调用(这将返回UIColor的一个红色实例)。当然,当这个选择器不存在时,程序将崩溃。但是在本示例中,这段代码能满足我们的需要。strokeColor属性与color属性一样,但当strokeColor值为none时,我们将strokewidth也设置为0,这样将取消删除线样式。

注: 关于代码中正则式的具体含义,可以理解为“查找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); 
}

先来看看代码。

  1. 进行初始设置——定义了x和y偏移,开启分页模式,创建frames数组。
  2. 用视图的bounds创建路径和frame(利用x和y偏移在四边留白)。
  3. 声明textPos变量,用于存放当前文本位置。声明columnIndex,用于记录当前已经创建的栏数。
  4. while 循环,直到整个文本结束。在循环内部,我们创建了栏的矩形边界:colRect是一个CGRect,根据栏号(columnIndex)计算各栏的origin和size。注意,我们创建的栏是从左至右排列的(不是从上到下)。
  5. 此句使用CTFrameGetVisibleStringRange函数计算本栏的文本内容(本例中是一个文本栏)。textPos根据range的长度自动累加,以便下次循环用于下一栏(假设还有更多的字符)。
  6. 这次,我们不使用以前的方法绘制CTFrame。而是把它作为参数传递给新创建的CTColumnView,将它保存到self.frames数组以备后用,然后把CTColumnView添加到scrollView的subviews中。
  7. 最后,totalPages中保存所有生成的页的数目。 更新CTView的contentSize属性,以便内容超过1页时滚动scrollview。

现在,当Coret Text准备妥当后调用buildFrames方法。在CoreTextMagazineViewController.m的viewDidLoad:方法最后加入代码:

[(CTView *)[self view] buildFrames];

在此之前,将CTView.m的drawRect:方法删除。现在,我们通过CTColumnView来渲染文字,因此不需要在CTView的drawRect:方法中做任何额外的工作。

点击Run,iPad屏幕显示如下图所示!左右滑动以进行翻页……简直是酷极了!









收藏 推荐 打印 | 录入:admin | 阅读:
相关新闻