CoreText 的简单使用(四)

回顾

前面几篇文章我们介绍了,富文本的排版,并且封装了一套自己的基于 CoreText 富文本排版库,但是实际开发中,不会只限于此,因为我们开发中页面中都是图文混排的,所以我们在之前的基础上再次修改,达到能够支持图文混排的效果。

支持图文混排的 排版引擎

上一篇中我们进行封装读取的时候,JSON 数据中有一个 type 值,用来确认是否是 txt 格式的,现在我们再新增一个类型 img 类型,但是在这里还需要注意的是,我们开发的时候,图片都是从服务器获取的 url,然后本地下载后使用,但是使用 CoreText 排版,我们进行绘制的时候,需要确切的宽高值,这时候图片是还没有下载下来的,那么我们就获取不到图片的宽高了,并且图片资源不需要 color 以及 size 字段,那么我们就更换下这两个字段,换成 width 和 height 字段,让服务器给我们返回图片资源的宽高,方便我们进行绘制时计算高度。下面看下数据格式:

[{
        "width":"2074",
        "height":"1382",
        "content":"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1597124636903&di=4e86100999f185237b482a27f7659ed4&imgtype=0&src=http%3A%2F%2Fyouimg1.c-ctrip.com%2Ftarget%2Ftg%2F688%2F660%2F259%2F20eedbcd17834ef3ab9730a5dab424d5.jpg",
        "type":"img"
    },
    {
        "color":"red",
        "size":"12",
        "content":"但是在实际开发的时候,不会只是简简单单显示这种,而且前后端数据交互啥的",
        "type":"txt"
    },
    {
        "width":"530",
        "height":"398",
        "content":"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1597124574690&di=3f11d257b7d6b59cc26e84518cf84856&imgtype=0&src=http%3A%2F%2Fp2.so.qhimgs1.com%2Ft01dfcbc38578dac4c2.jpg",
        "type":"img"
    },
    {
        "color":"green",
        "size":"16",
        "content":"我们拿到是这种的数据的话,处理起来很麻烦,所以一般都是和后台进行约定",
        "type":"txt"
    },
    {
        "color":"blue",
        "size":"20",
        "content":"规定一种格式,方便前端进行使用,最常见的就是JSON格式直接返回排版需要的配置",
        "type":"txt"
    }
]

然后在进行后续操作前,先了解下CTFrame内部组成,CTFrame内部是由多个CTLine组成的,每个CTLine又是有单个或多个CTRun组成的,每个CTRun代表一组显示风格一致的文本。我们不需要手动去管理CTLine以及CTRun。虽然我们不用管理CTRun,但是我们可以指定某一个CTRun的CTRunDelegate来指定绘制时的宽度、高度、排列对齐方式等。

对于图片排版CoreText实际上是不支持的,但是我们使用CoreText绘制的时候,是在View的drawReact方法里面,所以我们可以在图片的位置预留出来空白,在drawReact方法里面绘制图片上去。

理解完这些后,就需要开始修改我们KGCTFrameParser类,使其支持img类型的解析,并且对img类型的CTRun设置CTRunDelegate。

改造KGCoreTextData类,增加图片相关信息,并且增加图片绘制区域相关的逻辑。

改造KGDisPlayView类,增加绘制图片相关的逻辑。

+ (KGCoreTextData *)parseJSONContent:(NSString *)jsonContent config:(KGCTFrameParserConfig *)config{
    //创建图片信息保存数据
    NSMutableArray *imageArr = [NSMutableArray array];
    //通过JSON数据,得到富文本
    NSAttributedString *content = [self loadJSONContent:jsonContent config:config imageArray:imageArr];
    //创建CTFramesetterRef实例
    CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
    //设置绘制边界大小
    CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
    //获得实际绘制所需要的大小
    CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0, 0), nil, restrictSize, nil);
    //因为宽度已经国定,所以在这获取实际绘制需要的高度
    CGFloat textHeight = coreTextSize.height;
    //生成CTFrameRef实例
    CTFrameRef frame = [self createFrameWithFrameSetter:frameSetter config:config height:textHeight];
    //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回实例
    KGCoreTextData *data = [[KGCoreTextData alloc] init];
    //设置实际绘制需要的实例对象
    data.ctFrame = frame;
    //设置实际绘制需要的高度
    data.height = textHeight;
    //设置图片绘制信息
    data.imageArray = imageArr;
    //因为底层库不受ARC约束,所以需要手动调用CFRelease来进行释放
    //释放生成的CTFrameRef实例
    CFRelease(frame);
    //释放CTFramesetterRef实例
    CFRelease(frameSetter);
    //返回需要的绘制数据模型
    return data;
}

然后修改loadJSONContent方法,保存当前节点图片信息,以及新建一个空白占位符。代码如下:

#pragma mark --根据JSON数据,创建富文本


+ (CTFrameRef)createFrameWithFrameSetter:(CTFramesetterRef)frameSetter config:(KGCTFrameParserConfig *)config height:(CGFloat)height{
    //创建一个可变路径,图形上下文中要绘制的图形或线条的数学描述
    CGMutablePathRef path = CGPathCreateMutable();
    //追加矩形到可变路径
    CGPathAddRect(path, NULL, CGRectMake(0, 0, config.width, height));
    //对核心文本框架对象的引用
    CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, NULL);
    CFRelease(path);
    return frame;
}

+ (UIColor *)colorFromDictionary:(NSDictionary *)dict{
    if ([dict[@"color"] isEqualToString:@"red"]) {
        return [UIColor redColor];
    } else if ([dict[@"color"] isEqualToString:@"blue"]) {
        return [UIColor blueColor];
    } else if ([dict[@"color"] isEqualToString:@"green"]) {
        return [UIColor greenColor];
    }else{
        return nil;
    }
}

+ (NSAttributedString *)attributedingWithDictionary:(NSDictionary *)dic config:(KGCTFrameParserConfig *)config{
    //创建一个可变字典,保存默认富文本信息配置选项
    NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithDictionary:[self attributesWithConfig:config]];
    //获取数据中颜色值
    UIColor *color = [self colorFromDictionary:dic];
    if (color) {
        //如果数据中给出颜色值,替换默认设置的色值
        attributes[NSForegroundColorAttributeName] = color;
    }
    CGFloat fontSize = [dic[@"size"] floatValue];
    if (fontSize > 0) {
        //获取数据返回的字体大小,如果存在,创建一个CTFontRef实例,替换默认设置项中的字体设置
        CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
        attributes[NSFontAttributeName] = (__bridge id)fontRef;
        //释放CTFontRef实例
        CFRelease(fontRef);
    }
    NSString *content = dic[@"content"];
    //根据富文本培训选项以及给定字符串创建并返回一个富文本
    return [[NSAttributedString alloc] initWithString:content attributes:attributes];
}

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 *)attributedingImageWithDict:(NSDictionary *)dict config:(KGCTFrameParserConfig *)config{
    //创建CTRunDelegate回调
    CTRunDelegateCallbacks callbacks;
    //初始化callbacks
    memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
    //设置回调版本号,初始化版本号为kCTRunDelegateVersion1
    callbacks.version = kCTRunDelegateVersion1;
    //为请求run委托以确定并返回在运行中字形的排版上升而调用的回调
    callbacks.getAscent = ascentCallback;
    //为请求run委托来确定并返回运行中符号的印刷下降而调用的回调
    callbacks.getDescent = descentCallback;
    //请求run委托来确定并返回运行中字形的排版宽度
    callbacks.getWidth = widthCallback;
    //创建CTRunDelegateRef实例
    CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)dict);
    //使用0xFFCC作为空白占位符
    unichar objectReplacementChar = 0xFFCC;
    //转换为字符串
    NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:1];
    //获取富文本配置属性
    NSDictionary *atts = [self attributesWithConfig:config];
    //创建富文本
    NSMutableAttributedString *attributes = [[NSMutableAttributedString alloc] initWithString:content attributes:atts];
    //在指定范围内设置单个属性的值
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)attributes, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
    //释放CTRunDelegateRef实例
    CFRelease(delegate);
    return attributes;
}

+ (NSAttributedString *)loadJSONContent:(NSString *)jsonContent config:(KGCTFrameParserConfig *)config imageArray:(NSMutableArray *)imageArray{
    //JSON字符串转NSData
    NSData *data = [jsonContent dataUsingEncoding:NSUTF8StringEncoding];
    //创建一个可变富文本,保存JSON数据
    NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
    //这里做下容错处理,如果传入的json数据是非空并且有内容,进行数据转换
    if (data) {
        //因为JSON数据本身最外层就是一个数组,所以这块需要用数组去接受JSON解析后得到的值
        NSArray *arr = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
        //这里再做下容错处理,如果是NSArray类,那就说明数据没有问题,可以进行下一步操作
        if ([arr isKindOfClass:[NSArray class]]) {
            //直接使用for in遍历
            for (NSDictionary *dict in arr) {
                NSString *type = dict[@"type"];
                //判断type类型是否是txt,因为现在只是对txt类型进行的处理,所以这块需要进行过滤
                if ([type isEqualToString:@"txt"]) {
                    //获取到单条数据富文本信息
                    NSAttributedString *as = [self attributedingWithDictionary:dict config:config];
                    //将单条数据富文本信息拼接到总数据中
                    [result appendAttributedString:as];
                }else if ([type isEqualToString:@"img"]) {
                    //创建图片信息保存模型
                    KGCoreTextImageData *imageData = [[KGCoreTextImageData alloc] init];
                    //设置图片连接
                    imageData.name = dict[@"content"];
                    //设置图片需要加载位置
                    imageData.position = [result length];
                    //保存到图片信息数组中
                    [imageArray addObject:imageData];
                    //获取到单条数据富文本信息
                    NSAttributedString *as = [self attributedingImageWithDict:dict config:config];
                    //将单条数据富文本信息拼接到总数据中
                    [result appendAttributedString:as];
                }
            }
        }
    }
    return result;
}

+ (KGCoreTextData *)parseJSONContent:(NSString *)jsonContent config:(KGCTFrameParserConfig *)config{
    //创建图片信息保存数据
    NSMutableArray *imageArr = [NSMutableArray array];
    //通过JSON数据,得到富文本
    NSAttributedString *content = [self loadJSONContent:jsonContent config:config imageArray:imageArr];
    //创建CTFramesetterRef实例
    CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
    //设置绘制边界大小
    CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
    //获得实际绘制所需要的大小
    CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0, 0), nil, restrictSize, nil);
    //因为宽度已经国定,所以在这获取实际绘制需要的高度
    CGFloat textHeight = coreTextSize.height;
    //生成CTFrameRef实例
    CTFrameRef frame = [self createFrameWithFrameSetter:frameSetter config:config height:textHeight];
    //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回实例
    KGCoreTextData *data = [[KGCoreTextData alloc] init];
    //设置实际绘制需要的实例对象
    data.ctFrame = frame;
    //设置实际绘制需要的高度
    data.height = textHeight;
    //设置图片绘制信息
    data.imageArray = imageArr;
    //因为底层库不受ARC约束,所以需要手动调用CFRelease来进行释放
    //释放生成的CTFrameRef实例
    CFRelease(frame);
    //释放CTFramesetterRef实例
    CFRelease(frameSetter);
    //返回需要的绘制数据模型
    return data;
}

新建一个类用来保存图片数据,代码如下:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface KGCoreTextImageData : NSObject

/** 图片地址 */
@property (nonatomic, copy) NSString *name;
/** 图片位置 */
@property (nonatomic, assign) NSInteger position;
/** 绘制边界 */
@property (nonatomic, assign) CGRect imagePosition;

@end

NS_ASSUME_NONNULL_END

#import "KGCoreTextImageData.h"

@implementation KGCoreTextImageData

@end

修改KGCoreTextData类,新增保存图片信息数组属性。代码如下:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface KGCoreTextData : NSObject

/** CTFrameRef实例 */
@property (nonatomic, assign) CTFrameRef ctFrame;
/** 实际绘制所需要的高度 */
@property (nonatomic, assign) CGFloat height;
/** 保存图片绘制信息 */
@property (nonatomic, copy) NSArray *imageArray;

@end

NS_ASSUME_NONNULL_END

#import "KGCoreTextData.h"
#import "KGCoreTextImageData.h"

@implementation KGCoreTextData

- (void)setCtFrame:(CTFrameRef)ctFrame{
    if (_ctFrame != ctFrame) {
        if (_ctFrame != nil) {
            CFRelease(_ctFrame);
        }
        CFRetain(ctFrame);
        _ctFrame = ctFrame;
    }
}

- (void)dealloc
{
    if (_ctFrame != nil) {
        CFRelease(_ctFrame);
        _ctFrame = nil;
    }
}

- (void)setImageArray:(NSArray *)imageArray{
    _imageArray = imageArray;
    [self chackImagePosition];
}

- (void)chackImagePosition{
    //容错处理,当没有图片时不再进行后续操作
    if (self.imageArray.count == 0) {
        return;
    }
    
    //获取当前CTFrame实例的行信息
    NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
    //获取行数
    int linesCount = (int)[lines count];
    //CTFrameGetLines返回的行数组中相对于路径边界盒原点的对应行的原点保存数组
    CGPoint lineOrigin[linesCount];
    //复制帧的线源范围
    CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigin);
    //设置图片位置初始值,默认为0
    int imgIndex = 0;
    //读取数组中图片信息
    KGCoreTextImageData *imageData = self.imageArray[0];
    //循环去读图片信息
    for (int i = 0; i < linesCount; i++) {
        //容错处理,如果图片信息不存在,直接返回
        if (imageData == nil) {
            break;
        }
        //读取单行信息
        CTLineRef line = (__bridge CTLineRef)lines[i];
        //获取单个CTLine中CTRun
        NSArray *runObjectArray = (NSArray *)CTLineGetGlyphRuns(line);
        for (id runOjb in runObjectArray) {
            //读取单个CTRun
            CTRunRef run = (__bridge CTRunRef)runOjb;
            //获取单个CTRun的布局属性
            NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
            //获取CTRunDelegate不透明对象的类型
            CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
            //容错处理,如果代理不存在直接跳过本次循环
            if (delegate == nil) {
                continue;
            }
            //创建委托对象
            NSDictionary *metaDic = CTRunDelegateGetRefCon(delegate);
            //容错处理,如果返回值不是字典类型,跳过本次循环
            if (![metaDic isKindOfClass:[NSDictionary class]]) {
                continue;
            }
            
            CGRect runBounds;
            CGFloat ascent;
            CGFloat descent;
            //获取运行的排版边界
            runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
            runBounds.size.height = ascent + descent;
            //确定字符串索引的图形偏移量或偏移量
            CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
            runBounds.origin.x = lineOrigin[i].x + xOffset;
            runBounds.origin.y = lineOrigin[i].y;
            runBounds.origin.y -= descent;
            
            //获取绘制路径
            CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
            //包含图形路径中所有点的边界框
            CGRect colReact = CGPathGetBoundingBox(pathRef);
            //返回一个矩形,其原点与源矩形的原点偏移
            CGRect delegateReact = CGRectOffset(runBounds, colReact.origin.x, colReact.origin.y);
            //设置图片数据区域
            imageData.imagePosition = delegateReact;
            imgIndex++;
            if (imgIndex == self.imageArray.count) {
                imageData = nil;
                break;
            }else{
                imageData = self.imageArray[imgIndex];
            }
        }
    }
}

@end

最后修改KGDisPlayView,代码如下:

#import "KGDisPlayView.h"
#import "KGCoreTextImageData.h"

@implementation KGDisPlayView


- (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);
    }
    if (self.data.imageArray.count > 0) {
        for (KGCoreTextImageData *obj in self.data.imageArray) {
            CGContextDrawImage(context, obj.imagePosition, KGImage(obj.name).CGImage);
        }
    }
}

@end

到此框架改版完成了,然后运行下,最后效果如下:

20200811002.png

到这里基本上一个常见的图文混排框架就写好了,然后或许可能根据业务不同做一些微调。下一篇探索框架中对图片添加点击事件。

系列文章:

<a href="https://juejin.cn/post/6970879379425460255/">《CoreText的简单使用(一)》</a>

<a href="https://juejin.cn/post/6970879593129443336/">《CoreText的简单使用(二)》</a>

<a href="https://juejin.cn/post/6970880935327694879/">《CoreText的简单使用(三)》</a>

<a href="https://juejin.cn/post/6970881236936081439/">《CoreText的简单使用(四)》</a>

<a href="https://juejin.cn/post/6970881873304092686/">《CoreText 的简单使用(五)》</a>

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容