CoreText
原理
为了方便使用,需要先创建一个自定义UIView,我们将在
drawRect
函数里使用CoreText。
- (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);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddEllipseInRect(path, NULL, self.bounds);
NSAttributedString *attString = [[NSAttributedString alloc] initWithString:@"Hello World"];
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attString.length), path, NULL);
CTFrameDraw(frame, context);
CFRelease(frame);
CGPathRelease(path);
CGContextRelease(context);
}
- 我们首先创建了一个
CGContextRef
上下文。 - 因为CoreText的坐标系是以右下角为原点,所以我们将CoreText坐标系翻转一下。
- 创建了一个
CGPath
,来完成绘制文字的区域。CoreText支持矩形、环形。因为用整个View来做显示区域,所以我们通过self.bounds
来创建CGPath
。 - 在CoreText我们要渲染的文字需要用
NSAttributedString
来创建,它允许你对文字颜色、字体、大小设置不同的样式。 -
CTFramesetterRef
是CoreText非常重要的一个类,他管理了你得字体和文本渲染块(CTFrame)
。最简单的创建就是通过NSAttributedString
来创建一个CTFramesetterRef
。然后通过刚创建的CTFramesetterRef
来创建一个CTFrameRef
。CTFramesetterCreateFrame
的参数分别是frameSetter
、将要显示文字的Range
(这里☞长度attString.length)、文本要显示的区域(刚刚创建的path
)和frameAttributes
(可以为空)。 - 通过
CTFrameDraw
在给定的context
里绘制frame
。 - 最后不要忘了清理资源。
CoreText对象模型
在CTFrame内部是由多个CTline组成,每行CTline又是由多个CTRun组成
每个CTRun代表一组风格一致的文本(CTline和CTRun的创建不需要我们管理)
在CTRun中我们可以设置代理来指定绘制此组文本的宽高和排列方式等信息
我们通过NSAttributedString
创建一个CTFramesetter
,这时候会自动创建一个 CTTypesetter
实例,它负责管理字体,下面通过CTFramesetter
来创建一个或多个frame来渲染文字。然后Core Text会根据frame的大小自动创建CTLine
(每行对应一个CTLine)和CTRun
(相同格式的一个或多个相邻字符组成一个CTRun)。
举例来说,Core Text将创建一个CTRun来绘制一些红色文字,然后创建一个CTRun
来绘制纯文本,然后再创建一个CTRun
来绘制加粗文字等等。要注意,你不需要自己创建CTRun
,Core Text将根据NSAttributedString
的属性来自动创建CTRun
。每个CTRun
对象对应不同的属性,正因此,你可以自由的控制字体、颜色、字间距等等信息。
#import "CoreTextData.h"
@interface CoreTextData : NSObject
/** 文本绘制的区域大小 */
@property (nonatomic, assign) CTFrameRef ctFrame;
/** 文本绘制区域高度 */
@property (nonatomic, assign) CGFloat height;
/** 文本中存储图片信息数组 */
@property (nonatomic, strong) NSMutableArray *imageArray;
/** 文本中存储链接信息数组 */
@property (nonatomic, strong) NSMutableArray *linkArray;
@end
@implementation CoreTextData
- (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 fillImagePosition];
}
- (void)fillImagePosition {
if (self.imageArray.count == 0) {
return;
}
// 此处利用CTRun代理设置一个空白的字符给定宽高,最后在利用CGContextDrawImage将其绘制
// 获取CTFrame中所有的line
NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
NSUInteger lineCount = [lines count];
// 利用CGPoint数组获取所有line的起始坐标
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);
// 获取图片数组中第一个图片信息
int imgIndex = 0;
CoreTextImageData * imageData = self.imageArray[0];
for (int i = 0; i < lineCount; ++i) {
// 如果不存在图片则返回
if (imageData == nil) {
break;
}
// 存在图片信息则获取图片具体位置信息
// 获取每行信息
CTLineRef line = (__bridge CTLineRef)lines[i];
// 得到每行的CTRun信息,并遍历
NSArray * runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
for (id runObj in runObjArray) {
CTRunRef run = (__bridge CTRunRef)runObj;
NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
// 获取CTRun的代理信息,若无代理信息则直接进入下次循环
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;
// 找到CTRunDelegate中的宽度并给上升和下降高度赋值
runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
runBounds.size.height = ascent + descent;
// 获取CTRun在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;
// 获取路径,并利用路径获取绘制视图的Rect
CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
CGRect colRect = CGPathGetBoundingBox(pathRef);
CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
// 保存图片位置信息
imageData.imagePosition = delegateBounds;
imgIndex++;
if (imgIndex == self.imageArray.count) {
imageData = nil;
break;
} else {
imageData = self.imageArray[imgIndex];
}
}
}
}
@end
#import "CoreTextImageData.h"
@interface CoreTextImageData : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) CGRect imagePosition;
@end
#import "CoreTextLinkData.h"
@implementation CoreTextLinkData
+ (CoreTextLinkData *)touchLinkInView:(UIView *)view atPoint:(CGPoint)point data:(CoreTextData *)data {
CTFrameRef ctFrame = data.ctFrame;
CFArrayRef lines = CTFrameGetLines(ctFrame);
if (lines == nil) {
return nil;
}
CFIndex linesCount = CFArrayGetCount(lines);
CoreTextLinkData *linkdata = nil;
CGPoint linesOrigins[linesCount];
CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, 0), linesOrigins);
//由于CoreText和UIKit坐标系不同所以要做个对应转换
CGAffineTransform transform = CGAffineTransformMakeTranslation(0, view.bounds.size.height);
transform = CGAffineTransformScale(transform, 1, -1);
for (int i = 0; i < linesCount; i ++) {
CGPoint linePoint = linesOrigins[i];
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
//获取当前行的rect信息
CGRect flippedRect = [self getLineBounds:line point:linePoint];
//将CoreText坐标转换为UIKit坐标
CGRect rect = CGRectApplyAffineTransform(flippedRect, transform);
//判断点是否在Rect当中
if (CGRectContainsPoint(rect, point)) {
//获取点在line行中的位置
CGPoint relativePoint = CGPointMake(point.x - CGRectGetMinX(rect), point.y - CGRectGetMinY(rect));
//获取点中字符在line中的位置(在属性文字中是第几个字)
CFIndex idx = CTLineGetStringIndexForPosition(line, relativePoint);
//判断此字符是否在链接属性文字当中
linkdata = [self linkAtIndex:idx linkArray:data.linkArray];
break;
}
}
return linkdata;
}
+ (CGRect)getLineBounds:(CTLineRef)line point:(CGPoint)point {
//配置line行的位置信息
CGFloat ascent = 0;
CGFloat descent = 0;
CGFloat leading = 0;
//在获取line行的宽度信息的同时得到其他信息
CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CGFloat height = ascent + descent;
return CGRectMake(point.x, point.y, width, height);
}
+ (CoreTextLinkData *)linkAtIndex:(CFIndex)i linkArray:(NSArray *)linkArray {
CoreTextLinkData *linkdata = nil;
for (CoreTextLinkData *data in linkArray) {
if (NSLocationInRange(i, data.range)) {
linkdata = data;
break;
}
}
return linkdata;
}
@end