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

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

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

在 Core Text中绘制图片

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

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


当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起点之间的差距

现在开始绘制图片!首先修改 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数组中存放了:

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

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

//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!

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

这段代码解说如下:

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

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

[self attachImagesWithFrame:frame inColumnView: content];

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

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

  1. 在项目导航窗口,删除test.txt
  2. 从这里下载并解压缩文件: 僵尸杂志
  3. 把所有文件拖到你的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增加更多的格式
  • 增加更多的段落格式
  • 为单词、段落和句子增加自适应格式
  • 连体字及字距支持

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

自定义语法解析器,你可以使用 Obj-C ParseKit




1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值