上一节中,我详细的讲解了用面向对象的思想将Core Text的纯C语言的代码进行了封装。这一节,我将对“图文混排”的效果也进行封装工作。不过,这一节的代码是基于上一节的,所以,如果你没有浏览过上一节的内容,请点击这里。先看看最终的效果图:
现在,我们就来对上一节的代码,继续扩充。
1. 添加了图片信息,所以我们需要修改数据源(plist)的结构
1)为每一项添加了type信息,“txt”表示纯文本;“img”表示图片;图片信息包括name,width,height。 name就是图片的地址,我这里是存储在沙盒中,实际开发的时候,可以加载远程图片。
2)一定要提供图片的width和height信息,因为Core Text排版是要计算每一个元素的占位大小的。如果不提供图片的width和height信息,客户端在加载远程图片后,还要计算出width和height,效率低下,如果在网络比较差的情况下,图片一直加载不到,那么Core Text排版就明显混乱了;如果服务端数据提供了width和height信息,就算图片没有加载过来,也可以有同等大小的空白区域被占位着,不影响整体的布局。
2. 定义CoreTextImageData模型,用于存储图片的名称及位置信息
@interface CoreTextImageData : NSObject @property (nonatomic,copy) NSString *name; // 此坐标是 CoreText 的坐标系,而不是UIKit的坐标系 @property (nonatomic,assign) CGRect imagePosition; @end
3. CoreTextData类中应该包含CoreTextImageData模型信息,这里用的是数组imageArray,因为有可能包含多张图片。所以改造一下CoreTextData类,CoreTextData.h代码如下:
@interface CoreTextData : NSObject @property (nonatomic,assign) CTFrameRef ctFrame; @property (nonatomic,assign) CGFloat height; @property (nonatomic,strong) NSArray *imageArray; @end
4. 改造CTFrameParser类中的parseTemplateFile方法,使其包含CoreTextImageData信息
+ (CoreTextData *)parseTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config { NSMutableArray *imageArray = [NSMutableArray array]; NSAttributedString *content = [self loadTemplateFile:path config:config imageArray:imageArray]; CoreTextData *data = [self parseAttributedContent:content config:config]; data.imageArray = imageArray; return data; }
5. 在loadTemplateFile方法添加支持image的代码, 这样,就将plist中img的相关信息保存到CoreTextImageData模型中了。
但是问题来了,Core Text本身并不支持对图片的展示功能!但是,我们可以在要显示文本的地方,用一个特殊的空白字符代替,同时设置该字体的CTRunDelegate信息为要显示的图片的宽度和高度,这样最后生成的CTFrame实例,就会在绘制时将图片的位置预留下来。因为CTDisplayView的绘制代码是在drawRect里面的,所以我们可以方便的把需要绘制的图片,用Quartz 2D的CGContextDrawImage方法直接绘制出来就行了。我这里所描述的流程,就是在调用的parseImageDataFromNSDictionary中实现的。
+ (NSAttributedString *)loadTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config imageArray:(NSMutableArray *)imageArray{ NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init]; // JSON方式获取数据 // NSArray *array = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil]; NSArray *array = [NSArray arrayWithContentsOfFile:path]; if (array) { if ([array isKindOfClass:[NSArray class]]) { for (NSDictionary *dict in array) { NSString *type = dict[@"type"]; if ([type isEqualToString:@"txt"]) { NSAttributedString *as = [self parseAttributedContentFromNSDictionary:dict config:config]; [result appendAttributedString:as]; } else if ([type isEqualToString:@"img"]) { CoreTextImageData *imageData = [[CoreTextImageData alloc] init]; imageData.name = dict[@"name"]; [imageArray addObject:imageData]; NSAttributedString *as = [self parseImageDataFromNSDictionary:dict config:config]; [result appendAttributedString:as]; } } } } return result; }
6. 占位字符及设置占位字符的CTRunDelegate,代码中是用‘0xFFFC‘这个字符进行占位的。
static CGFloat ascentCallback(void *ref) { return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"height"] floatValue]; } static CGFloat descentCallback(void *ref) { return 0; } static CGFloat widthCallback(void *ref) { return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"width"] floatValue]; } + (NSAttributedString *)parseImageDataFromNSDictionary:(NSDictionary *)dict config:(CTFrameParserConfig *)config { CTRunDelegateCallbacks callbacks; // memset将已开辟内存空间 callbacks 的首 n 个字节的值设为值 0, 相当于对CTRunDelegateCallbacks内存空间初始化 memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks)); callbacks.version = kCTRunDelegateVersion1; callbacks.getAscent = ascentCallback; callbacks.getDescent = descentCallback; callbacks.getWidth = widthCallback; CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)(dict)); // 使用0xFFFC 作为空白的占位符 unichar objectReplacementChar = 0xFFFC; NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:1]; NSDictionary *attributes = [self attributesWithConfig:config]; NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:content attributes:attributes]; CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate); CFRelease(delegate); return space; }
7. 在5,6 两点的代码执行完毕后,代码会返回到第4点,执行下面这句代码:
data.imageArray = imageArray;
它实际上就是重写了CoreTextData中的imageArray属性方法,下面代码的目的就是计算空白字符的实际占位大小。对下面的代码,我进行大致的说明:
1) 通过调用CTFrameGetLines方法获得所有的CTLine。
2)通过调用CTFrameGetLineOrigins方法获取每一行的起始坐标。
3)通过调用CTLineGetGlyphRuns方法,获取每一行所有的CTRun。
4)通过CTRun的attributes信息找到key为CTRunDelegateAttributeName的信息,如果存在,表明他就是占位字符,否则的话直接过滤掉。
5)最终计算获得每一个占位字符的实际尺寸大小。
- (void)setImageArray:(NSArray *)imageArray { _imageArray = imageArray; [self fillImagePosition]; } - (void)fillImagePosition { if (self.imageArray.count == 0) return; NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame); int lineCount = lines.count; // 每行的起始坐标 CGPoint lineOrigins[lineCount]; CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins); int imageIndex = 0; CoreTextImageData *imageData = self.imageArray[0]; for (int i = 0; i < lineCount; i++) { if (!imageData) break; CTLineRef line = (__bridge CTLineRef)(lines[i]); NSArray *runObjectArray = (NSArray *)CTLineGetGlyphRuns(line); for (id runObject in runObjectArray) { CTRunRef run = (__bridge CTRunRef)(runObject); NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run); CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)([runAttributes valueForKey:(id)kCTRunDelegateAttributeName]); // 如果delegate是空,表明不是图片 if (!delegate) continue; NSDictionary *metaDict = CTRunDelegateGetRefCon(delegate); if (![metaDict isKindOfClass:[NSDictionary class]]) continue; /* 确定图片run的frame */ CGRect runBounds; CGFloat ascent,descent; runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL); runBounds.size.height = ascent + descent; // 计算出图片相对于每行起始位置x方向上面的偏移量 CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL); runBounds.origin.x = lineOrigins[i].x + xOffset; runBounds.origin.y = lineOrigins[i].y; runBounds.origin.y -= descent; imageData.imagePosition = runBounds; imageIndex++; if (imageIndex == self.imageArray.count) { imageData = nil; break; } else { imageData = self.imageArray[imageIndex]; } } } }
8. 改造CTDisplayView中的代码,完成绘制工作。
1)先调用CTFrameDraw方法完成整体的绘制,此时图片区域就是图片实际大小的一片空白显示。
2)遍历CoreTextData中的imageArray数组,使用CGContextDrawImage方法在对应的空白区域绘制图片。
- (void)drawRect:(CGRect)rect { [super drawRect:rect]; CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM(context, 1.0, -1.0); // 先整体绘制 if (self.data) { CTFrameDraw(self.data.ctFrame, context); } // 绘制出图片 for (CoreTextImageData *imageData in self.data.imageArray) { UIImage *image = [UIImage imageNamed:imageData.name]; if (image) { CGContextDrawImage(context, imageData.imagePosition, image.CGImage); } } }