首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 移动开发 > 移动开发 >

用Core Text创办简单杂志应用(3)

2013-04-09 
用Core Text创建简单杂志应用(3)有栏、有可以样式化的文字,但没有图片。用Core Text来绘制图片不太容易——它

用Core Text创建简单杂志应用(3)

有栏、有可以样式化的文字,但没有图片。用Core Text来绘制图片不太容易——它毕竟只是一个文本绘制框架。

幸运的是我们已经有一个简单的标签解析器。我们可以容易地从字符串中取出图片。

在 Core Text中绘制图片

本质上,Core Text是不能绘制图形的。但是,它同时还是一个布局引擎,因此我们可以让它流出一些空白以便我们有绘图的空间。在drawRect:方法中绘图是一件简单的事。

首先看我们如何在文本绘制时保留足够的空白空间。还记得文本块其实都是一些CTRun实例吗?简单地为某个CTRun指定一个委托,然后让委托对象告诉Core Text要该 CTRun 上升/下降多少空间,以及空间的宽度。如下图所示:

用Core Text创办简单杂志应用(3)

当Core Text“到达”某个被设置了委托的CTRun时,它会询问委托“要预留给这个CTRun多少宽度和高度?”。这样,就可以在文本中挖出一个洞——用于绘制图片。

让我们在我们的标签解析器中增加对<img>的支持。打开MarkupParser.m找到"} //end of font parsing"一行,在后面加入一下代码:

if ([tag hasPrefix:@"img"]) {

 

    __block NSNumber* width = [NSNumbernumberWithInt:0];

    __block NSNumber* height = [NSNumbernumberWithInt:0];

    __block NSString* fileName = @"";

 

    //width

    NSRegularExpression* widthRegex =[[[NSRegularExpression alloc]initWithPattern:@"(?<=width=\")[^\"]+" options:0error:NULL] autorelease];

    [widthRegex enumerateMatchesInString:tagoptions:0 range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult*match, NSMatchingFlags flags, BOOL *stop){

        width = [NSNumbernumberWithInt: [[tag substringWithRange: match.range] intValue] ];

    }];

 

    //height

    NSRegularExpression* faceRegex =[[[NSRegularExpression alloc]initWithPattern:@"(?<=height=\")[^\"]+" options:0error:NULL] autorelease];

    [faceRegex enumerateMatchesInString:tagoptions:0 range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult*match, NSMatchingFlags flags, BOOL *stop){

        height = [NSNumbernumberWithInt: [[tag substringWithRange:match.range] intValue]];

    }];

 

    //image

    NSRegularExpression* srcRegex =[[[NSRegularExpression alloc]initWithPattern:@"(?<=src=\")[^\"]+" options:0error:NULL] autorelease];

    [srcRegex enumerateMatchesInString:tag options:0range:NSMakeRange(0, [tag length]) usingBlock:^(NSTextCheckingResult *match,NSMatchingFlags flags, BOOL *stop){

        fileName = [tagsubstringWithRange: match.range];

    }];

 

    //add the image for drawing

    [self.images addObject:

     [NSDictionarydictionaryWithObjectsAndKeys:

      width, @"width",

      height, @"height",

      fileName, @"fileName",

      [NSNumber numberWithInt: [aStringlength]], @"location",

      nil]

     ];

 

    //render empty space for drawing the image inthe text //1

    CTRunDelegateCallbacks callbacks;

    callbacks.version = kCTRunDelegateVersion1;

    callbacks.getAscent = ascentCallback;

    callbacks.getDescent = descentCallback;

    callbacks.getWidth = widthCallback;

    callbacks.dealloc = deallocCallback;

 

    NSDictionary* imgAttr = [[NSDictionarydictionaryWithObjectsAndKeys: //2

                             width, @"width",

                             height, @"height",

                             nil] retain];

 

    CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks,imgAttr); //3

    NSDictionary *attrDictionaryDelegate =[NSDictionary dictionaryWithObjectsAndKeys:

                                           //set the delegate

                                           (id)delegate, (NSString*)kCTRunDelegateAttributeName,

                                           nil];

 

    //add a space to the text so that it can callthe delegate

    [aStringappendAttributedString:[[[NSAttributedString alloc] initWithString:@"" attributes:attrDictionaryDelegate] autorelease]];

}

阅读这段代码——实际上,<img>标签并不像<font>标签那么好解析。需要使用3个正则式,才能读取到<img>的三个属性:width、height和src。然后,用一个NSDictionar保存这些信息(另外再加上图片在文字中出现的位置)并添加到self.images中。

来到第1个代码块——CTRunDelegateCallbacks 是一个结构体,结构体中包含了一些函数指针。该结构体包含了你想告诉给CTRunDelegate的一些信息。你也许猜到了,getWith调用将告诉CTRun的宽度,getAscent调用将告诉CTRun的高度等等。在这段代码中,你为这些handler提供了函数名,随后我们将为这些函数提供实现。

代码块2非常重要——imgAttr字典保存了图片的尺寸;同时这个对象会被传递给处理函数——因此,当getAscent函数被触发时,它将收到一个imgAttr参数,同时读取图片的高度,以便传递给CoreText。

在代码块3,CTRunDelegateCreate函数创建了委托实例并将imgAttr和指定的CTRunDelgateCallBacks进行绑定。

下一步,我们需要创建一个字典(和前面创建字体属性是一样的),但将字体样式属性替代以委托对象。最后,我们加入了一个空格字符,以便触发委托方法和创建文本“空洞”用于绘制图片。

接下来,你可能想到了,我们要让委托对象提供回调函数的实现。

//inside MarkupParser.m, just above@implementation

 

/* Callbacks */

static void deallocCallback( void* ref ){

    [(id)ref release];

}

static CGFloat ascentCallback( void *ref){

    return [(NSString*)[(NSDictionary*)refobjectForKey:@"height"] floatValue];

}

static CGFloat descentCallback( void *ref){

    return [(NSString*)[(NSDictionary*)refobjectForKey:@"descent"] floatValue];

}

static CGFloat widthCallback( void* ref){

    return [(NSString*)[(NSDictionary*)refobjectForKey:@"width"] floatValue];

}

 

ascentCallback, descentCallback 和 widthCallback仅仅读取了字典中的对应内容返回给Core Text。deallocCallback则释放字典——当CTRunDelegate被释放时调用,因此这里是让你进行内存管理的地方。

在为解析器增加对<img>标签的处理之后,我们还需要修改CTView。首先需要定义一个方法,将images数组传递给视图,我们可以将NSAttributedString和imgAttr一起传递给这个方法。

//CTView.h - inside @interfacedeclaration as an ivar

NSArray* images;

 

//CTView.h - declare property for images

@property (retain, nonatomic) NSArray*images;

 

//CTView.h - add a method declaration

-(void)setAttString:(NSAttributedString*)attString withImages:(NSArray*)imgs;

 

//CTView.m - just below @implementation

@synthesize images;

 

//CTView.m - inside the dealloc method

self.images = nil;

 

//CTView.m - anywhere inside theimplementation

-(void)setAttString:(NSAttributedString*)string withImages:(NSArray*)imgs

{

    self.attString = string;

    self.images = imgs;

}

好了,CTView已经能够接收一个图片数组了,让我们从解析器将图片传递给它就可以了。

转到CoreTextMagazineViewController.m,找到这行“[(CTView*)self.view setAttString: attString];”,修改为:

[(CTView *)[self view] setAttString:attString withImages: p.images];

MarkupParser的attrStringFromMarkup:方法将所有的图片标签解析为数据放入了self.images,也就是你现在传给CTView的东西。

渲染图片,首先要算出图片将要显示的准确位置。计算过程如下:

    contentView滚动时的contentOffset相对于CTView的偏移 (frameXOffset,frameYOffset)CTLine 的原点坐标(CTLine和这段文本的起始位置有一个偏移量) CTLine起点和CTRun起点之间的差距用Core Text创办简单杂志应用(3)

    现在开始绘制图片!首先修改 CTColumnView 类:

    /inside CTColumnView.h

    //as an ivar

    NSMutableArray* images;

     

    //as a property

    @property (retain, nonatomic)NSMutableArray* images;

     

    //inside CTColumnView.m

    //after @implementation...

    @synthesize images;

     

    -(id)initWithFrame:(CGRect)frame

    {

        if ([super initWithFrame:frame]!=nil) {

            self.images =[NSMutableArray array];

        }

        return self;

    }

     

    -(void)dealloc

    {

        self.images= nil;

        [super dealloc];

    }

     

    //at the end of drawRect:

    for (NSArray* imageData in self.images) {

        UIImage* img = [imageData objectAtIndex:0];

        CGRectimgBounds = CGRectFromString([imageData objectAtIndex:1]);

        CGContextDrawImage(context, imgBounds,img.CGImage);

    }

     

    我们修改了几个地方:增加了本地变量images和对应的属性,用于持有在每个文本栏中的图片列表。为求简便,我们没有声明新的类,而是直接在images数组中存放了:

      UIImage 对象UIImage 的位置大小 - 例如图片在文本中所处的位置以及图片大小。 

    现在,我们来计算图片的位置并将它们添加到对应的文本栏中:

    //inside CTView.h

    -(void)attachImagesWithFrame:(CTFrameRef)finColumnView:(CTColumnView*)col;

     

    //inside CTView.m

    -(void)attachImagesWithFrame:(CTFrameRef)finColumnView:(CTColumnView*)col

    {

        //drawing images

        NSArray *lines = (NSArray *)CTFrameGetLines(f);//1

     

        CGPoint origins[[lines count]];

        CTFrameGetLineOrigins(f, CFRangeMake(0, 0),origins); //2

     

        int imgIndex = 0; //3

        NSDictionary* nextImage = [self.imagesobjectAtIndex:imgIndex];

        int imgLocation = [[nextImageobjectForKey:@"location"] intValue];

     

        //find images for the current column

        CFRange frameRange =CTFrameGetVisibleStringRange(f); //4

        while ( imgLocation < frameRange.location ) {

            imgIndex++;

            if(imgIndex>=[self.images count]) return; //quit if no images for this column

            nextImage = [self.imagesobjectAtIndex:imgIndex];

            imgLocation =[[nextImage objectForKey:@"location"] intValue];

        }

     

        NSUInteger lineIndex = 0;

       for (id lineObj inlines) { //5

            CTLineRef line =(CTLineRef)lineObj;

     

            for (id runObj in(NSArray *)CTLineGetGlyphRuns(line)) { //6

               CTRunRef run = (CTRunRef)runObj;

               CFRange runRange = CTRunGetStringRange(run);

     

               if (runRange.location <= imgLocation &&runRange.location+runRange.length > imgLocation ) { //7

               CGRect runBounds;

               CGFloat ascent;//height above the baseline

               CGFloat descent;//height below the baseline

                runBounds.size.width= CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent,NULL); //8

               runBounds.size.height = ascent + descent;

     

               CGFloat xOffset = CTLineGetOffsetForStringIndex(line,CTRunGetStringRange(run).location, NULL); //9

               runBounds.origin.x = origins[lineIndex].x + self.frame.origin.x +xOffset + frameXOffset;

               runBounds.origin.y = origins[lineIndex].y + self.frame.origin.y +frameYOffset;

               runBounds.origin.y -= descent;

     

                   UIImage *img = [UIImage imageNamed: [nextImageobjectForKey:@"fileName"] ];

                   CGPathRef pathRef = CTFrameGetPath(f); //10

                   CGRect colRect = CGPathGetBoundingBox(pathRef);

     

                   CGRect imgBounds = CGRectOffset(runBounds, colRect.origin.x -frameXOffset - self.contentOffset.x, colRect.origin.y - frameYOffset -self.frame.origin.y);

                   [col.images addObject: //11

                       [NSArray arrayWithObjects:img, NSStringFromCGRect(imgBounds) , nil]

                    ];

                   //load the next image //12

                   imgIndex++;

                   if (imgIndex < [self.images count]) {

                       nextImage = [self.images objectAtIndex: imgIndex];

                       imgLocation = [[nextImage objectForKey: @"location"]intValue];

                   }

     

               }

            }

            lineIndex++;

        }

    }

    注:这段代码基于DavidBeck的提供的代码而来,非常感谢David Beck!

    这段代码不是很好阅读,但你千万再忍耐一下——本文即将结束,我们都即将解脱!

    这段代码解说如下:

      CTFrameGetLines 返回一个 CTLine 对象数组。获取当前帧所有行的起点坐标:每行文本的左上角。通过nextImage字典加载图片数据,然后从nextImage字典中读取图片在文本中的位置放入imgLocation变量。CTFrameGetVisibleStringRange 从返回本帧文本的可视范围——当前你正在渲染的是哪部分文字,然后对图片数组进行迭代,知道查找到位于本帧的第1张图片。即快速找到本帧要渲染的图片的位置。对行进行迭代,加载每一行到line变量。对line中的CTRun进行迭代(通过 CTLineGetGlyphRuns获得行中的CTRun)。判断nextImage是否在CTRun的范围内——如果是,则需要在这里渲染图片。用 CTRunGetTypographicBounds 获得CTRun宽高。用CTLineGetOffsetForStringIndex计算CTRun与CTLine原点的距离。加载图片,获得当前帧矩形并计算出图片的矩形。将UIImage和它的frame放入一个NSArray,再将NSArray保存到CTColumnView的images数组。设置nextImage为下一张图片(如果存在),继续循环,直至本行结束。

    好了!还有一个小地方:在 CTView.m 中找到行 “[content setCTFrame:(id)frame];”,在前面插入:

    [self attachImagesWithFrame:frame inColumnView: content];

    最后一件事,你需要提供一些大文本以供测试。

    不用担心,我已经为你准备好了下一期的“僵尸月刊”——一个关于僵尸的大众月刊,你只需要:

      在项目导航窗口,删除test.txt从这里下载并解压缩文件: 僵尸杂志。把所有文件拖到你的Xcode项目中,注意选中“Copy items……”,然后点击Finish。

    打开CoreTextMagazineViewController.m ,找到获取test.txt文件路径的一行,替换为:

    NSString *path = [[NSBundle mainBundle] pathForResource:@"zombies" ofType:@"txt"];

    编译运行,开始享受最新一期的僵尸月刊吧!

    还剩最后一环节。就是使文本自动适应栏宽。加入代码:

    //在CTView.m文件

    // setAttString:withImages: 方法最后

     

    CTTextAlignment alignment =kCTJustifiedTextAlignment;

     

    CTParagraphStyleSetting settings[] = {

        {kCTParagraphStyleSpecifierAlignment,sizeof(alignment), &alignment},

    };

    CTParagraphStyleRef paragraphStyle =CTParagraphStyleCreate(settings, sizeof(settings) / sizeof(settings[0]));

    NSDictionary *attrDictionary =[NSDictionary dictionaryWithObjectsAndKeys:

                                   (id)paragraphStyle, (NSString*)kCTParagraphStyleAttributeName,

                                   nil];

    NSMutableAttributedString* stringCopy =[[[NSMutableAttributedString alloc] initWithAttributedString:self.attString]autorelease];

    [stringCopy addAttributes:attrDictionaryrange:NSMakeRange(0, [attString length])];

    self.attString =(NSAttributedString*)stringCopy;

    这会让你开始使用段落格式, 在苹果Core Text文档中查看 kCTParagraphStyleSpecifierAlignment ,可以找到你能使用的所有段落样式。

    什么时候使用 Core Text ?为什么要用 Core Text?

    现在你的Core Text杂志应用程序已经完成了,你可能会问自己:为什么要用CoreText而不是UIWebView呢?

    CT和UIWebView有各自的应用场景。

    UIWebView是一个成熟的web浏览器,仅仅为了显示一行由多个颜色组成文字未免有点杀鸡用牛刀了。

    如果你的UI上有10个多色标签,那你岂不是要用10个Safari?那会占用多少内存?

    请记住:UIWebView是一个好的浏览器,而CoreText是一个高效的文本渲染引擎。

    Core Text还能做些什么?

    这个教程中使用的完整示例在这里下载: CoreText example project 。

    如果你想在这个项目上进行扩展,并了解更多Core Text的内容,请阅读苹果的 CoreText Reference Collection 。然后考虑在这个程序中加入以下特性:

      为解析器添加更多标签的支持为CTRun增加更多的格式增加更多的段落格式为单词、段落和句子增加自适应格式连体字及字距支持

      我想,你可能已经想到如何去扩展解析器了,我有两个建议:

        解析HTML可以使用并扩展 Ben Reeves的Obj-C HTML Parser;自定义语法解析器,你可以使用 Obj-C ParseKit




热点排行