CoreText简介
CoreText
是用于处理文字和字体的底层技术,它直接和Core Graphic
(又称为Quartz
)打交道。Quartz
是一个2d图形渲染引擎,能够处理OS X和iOS的图形显示问题。
Quartz
能够直接处理字体和字形,将文字渲染到界面上,也是基础库中为一个可以处理字形的模块。因此,CoreText
为了排版,会将需要显示的文本内容,位置和字体字形直接传给Quartz
。与其他UI组件相比,它直接与Quartz
交互,所以它具有高效的排版功能。
- 底层架构图
UILabel | UITextField | UITextView | UIWebView |
---|---|---|---|
TextKit | WebKit | ↑ | |
CoreText | ↑ | ||
Core Graphic | ↑ |
UIWebView
本身可以作为复杂排版的备选方案
基于CoreText
和基于UIWebView
相比,具有以下不同之处:
CoreText | UIWebView |
---|---|
占用内存更少,渲染更快 | 占用内存多,渲染速度慢 |
渲染之前就可以精确获得显示内容的高度(只要有CFrame) | 只有渲染出内容之后才能获得内容的高度(而且需要配合JS) |
可以后台线程渲染 | 只能在主线程渲染 |
可以做出更好的原生交互效果,交互更细腻 | 交互需要依靠JS来实现,交互有卡顿 |
渲染内容不能方便的支持复制 | -- |
需要自己出伏复杂的显示逻辑和响应事件 | -- |
开始代码之前需要清楚几个点
渲染层级如下:
截屏2021-12-28 上午11.09.15.png
CTFrame |
---|
CTLine |
CTRun - CTRun - CTRun |
我们无法直接控制CTRun
的创建过程,但是可以设置控制文本的高度、宽度、对齐方式
对于图片排版,本身是不支持的,但我们可以用特殊空白字符代替
同时设置该字体的为要显示的图片的宽度和高度,这样最后生成的
CTFrame
实例就会在绘制时,将图片位置预留出来
示例代码
CoreText
整个示例可以分为4个类:
-
用于绘制和显示
-
用于保存会只需要的数据
-
用于解析本地json数据,或者网络下载的json数据
-
用于配置绘制的参数,例如文字大小,颜色,行间距
一、CADisplayView
- 获取画布、设置矩阵、坐标系翻转、镜像翻转是标配
- 绘制之前,
CoreTextData
要赋值 - 图片需要单独计算坐标并绘制
#import <UIKit/UIKit.h>
#import "CoreTextData.h"
@interface CADisplayView : UIView
@property (nonatomic, strong) CoreTextData *data;
@end
@implementation CADisplayView
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
// 获取画布上下文
CGContextRef ref = UIGraphicsGetCurrentContext();
// 坐标系上下翻转
//对于底层的绘制引擎来说,左下角是(0,0)点,对于UIKit左上角是(0,0)点
//为了原点一致需要翻转坐标系
CGContextSetTextMatrix(ref, CGAffineTransformIdentity);
//翻转后需要将坐标系平移
CGContextTranslateCTM(ref, 0, self.bounds.size.height);
// 镜像翻转
CGContextScaleCTM(ref, 1, -1);
// 文本帧绘制
if (self.data)
{
CTFrameDraw(self.data.ctframe, ref);
}
// 富文本中图片绘制
for (ImageElementModel *model in self.data.imageArr)
{
UIImage *image= [UIImage imageNamed:model.name];
CGContextDrawImage(ref, model.frame, image.CGImage);
}
}
二、CoreTextData
- 很多
cell
在渲染之前需要计算出高度,所以这里提供了内容高度计算 - 图片需要单独绘制,图片数组保存在
data
中
#import <Foundation/Foundation.h>
#import <CoreText/CoreText.h>
#import "ImageElementModel.h"
NS_ASSUME_NONNULL_BEGIN
@interface CoreTextData : NSObject
@property (nonatomic, assign) CTFrameRef ctframe; // 文本帧
@property (nonatomic, assign) NSInteger height; // 内容高度
@property (nonatomic, strong) NSArray *imageArr; //图片数组
@end
#import "CoreTextData.h"
@implementation CoreTextData
- (void)setImageArr:(NSArray *)imageArr
{
_imageArr = imageArr;
[self calculateImageRectWithFrame];
}
// 在保存图片时计算图片具体的位置
-(void)calculateImageRectWithFrame
{
// 从CTFrame中获取所有的Line
NSArray * arrLines = (NSArray *)CTFrameGetLines(self.ctframe);
NSInteger count = [arrLines count];
CGPoint points[count];
获取每行的原点
CTFrameGetLineOrigins(self.ctframe, CFRangeMake(0, 0), points);
for (int i = 0; i < count; i ++)
{
CTLineRef line = (__bridge CTLineRef)arrLines[I];
//获取所有的CTRun
NSArray * arrGlyphRun = (NSArray *)CTLineGetGlyphRuns(line);
for (int j = 0; j < arrGlyphRun.count; j ++)
{
CTRunRef run = (__bridge CTRunRef)arrGlyphRun[j];
NSDictionary * attributes = (NSDictionary *)CTRunGetAttributes(run);
// 获取delegate
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];
if (delegate == nil)
{
continue;
}
// 获取之前创建delegate传入的dict
NSDictionary * dic = CTRunDelegateGetRefCon(delegate);
if (![dic isKindOfClass:[NSDictionary class]])
{
continue;
}
CGPoint point = points[I];
CGFloat ascent;
CGFloat descent;
//相对位置
CGRect boundsRun;
// 一个图片被特殊字符代替后只是一个CTRun,下面也只会调用一次
boundsRun.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
boundsRun.size.height = ascent + descent;
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
boundsRun.origin.x = point.x + xOffset;
boundsRun.origin.y = point.y - descent;
CGPathRef path = CTFrameGetPath(self.ctframe);
//获得View的位置
CGRect colRect = CGPathGetBoundingBox(path);
//相对偏移获取image的实际位置
CGRect imageBounds = CGRectOffset(boundsRun, colRect.origin.x, colRect.origin.y);
NSNumber *index = [dic objectForKey:@"index"];
ImageElementModel *model = [self.imageArr objectAtIndex:[index intValue]];
model.frame = imageBounds;
}
}
}
- (void)dealloc
{
CFRelease(self.ctframe);
}
@end
三、CTFrameParse
- 所有解析结构都是根据业务来的,这里只是参考
-
Frame
作为返回值,不能立即释放,绘制完成会才可以释放
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "CoreTextData.h"
#import "CTFrameParseConfig.h"
NS_ASSUME_NONNULL_BEGIN
@interface CTFrameParse : NSObject
+ (CoreTextData *)parseJsonData:(NSString *)path config:(CTFrameParseConfig *)config;
@end
#import "CTFrameParse.h"
#import "TextElementModel.h"
#import <CoreText/CoreText.h>
#import "UIColor+Hex.h"
@implementation CTFrameParse
+ (CoreTextData *)parseJsonData:(NSString *)path config:(nonnull CTFrameParseConfig *)config
{
CoreTextData *data = [[CoreTextData alloc] init];
NSData *jsonData = [NSData dataWithContentsOfFile:path];
NSArray *arr = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:nil];
NSMutableAttributedString *resultString = [[NSMutableAttributedString alloc] init];
NSMutableArray *imageArr = [NSMutableArray array];
for (NSDictionary *dict in arr)
{
NSString *type = [dict objectForKey:@"type"];
if ([type isEqualToString:@"txt"])
{
[self parseText:dict result:resultString config:config];
}
else if ([type isEqualToString:@"img"])
{
[self parseImageData:dict result:resultString imageArr:imageArr config:config];
}
}
CTFramesetterRef setter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)resultString);
// 计算绘制区域高度
CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
CGSize textSize = CTFramesetterSuggestFrameSizeWithConstraints(setter, CFRangeMake(0, 0), nil, restrictSize, nil);
CGMutablePathRef pathRef = CGPathCreateMutable();
UIScreen *screen = [UIScreen mainScreen];
if (textSize.height > screen.bounds.size.height)
{
CGPathAddRect(pathRef, NULL, CGRectMake(0, 0, config.width, screen.bounds.size.height));
}
else
{
CGPathAddRect(pathRef, NULL, CGRectMake(0, 0, config.width, textSize.height));
}
CTFrameRef frame = CTFramesetterCreateFrame(setter, CFRangeMake(0, resultString.length), pathRef, nil);
data.ctframe = frame;
data.height = textSize.height;
data.imageArr = imageArr;
// 书中的代码有问题,这里不能释放
// CFRelease(frame);
CFRelease(pathRef);
CFRelease(setter);
return data;
}
+ (void)parseText:(NSDictionary *)dict
result:(NSMutableAttributedString *)result
config:(nonnull CTFrameParseConfig *)config
{
NSString *content = [dict objectForKey:@"content"];
UIColor *color = [UIColor colorWithHexString:[dict objectForKey:@"color"]];
UIFont *font = [UIFont systemFontOfSize:[[dict objectForKey:@"size"] floatValue]];
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
style.lineSpacing = config.lineSpace;
style.lineBreakMode = NSLineBreakByWordWrapping;
style.firstLineHeadIndent = 2;
NSAttributedString *as = [[NSAttributedString alloc] initWithString:content attributes:@{NSFontAttributeName:font,NSForegroundColorAttributeName:color, NSParagraphStyleAttributeName:style}];
[result appendAttributedString:as];
}
+ (void)parseImageData:(NSDictionary *)dict
result:(NSMutableAttributedString *)result
imageArr:(NSMutableArray *)imageArr
config:(nonnull CTFrameParseConfig *)config
{
NSString *url = [dict objectForKey:@"url"];
NSString *name = [dict objectForKey:@"name"];
NSString *width = [dict objectForKey:@"width"];
NSString *height = [dict objectForKey:@"height"];
ImageElementModel *model = [[ImageElementModel alloc] init];
model.url = url;
model.name = name;
model.width = [width floatValue];
model.height = [height floatValue];
[imageArr addObject:model];
NSMutableDictionary *dic = [NSMutableDictionary dictionary];
[dic setValue:[NSNumber numberWithFloat:model.width] forKey:@"width"];
[dic setValue:[NSNumber numberWithFloat:model.height] forKey:@"height"];
[dic setValue:[NSNumber numberWithLong:imageArr.count-1] forKey:@"index"];
model.dict = dic;
NSAttributedString *as = [self replaceImageDataWithWhiteSpace:model config:config];
[result appendAttributedString:as];
}
static CGFloat ascentCallback(void *bef)
{
return [(NSNumber *)[(__bridge NSDictionary *)bef objectForKey:@"height"] floatValue];
}
static CGFloat descentCallback(void *bef)
{
return 0;
}
static CGFloat widthCallback(void *bef)
{
return [(NSNumber *)[(__bridge NSDictionary *)bef objectForKey:@"width"] floatValue];
}
+ (NSAttributedString *)replaceImageDataWithWhiteSpace:(ImageElementModel *)model
config:(nonnull CTFrameParseConfig *)config
{
CTRunDelegateCallbacks callbacks;
memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
callbacks.version = kCTRunDelegateVersion1;
callbacks.getAscent = ascentCallback;
callbacks.getWidth = widthCallback;
callbacks.getDescent = descentCallback;
CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void * _Nullable)(model.dict));
// 使用0xFFFC作为空白站位符
unichar ojectReplacement = 0xFFFC;
NSString * placeHolderStr = [NSString stringWithCharacters:&ojectReplacement length:1];
NSMutableAttributedString *atStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)atStr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
CFRelease(delegate);
return atStr;
}
@end