IOS CoreText --- 图文混排之代码封装

上一节中,我详细的讲解了用面向对象的思想将Core Text的纯C语言的代码进行了封装。这一节,我将对“图文混排”的效果也进行封装工作。不过,这一节的代码是基于上一节的,所以,如果你没有浏览过上一节的内容,请点击这里。先看看最终的效果图:

IOS CoreText --- 图文混排之代码封装_第1张图片


现在,我们就来对上一节的代码,继续扩充。

1. 添加了图片信息,所以我们需要修改数据源(plist)的结构

1)为每一项添加了type信息,“txt”表示纯文本;“img”表示图片;图片信息包括name,width,height。 name就是图片的地址,我这里是存储在沙盒中,实际开发的时候,可以加载远程图片。

2)一定要提供图片的width和height信息,因为Core Text排版是要计算每一个元素的占位大小的。如果不提供图片的width和height信息,客户端在加载远程图片后,还要计算出width和height,效率低下,如果在网络比较差的情况下,图片一直加载不到,那么Core Text排版就明显混乱了;如果服务端数据提供了width和height信息,就算图片没有加载过来,也可以有同等大小的空白区域被占位着,不影响整体的布局。

IOS CoreText --- 图文混排之代码封装_第2张图片


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);
        }
    }
}

你可能感兴趣的:(quartz,text,graphics,core,core,图文混排)