回顾
前面几篇文章我们介绍了,富文本的排版,并且封装了一套自己的基于 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
到此框架改版完成了,然后运行下,最后效果如下:
到这里基本上一个常见的图文混排框架就写好了,然后或许可能根据业务不同做一些微调。下一篇探索框架中对图片添加点击事件。
系列文章:
<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>