CoreText简介
处理文字和字体的底层技术。它直接和Core Graphics打交道,是iOS和OSX底层的告诉二维图像渲染引擎。Quartz能够直接处理字体和字形,将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。因此CoreText为了排版,需要将显示的文本内容、位置、字体等信息传递给Quartz。与其他组件相比,具有更高效的排版功能。
UIWebView也可以作为处理复杂的文字排版的备选方案。两者之间的比较:
1.CoreText占用内存更少,渲染速度更快;UIWebView占用内存更多,渲染速度较慢;
2.CoreText在渲染前就可以精确的获取展示区域的高度(只要有了CTFrame即可);UIWebView只有在加载完成之后才能知道内容的高度(且得利用JavaScript);
3.CoreText的CTFrame可以在子线程渲染;UIWebView只能在主线程渲染;
4.CoreText渲染出得内容不能像UIWebView那样方便的支持内容的复制;
5.基于CoreText来排版,需要自己处理很多复杂逻辑,包括图文混排相关逻辑,点击的操作。
相关概念
CGMutablePathRef --- CoreGraphics 下的CGPath。CGPath CGMutablePath 都定义了画path的方法,要画一个Quartz Path 到一个Context:需要通过方法 CGContextAddPath 添加path 到 graphics context,然后调用context的drawing(画图)方法。
CTFrameSetterRef --- CTFramesetter 类型用于生成text frames,CTFramesetter是CTFrame对象的对象工厂。CTFramesetter 获取attributed类型对象和一个形状描述对象,创建line 对象填充形状。输出是一个包含了一个line数组的frame对象,frame 可以直接把自己画在graphic context。
CTFrameRef --- CoreText的frame,渲染区域;每个CTFrame对象代表着一个段落。这个frame 对象是由framesetter 对象生成的能够画整个text frame 到当前的graphic context,这个frame对象包含了多行数组,这些数组能够检索单个的渲染和字形信息。
CTLineRef --- 见下方图示;
CTRunRef --- 见下方图示;
在CTFrame内部,是有多个CTLine类组成的,每一个CTLine代表一行,每个CTLine又是由多个CTRun来组成,每一个CTRun代表一组显示风格一致的文本。我们不用手工管理CTLine和CTRun的创建过程。
framesetter 调用一个typesetter对象生成 frame, 在 frame 中放置文本, framesetter将段落样式应用到这个 frame 上, 包括对齐方式, 制表符, 行间距, 缩进和断句模式. typesetter 将属性字符串中的字符转换成字形, 并将字形填充到文本框的行中
每个 CTFrame 对象包括段落的行对象(CTLine). 每个行对象代表着一行文本. 一个 CTFrame对象可能只包含一行很长的CTLine 对象或者很多行. 在framesetting操作过程中, 会创建行对象. 这些行对象跟 frame 一样, 可以直接将自己绘制到图像上下文中.
每个行对象包含一个数组的glyph run(CTRun)对象. 一个glyph run 每一行对象都包含一系列连续不断的字形,这些字形都包含相同的属性和方向. typesetter会在从字符产生行时创建glyph run对象. 这就意味着, 一个行对象是由一个或多个glyphs run 构成. glyphs run 可以将自己绘入图像上下文中, 若非必要时,大多数客户端不需要直接跟 glyph run 打交道.
相关方法
CFArrayRef CTFrameGetLines(CTFrameRef frame) //获取包含CTLineRef的数组
void CTFrameGetLineOrigins(CTFrameRef frame,CFRange range,CGPoint origins[])//获取所有CTLineRef的原点
CFRange CTLineGetStringRange(CTLineRef line) //获取line中文字在整段文字中的Range
CFArrayRef CTLineGetGlyphRuns(CTLineRef line)//获取line中包含所有run的数组
CFRange CTRunGetStringRange(CTRunRef run)//获取run在整段文字中的Range
CFIndex CTLineGetStringIndexForPosition(CTLineRef line,CGPoint position)//获取点击处position文字在整段文字中的index
CGFloat CTLineGetOffsetForStringIndex(CTLineRef line,CFIndex charIndex,CGFloat* secondaryOffset)//获取整段文字中charIndex位置的字符相对line的原点的x值
最简单的文本渲染
- (void)drawRect:(CGRect)rect {
// Drawing code
[super drawRect:rect];
//1.创建上下文
CGContextRef context = UIGraphicsGetCurrentContext();
//2.旋转坐坐标系(默认和UIKit坐标是相反的)
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
//创建绘制区域
CGMutablePathRef path1 = CGPathCreateMutable();
//将绘制区域添加到rect中
// CGPathAddRect(path, NULL, self.bounds);
//因为坐标系是反的 所以再上面的段落rect需要注意一下
CGPathAddRect(path1, NULL, CGRectMake(0, self.bounds.size.height/2, self.bounds.size.width, self.bounds.size.height/2));
//设置绘制内容
NSAttributedString *attString = [[NSAttributedString alloc] initWithString:@"凯文·加内特(Kevin Garnett),1976年5月19日出生在美国南卡罗来纳,前美国职业篮球运动员,司职大前锋/中锋,绰号狼王(森林狼时期)、KG(名字缩写)、The BIG TICKET、Da Kid。"
"1995年NBA选秀,凯文·加内特首轮第五顺位被明尼苏达森林狼队选中,2003-04赛季获得常规赛MVP。2007年夏季转会至波士顿凯尔特人,和雷·阿伦和保罗·皮尔斯一起形成了“凯尔特人三巨头”,2008年的总决赛中击败湖人队,获得NBA总冠军。2013年,加内特被交易至布鲁克林篮网队。2015年重回明尼苏达森林狼队。"];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
//第一个段落
CTFrameRef frame1 = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [attString length]/2), path1, NULL);
//开始绘制
CTFrameDraw(frame1, context);
CGMutablePathRef path2 = CGPathCreateMutable();
CGPathAddRect(path2, NULL, CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height/2));
//第二个段落
CTFrameRef frame2 = CTFramesetterCreateFrame(framesetter, CFRangeMake([attString length]/2, [attString length]/2), path2, NULL);
CTFrameDraw(frame2, context);
//释放资源
CFRelease(framesetter);
CFRelease(frame1);
CFRelease(path1);
CFRelease(frame2);
CFRelease(path2);
}
@end
注意,quartz和OSX的坐标系都是右下角,而iOS的坐标系圆点是左上角,所以在iOS中使用时需要旋转一下坐标系。
//2.旋转坐坐标系(默认和UIKit坐标是相反的)
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
步骤解释:
1.获得当前绘图上下文;
2.旋转坐标系;
3.创建绘制区域,CGMutablePath;
4.将绘制区域添加到Rect中,CGPathAddRect(path, NULL, self.bounds);
5.根据NSAttributedString绘制内容生成一个CTFramesetterRef对象;
6.分别得到两个CTFrameRef对象,段落一和段落二;
7.绘制,CTFrameDraw(CTFrameRef , CGContextRef);
8.释放用到的CGMutablePath,CTFramesetterRef,CTFrameRef对象;
这里注意,跟Quartz打交道的类基本都不支持ARC,需要手动释放。
图文混排
思路:其实对于图片的排版,CoreText本身是不支持的,但是可以在需要插入图片的地方,用一个空白字符代替,同时设置该字符的CTRunDelegate
为要显示图片的宽高信息,这样生成的CTFrame对象就会在绘制时,把图片的位置预留出来,之后,在drawRect
方法中调用CGContextDrawImage
方法直接绘制出来进行了。
主要代码如下,完整代码请看Demo。
+ (TBZCoreTextData *)parseTemplateFile:(NSString *)path config:(TBZFrameParserConfig *)config{
NSMutableArray *mArr = [NSMutableArray array];
NSAttributedString *attString = [self loadTemplateFile:path config:config imageArray:mArr];
TBZCoreTextData *data = [self parseAttributedContent:attString config:config];
data.imageArray = mArr;
return data;
}
//方法二:读取JSON文件内容,并且调用方法三获得从NSDcitionay到NSAttributedString的转换结果
+ (NSAttributedString *)loadTemplateFile:(NSString *)path config:(TBZFrameParserConfig *)config imageArray:(NSMutableArray *)imageArray{
NSData *data = [NSData dataWithContentsOfFile:path];
NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
if (data) {
NSArray *array = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
if ([array isKindOfClass:[NSArray class]]) {
for (NSDictionary *dict in array) {
NSString *type = dict[@"type"];
//区分文本和图片
if ([type isEqualToString:@"txt"]) {
NSAttributedString *as = [self parseAttributeContentFromNSDictionary:dict config:config];
[result appendAttributedString:as];
}else if ([type isEqualToString:@"img"]){
//创建TBZCoreImageData,保存图片到imageArray数组中
TBZCoreImageData *imageData = [[TBZCoreImageData alloc] init];
//设置图片的名字字符串;
imageData.name = dict[@"name"];
//设置图片的插入位置
imageData.position = [result length];
[imageArray addObject:imageData];
//创建空白占位符,并且设置它的CTRunDelegate信息
NSAttributedString *as = [self parseImageDataFromNSDictionary:dict config:config];
[result appendAttributedString:as];
}
}
}
}
return result;
}
//接受一个NSAttributedString和一个Config参数,将NSAttributedString转换成CoreTextData返回
+ (TBZCoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(TBZFrameParserConfig *)config{
//创建CTFrameStterRef实例
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
//获得要绘制的区域的高度
CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, [content length]), nil, restrictSize, nil);
CGFloat textHeight = coreTextSize.height;
//生成CTFrameRef实例
CTFrameRef frame = [self createFrameWithFramesetter:framesetter config:config height:textHeight];
//将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回CoreTextData实例
TBZCoreTextData *data = [[TBZCoreTextData alloc] init];
data.ctFrame = frame;
data.height = textHeight;
//释放内存
CFRelease(framesetter);
CFRelease(frame);
return data;
}
#pragma mark - 添加设置CTRunDelegate信息的方法
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 *)parseImageDataFromNSDictionary:(NSDictionary *)dict config:(TBZFrameParserConfig *)config{
CTRunDelegateCallbacks callbacks;
memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
callbacks.version = kCTRunDelegateVersion1;
callbacks.getAscent = ascentCallback;
callbacks.getDescent = descentCallback;
callbacks.getWidth = widthCallback;
//将宽高信息通过delegate返回
CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)dict);
//使用0xFFFC作为空白占位符
unichar objectReplacementChar = 0xFFFC;
NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:1];
NSDictionary *attributes = [self attributesWithConfig:config];
NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:content attributes:attributes];
//将CTRunDelegate对象跟CTAttributedString绑定
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
CFRelease(delegate);
return space;
}
//TBZCoreTextData.m
-(void)setImageArray:(NSArray *)imageArray{
_imageArray = imageArray;
[self fillImagePosition];
}
//填充图片
-(void)fillImagePosition{
if (self.imageArray.count==0) {
return;
}
NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
NSInteger lineCount = [lines count];
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);
int imgIndex = 0;
TBZCoreImageData *imageData = self.imageArray[0];
for (int i=0; i<lineCount; i++) {
if (imageData==nil) {
break;
}
//获得line对象
CTLineRef line = (__bridge CTLineRef)lines[i];
NSArray *runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
//遍历该line对象中的run对象
for (id runObj in runObjArray) {
CTRunRef run = (__bridge CTRunRef)runObj;
NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
//如果该run对象没有CTRunDelegate对象,则结束本次循环,继续下一次循环
if (delegate == nil) {
continue;
}
//得到CTRunDelegate对象绑定的数据,CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)dict);
NSDictionary *metaDic = CTRunDelegateGetRefCon(delegate);
//验证数据的格式
if (![metaDic isKindOfClass:[NSDictionary class]]) {
continue;
}
//计算图片的rect
CGRect runBounds;
CGFloat ascent;
CGFloat descent;
runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
runBounds.size.height = ascent + descent;
CGFloat x0ffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
runBounds.origin.x = lineOrigins[i].x + x0ffset;
runBounds.origin.y = lineOrigins[i].y;
runBounds.origin.y -= descent;
CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
CGRect colRect = CGPathGetBoundingBox(pathRef);
CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
imageData.imagePostion = delegateBounds;
imgIndex ++;
if (imgIndex == self.imageArray.count) {
//所有图片都处理完 结束遍历
imageData = nil;
break;
}else{
imageData = self.imageArray[imgIndex];
}
}
}
}
添加对图片的点击支持
需要给展示view添加单击手势,判断手势触摸点是否在图片中,实现该功能。
代码如下:
- (instancetype)initWithFrame:(CGRect)frame{
if ([super initWithFrame:frame]) {
[self addGesture];
}
return self;
}
- (void)addGesture{
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGestureRecognizer:)];
[self addGestureRecognizer:tap];
self.userInteractionEnabled = YES;
}
- (void)tapGestureRecognizer:(UITapGestureRecognizer *)recognizer{
CGPoint point = [recognizer locationInView:self];
for (TBZCoreImageData *data in self.textData.imageArray) {
//翻转坐标系,因为ImageData中的坐标是CoreText的坐标系
CGRect imageRect = data.imagePostion;
CGPoint imagePosition = imageRect.origin;
imagePosition.y = self.bounds.size.height - imageRect.origin.y - imageRect.size.height;
CGRect rect = CGRectMake(imagePosition.x, imagePosition.y, imageRect.size.width, imageRect.size.height);
//检测点击位置Point是否在rect之内
if (CGRectContainsPoint(rect, point)) {
//在这里处理点击后的逻辑
[self showTapImage:data];
break;
}
}
}
- (void)showTapImage:(TBZCoreImageData *)data{
UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
//图片
tapImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:data.name]];
tapImageView.frame = CGRectMake(0, 0, data.imagePostion.size.width, data.imagePostion.size.height);
tapImageView.center = keyWindow.center;
//蒙版
coverView = [[UIView alloc] initWithFrame:keyWindow.bounds];
[coverView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(cancel)]];
coverView.backgroundColor = [UIColor colorWithRed:0/255.0 green:0/255.0 blue:0/255.0 alpha:0.6];
coverView.userInteractionEnabled = YES;
[keyWindow addSubview:coverView];
[keyWindow addSubview:tapImageView];
}
- (void)cancel{
[tapImageView removeFromSuperview];
[coverView removeFromSuperview];
}
还是得注意坐标系是反的,很关键。
排版中增加链接格式,实现点击链接title,打开网页
思路:将链接的title渲染到frame中,当触摸点在这个title的range中时,拿到对应的url打开。
主要代码如下:
+ (NSAttributedString *)loadTemplateFile:(NSString *)path config:(TBZFrameParserConfig *)config imageArray:(NSMutableArray *)imageArray linkArray:(NSMutableArray *)linkArray{
NSData *data = [NSData dataWithContentsOfFile:path];
NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
if (data) {
NSArray *array = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
if ([array isKindOfClass:[NSArray class]]) {
for (NSDictionary *dict in array) {
NSString *type = dict[@"type"];
//区分文本和图片
if ([type isEqualToString:@"txt"]) {
NSAttributedString *as = [self parseAttributeContentFromNSDictionary:dict config:config];
[result appendAttributedString:as];
}else if ([type isEqualToString:@"img"]){
//创建TBZCoreImageData,保存图片到imageArray数组中
TBZCoreImageData *imageData = [[TBZCoreImageData alloc] init];
//设置图片的名字字符串;
imageData.name = dict[@"name"];
//设置图片的插入位置
imageData.position = [result length];
[imageArray addObject:imageData];
//创建空白占位符,并且设置它的CTRunDelegate信息
NSAttributedString *as = [self parseImageDataFromNSDictionary:dict config:config];
[result appendAttributedString:as];
}else if ([type isEqualToString:@"link"]){
NSUInteger startPo = [result length];
NSAttributedString *as = [self parseAttributeContentFromNSDictionary:dict config:config];
[result appendAttributedString:as];
NSRange linkRange = NSMakeRange(startPo, result.length - startPo);
TBZCoreUrlData *urlData = [[TBZCoreUrlData alloc] init];
urlData.title = dict[@"content"];
urlData.url = dict[@"url"];
urlData.range = linkRange;
[linkArray addObject:urlData];
}
}
}
}
return result;
}
///TBZUrlMixedView.m
//检测点击位置是否在链接上
- (TBZCoreUrlData *)touchLinkInView:(UIView *)view atPoint:(CGPoint)point data:(TBZCoreTextData *)data{
CTFrameRef textFrame = data.ctFrame;
CFArrayRef lines = CTFrameGetLines(textFrame);
if (!lines) return nil;
CFIndex count = CFArrayGetCount(lines);
TBZCoreUrlData *foundLink = nil;
//获得每一行的origin坐标
CGPoint origins[count];
CTFrameGetLineOrigins(textFrame, CFRangeMake(0, 0), origins);
//翻转坐标系
CGAffineTransform tranform = CGAffineTransformMakeTranslation(0, view.bounds.size.height);
tranform = CGAffineTransformScale(tranform, 1.f, -1.f);
for (int i=0; i<count; i++) {
CGPoint linePoint = origins[i];
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
//获取每一行的CGRect信息
CGRect flippedRect = [self getLineBounds:line point:linePoint];
CGRect rect = CGRectApplyAffineTransform(flippedRect, tranform);
if (CGRectContainsPoint(rect, point)) {
//将点击的坐标转换成相对于当前行的坐标
CGPoint relativePoint = CGPointMake(point.x-CGRectGetMinX(rect), point.y-CGRectGetMinY(rect));
//获得当前点击坐标对应的字符串偏移
CFIndex idx = CTLineGetStringIndexForPosition(line, relativePoint);
//判断这个偏移是否在我们的链接列表中
foundLink = [self linkAtIndex:idx linkArray:data.linkArray];
return foundLink;
}
}
return nil;
}
//获取每一行的CGRect信息
- (CGRect)getLineBounds:(CTLineRef)line point:(CGPoint)point{
CGFloat ascent = 0.0f;
CGFloat descent = 0.0f;
CGFloat leading = 0.0f;
CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CGFloat height = ascent + descent;
return CGRectMake(point.x, point.y, width, height);
}
//判断这个偏移是否在我们的链接列表中
- (TBZCoreUrlData *)linkAtIndex:(CFIndex)i linkArray:(NSArray *)linkArray{
TBZCoreUrlData *link = nil;
for (TBZCoreUrlData *data in linkArray) {
if (NSLocationInRange(i, data.range)) {
link = data;
break;
}
}
return link;
}
觉得有用,请帮忙点亮红心
Better Late Than Never!
努力是为了当机会来临时不会错失机会。
共勉!