本人录制技术视频地址:https://edu.csdn.net/lecturer/1899 欢迎观看。
上一节中,我详细的讲解了用面向对象的思想将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);
}
}
}