一、Core Text简介
Core Text是苹果官方提供的底层文字排版引擎,通过它,我们可以精确地控制文字的排版:如文字颜色、字体、字与字间距、行间距。甚至可以精确地控制每行的位置,计算某串文字的范围。正因为它有如此强大的功能,好多富文本的效果,都是通过它来实现的。而且使用它来做富文本效果,比通过UIWebview+js,效率更高,内存占用更少。只是其入门门槛较高,使用上稍微有点麻烦。
二、Core Text类及其关系(图片来自Apple官方):
CTFramesetter:
通过一个属性字符串初始化它,通过它可以创建CTFrame
CTFrame:
可以理解成一段文字,这段文字可能是创建CTFramesetter时,所使用的属性字符串的部分或者全部。
CTLine:
CTFrame又是由一行一行的文字组成,每一行文字对应一个CTLine对象;
可以从CTFrame中获取,也可以单独通过一个属性字符串创建它
通过相关API,可以获取到CTLine的位置,在CTFrame所代表的字符串中的范围,可以获取这一行文字的上行距离(ascend)、下行距离(descent)
CTRun:
每行文字又是由一个一个的字符组成,而每一个字符对应一个CTRun对象。
由CoreText自动创建,我们只需通过相关API,从CTLine中获取即可。
三、排版-相关属性(图片来自Apple官方)
1、红色线条:BaseLine 是一条假想出来的线,
2、Ascned: 上行距离,即baseLine 到 该行文字最高点的距离
3、Descend: 下行距离,即BaseLine到该行文字最低点的距离
所以在有emoji表情的文字中,因为emoji的descent比文字的descent大,所以整行的descent的为emoji的descnet,
所以会出现在有emoji的一段文字中,行间距不一致的情况。
特别说明:
1、通过相关CoreText API 获取到的Descend是大于0的,而不是向网上其他查到的资料所说的是小于0的
i.e CTLineGetTypographicBounds ( CTLineRef line, CGFloat *ascent, CGFloat *descent, CGFloat *leading )
所获取到的descent是大于0的
2、UIFont的descender属性,倒是小于0的
四、坐标系
1、UIKit的坐标系是左上角为原点,X轴向右为正,Y轴向下为正
2、而Core Text使用的坐标系是左下角为原点,X轴向右为正,Y轴向上为正。
当进入drawRect方法前,为了与UIKit的坐标系一致,系统内部会自动翻转了一下坐标系,变成左上角为原点
所以进入到drawRect方法时,若没有再次进行坐标系的翻转、平移,直接通过CoreText进行文字排版,会出现文字镜像倒置的现象。
再次翻转坐标系,变成左下角为原点:
CGContextRef ctx = UIGraphicsGetCurrentContext();
//平移
CGContextTranslateCTM(ctx, 0, rect.size.height);
//翻转Y轴
CGContextScaleCTM(ctx, 1, -1);
//设置文字矩阵
CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
五、一个实例
- (void)drawRect:(CGRect)rect {
if (_text.length == 0) {
return;
}
rect.size.height = SYTLabel_MAX_Height;
//将坐标系再次变为原点在左下角的笛卡尔坐标系
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(ctx, 0, rect.size.height);
CGContextScaleCTM(ctx, 1, -1);
CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
//通过要渲染的文本,创建FrameSetter
CTFramesetterRef setter = [self createFrameSetterWithText:_text];
//创建一帧 指定画在rect的矩阵区域内
CTFrameRef frame = [self createFrameInRect:rect fromSetter:setter];
//获取所有CTLine对象
NSArray *lines = (__bridge NSArray*)CTFrameGetLines(frame);
NSInteger count = lines.count;
CGPoint origins[count];
//获取每行的原点 注意这个原点是在BaseLine上的 请参考三、排版属性下的那张图
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
//此时坐标原点左下角 第一行的起始Y值 = rect.size.height - 行高 (这跟我们的书写习惯是一致的)
CGFloat y = rect.size.height;
//行高=font.ascender-_font.descender 为什么是减掉descender呢? 因为UIFont的descender是小于0的
//这里行高使用字体的行高 是为了避免文本中存在emoji 导致行间距不一致
CGFloat lineH = ceil(_font.ascender - _font.descender);
for (NSInteger i = 0; i < count; i++) {
CGPoint origin = origins[i];
CTLineRef line = (__bridge CTLineRef)lines[i];
//测试发现获取到的descend是大于0的
CGFloat ascend,descend;
CTLineGetTypographicBounds(line, &ascend, &descend, NULL);
y = y - lineH;
CGContextSetTextPosition(ctx, origin.x, y);
CTLineDraw(line, ctx);
y -= _lineSpace;
}
CFRelease(frame);
CFRelease(setter);
}
六、后记
更多相关API用法,请参考官方API文档。