iOS UI 优化 - Core Text
Core Text 富文本编辑
在 Core Graphics
后面一篇文章本该对 Core Image
框架进行整理,但是基于 Core Graphics
的富文本编辑 Core Text
框架更方便讲述。而且结合自己兴趣和好玩的程度在讲述自己对 Texture
即 ASDK
和 YYKit
详细描述。
Core Text 结构层级
这里小编对 Core Text
在实现的层次结构上所处位置,以及在对 UIKit
框架我们常见控件 UITextFiled
、UITextView
和 UILabel
具体实现基础。
上图是小编根据 2018 WWDC 中 TextKit Best Practices 和 Introducing Text Kit 得出
Core Graphics
、Core Text
和UIKit
中能够实现文本绘制的控件实现结构图。
根据上面结构可以看出:
(1)基于Quartz
封装的Core Graphics
为Core Text
的实现基础。而且Quartz
可以直接处理字体和字形将文字渲染到界面,也是在 Apple 基础库中唯一一个处理字体模块。
(2)基于Core Text
封装实现的Text Kit
为在UIKit
和AppKit
中文本显示控件TypeTextFiled
、TypeTextView
等提供最直接的接口。
Core Text API 族
Core Text API 族.png
Core Text
作为在OS
对应的五大平台唯一拥有实现文字绘制能力的跨平 台框架,族类API
都是表示CTType
形式。
Opaque Types
集合数据类型 | 表示 & 使用范围 |
---|---|
CTFont (字体) |
1、 表示字体类型参数。字体特征中基本参数表示:字体大小、字体所属的格式或者是转换矩阵等。2、 是在 UIKit 中我们经常使用设置子类类型的 UIFont 的 (__birdge CTFont *) 表现形式,在采用 context 绘制上下文中绘制确定字体。 |
CTFontCollection (字体组) |
1、 表示字体组合。字体组合也即是对一组文字或者一段短文字字体集合,提供对同一段文字不同格式文字内容字体访问和获取。一句话:字体整体表示。 |
CTFontDescriptor (字体描述) |
1、 表示字体类型描述。字体描述可以用获取、指定或者是修改当前字体属性,字体相关属性(字体名字、位置点大小和变化等)。 |
CTFrame (绘制画布) |
1、 表示多行文字绘制画布。CTFrame (绘制画布)是 Core Text 实现文字绘制对外集中体现形式,其中确定绘制参数:画布区域(Range )、绘制路径(Path )和绘制文本参数信息(Attribtes )。2、 对于单行来说可以精确获取在画布:行数和每一行对应开头坐标。在获取绘制的画布时调用对应的 CTFrameDraw(CTFrame, CGContext) 在上下文中绘制文字显示详细内容。 3、 CTFrame 不仅支持在 main thread 中进行绘制,同样也支持在 background Thread 进行内容的绘制。 这也是 YYKit 和 Texture 支持后台绘制控件实现的基础。 |
CTFramesetter (画布🏭) |
1、 表示字体具体绘制生成工厂。画布工厂根据要显示的富文本信息、显示画布路径和画布具体的区域(Range )来生成对应展示的显示画布的效果。2、 CTFramesetter (画布工厂)是采用 CFFrameDraw 来实现富文本编辑绘制基础,但是如果采用 CTLineDraw 或者是 CTRunDraw 形似就另当别论了。 YYText 中绘制的就是按照 CTLineDraw 和 CTRunDraw 来绘制的。
|
CTGlyphInfo (字体信息) |
1、 表示在字体对于 Glyph ID 特殊的映射关系。2、 。 |
CTLine (画布中行) |
1、 表示在具体执行 Frame (画布)中一行。是组成画布绘制 Frame 中单独的一行,同时也是字体不同格式组成在一行中组成最小单元 Run (块)的集合。 2、 在实现富文本绘制的过程中可以采用 CTLineDraw(CTLine, CGContext) 方式来绘制当前行。 |
CTParagaraphStyle (段落格式) |
1、 段落格式表示在文本绘制时,段落段落之间设置基本参数或者单独一段信息基本格式。例如:对其样式、截取样式和排布的方向等等2、 。 |
CTRun (块) |
1、 表示在富文本绘制过程中格式相同最小单元。根据字体设置的 CTFont (字体)参数不同在绘制过程中可以分割为格式一致一个一个单元,既是 Run 。 2、 在文本编辑过程中可以根据在画布中需要显示 Lines ,然后在但单独 Line 中获得 Run 采用 CTRunDraw(CTRun, CGContext) 来实现单个 Run (块)独自绘制。 |
CTRunDelegate (块协议) |
1、 表示在运行时的一个运行委托,在实现计算是可以调整字形上升、下降和当前绘制字形宽高。这个 API 是我们在实现文字图片换混排的基础,在生成 Frame 时计算当前 Line 里面 Run 需要绘制素材类型,然后在改素材类型位置设置 Image 的 Ascent 、Descent 、Width 和 Height 来设置当前图片绘制参数信息,然后在实际绘制中在当前需要绘制的 Image 采用 CGContextDrawImage(CGContext, CGRect, CGImageRef) 绘制当前图片。 |
CTTextTab (文本标签) |
1、 表示在文本段落中样式标签,用来保存段落之间段落对其方式和位置信息。 |
CTTypesetter (排版工厂) |
1、 表示字体具体绘制来执行布局。可以通过 CTFramesetterCreateWithTypesetter(CTTypesetter) 生成上文展示 CTFramesetter (绘制工厂)。
|
Reference
集合数据类型 | 表示 & 使用范围 |
---|---|
Core Text Sting Attributes (富文本展示样式) |
富文本下划线样式设置 。 |
Core Text Structure (结构) |
CTTextTab (标签) 范围和表,查找向量 Header。 。 |
Core Text Enumerations (枚举) |
Core Text 字体描述匹配、指定绑定标识符自动激活、指定 URL 字体注册失败、定义字体注册范文等等。 。 |
Core Text Constants (常量值) |
Core Text 中使用富文本设置一些常量值。 |
Core Text Fundations (功能) |
Core Text 中一些功能参数值。 |
Core Text Data Types (数据分类) |
Core Text 中 ATS 字体参考、字体集合排序描述符回调和 CT 描述符处理进度回调。 |
上面是
Core Text
所有的API
的接口,及其在实现富文本绘制中相对相应的API
的主要功能作用。具体CTFramesetter
怎么样通过NSString
来生成CTFrameRef
然后在UIKit
基础的显示控件上绘制出来的呢?
Core Text
最小系统
这里说的最小系统概念是在电子单片机中最小系统单元,这里指的是 Core Text
实现最基本文字排版。
下面贴出在实现中经典代码:
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
// Step 1
CGContextRef context = UIGraphicsGetCurrentContext();
// Step 2
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
// Step 3
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds);
// Step 4
NSAttributedString *attString = [[NSAttributedString alloc] initWithString:@"Hello world! Welcome to learn RICH TEXT knowladge."];
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attString.length), path, NULL);
// Step 5
CTFrameDraw(frame, context);
// Step 6
CFRelease(frame);
CFRelease(path);
CFRelease(frameSetter);
}
继承
UIView
来自定义FYView
然后重写drawRect:
,在上面drawRect:
重新写上面代码。在Core Text
实际绘制中主要分为:计算转换在UIKit
的坐标、设置绘制Path
、初始化CFFramesetter
画布工厂生成画布和在上下文中绘制。
具体分为 6 个步骤:
Step 1:获取当前绘制上下文context
;
Step 2:把Core Text
中左下角的坐标点转换为UIKit
左上角的坐标点;
Step 3:设置绘制的path
路径,把当前UIView
的bounds
设置为绘制区域;
Step 4:通过String
初始化NSAttributedString
来创建CTFramesetterRef
然后根据画布工厂创建CTFrameRef
;
Step 5:在上下文中绘制画布内容;
Step 6:释放创建frame
、path
和framesetter
的CFTypeRef
对象。
在当前 Core Text
绘制的基础之上,我们在看一下具体绘制实现中 CTTypeRef
中 Framesetter
、Frame
、 Line
和 Run
之间的关系,以及在实际绘制显示在界面上对应。
小编在实现图文混排实现中,贴出下面一段对于富文本 CFFrameRef
来计算 Image
富文本绘制布局代码。
//获取 frameRef 中 Lines
NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
NSInteger lineCount = lines.count;
//获取 frameRef 中 Each Line 开头 Point
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);
NSInteger imgIndex = 0;
//遍历 Each Line
for (int i = 0; i < lineCount; i++) {
CTLineRef lineRef = (__bridge CTLineRef)lines[i];
//获取 Each Line 的 Run
NSArray *runsArray = (NSArray *)CTLineGetGlyphRuns(lineRef);
//遍历 Each Run
for (id runObj in runsArray) {
CTRunRef runRef = (__bridge CTRunRef)runObj;
NSDictionary *runAttribs = (NSDictionary *)CTRunGetAttributes(runRef);
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttribs valueForKey:(id)kCTRunDelegateAttributeName];
if (delegate == nil) {
continue;
}
NSDictionary *metaDic = CTRunDelegateGetRefCon(delegate);
if (![metaDic isKindOfClass:[NSDictionary class]]) {
continue;
}
CGRect runBounds;
CGFloat ascent;
CGFloat descent;
//获取 run 的 width
runBounds.size.width = CTRunGetTypographicBounds(runRef, CFRangeMake(0, 0), &ascent, &descent, NULL);
//根据上下偏移获取 run 的 height
runBounds.size.height = ascent + descent;
CGFloat xOffset = CTLineGetOffsetForStringIndex(lineRef, CTRunGetStringRange(runRef).location, NULL);
runBounds.origin.x = lineOrigins[i].x + xOffset;
runBounds.origin.y = lineOrigins[i].y;
runBounds.origin.y -= descent;
CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
CGRect colRect = CGPathGetBoundingBox(pathRef);
CGRect delegateRect = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
}
}
从上面两段代码可以看出在
CTTypeRef
类族中类的关系如下:
(1)CTFramesetter
通过初始化NSAttributedString
来创建绘制画布CTFrameRef
对应的类;
(2) 通过画布CTFrame
可以获取所有的Line
基本信息,例如:行数、每行Start Point
等参数。再通过坐标转换就可以计算当前Line
在UIKit
界面上布局绘制信息,在实际的绘制时可以选用CTLineDraw(CTLineRef, CGContext)
来替代CTFrameDraw(CTFrameRef, CGContext)
整个画布在当前上下文中绘制;
(3)效仿从CTFrameRef
中获取对应的Line
,同样可以在Each Line
中来获取Core Text
最基本的单元CTRunRef
。通过获取的Each Line
中所有的Run
,然后借助CTRunDelegateRef
在实际绘制过程中动态设置当前Run
块需要绘制Rect
区域。同样在实际绘制时也可以采用CTRunDraw(CTRunRef, CGContext)
来代替CTLineDraw(CTLineRef, CGContext)
来单独绘制每个字形块。
在图文混排过程中分为两种情况,根据情况的不同也可以采用不同的方式实现排版引擎实现:
(a)对于排版的数据来源于Server
也就是我们需要需要访问后才会获取数据,然后来初始化控件,鉴于Network
的延迟性,我们可以预先设置通过设置CTRunDelegate
拓展在展示时的Image
的参数信息;
(b)对要排版的数据信息已知,在实现的基础上对Image
位置插入临时代替的字符串,通过CTDelegateRef
来获取对应Image
基本参数设置当前Run
块的绘制时上下缩进,然后添加到富文本字符串中此时也可以记录当前插入字符串所在的位置。
Core Text 图文混排实现
目前以 Core Text
为基础实现图文混排实现控件 YYKit
系列组件中 YYText
实现逻辑最为清晰,瞒住的情况也最为全面。下面根据要实现图文混排的数据来源做区分来分别讲解排版引擎实现逻辑。
来自 Server
当需要排版的数据来自与 Server
,客户端和后台来商量在模板传输数据格式。这里在本地模拟网络数据加载的数据格式采用 JSON
来实现,不过小编建议如果后台允许的情况下可以尝试 Protobuf
数据格式(后面小编会在网络协议中对该格式的数据进行详细的讲解)。
这里对 Core Text
绘制实现步骤进行划分,然后对每一个步骤的工作进行提取来实现不同的功能。下面区分
(1)
FYEvolveDisplay
:显示类,用于在富文本实际绘制,排版的图片的图片实现填充和图片及链接文本点击操作监听;
(2)FYFrameParser
:排版类,对需要排版的内容加载解析,然后生成排版;
(3)FYFrameParseConfig
:配置类,在排版绘制中文字基本参数的model
配置参数;
(4)FYCoreTextData
:模型类,作为实际绘制中数据显示承载体;
(5)FYCoreTextImageData
:图片配置类,在排版绘制中图片基本参数的model
配置参数;
(6)FYCoreTextLinkData
:链接文本配置类,在排版绘制中链接文字基本参数的model
配置参数;
(7)FYCoreTextLinkUtilts
:链接文本工具类,在FYEvolveDisplay
点击中查找判断当前点击在在具体哪个链接文字。
下面按照(1)数据加载解析生成显示承载 --> (2)然后在绘制 --> (3)最后在点击相应步骤来贴出相关代码段
数据解析
//FYFrameParser
//Step 1
+ (FYCoreTextData *)parseLocalImageTemplateFile:(NSString *)path config:(FYFrameParserConfig *)config {
NSMutableArray *imageArray = [NSMutableArray array];
NSMutableArray *linkArray = [NSMutableArray array];
//解析数据模板
NSAttributedString *imageTemplateAttrib = [self loadLocalTemplateFile:path config:config imageArray:imageArray linkArray:linkArray];
//根据配置信息生成显示载体
FYCoreTextData *coreTextData = [self parseAttributeContent:imageTemplateAttrib config:config];
coreTextData.imageArray = imageArray;
coreTextData.linkArray = linkArray;
return coreTextData;
}
//Step 2
+ (NSAttributedString *)loadLocalTemplateFile:(NSString *)path
config:(FYFrameParserConfig *)config
imageArray:(NSMutableArray *)imageArray
linkArray:(NSMutableArray *)linkArray {
NSData *data = [NSData dataWithContentsOfFile:path];
NSMutableAttributedString *mutableAttrib = [[NSMutableAttributedString alloc] init];
if (data) {
NSArray *templateArray = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
if ([templateArray isKindOfClass: [NSArray class]]) {
for (NSDictionary *dic in templateArray) {
NSString *typeDic = dic[@"type"];
if ([typeDic isEqualToString: @"txt"]) {//parser text data
NSAttributedString *textAttrib = [self parseAttributedContentFormDictionary:dic config:config];
[mutableAttrib appendAttributedString:textAttrib];
}else if ([typeDic isEqualToString: @"img"]) {//parser image data
NSAttributedString *imageAttrib = [self parseImageDataFromDictionary:dic config:config];
[mutableAttrib appendAttributedString:imageAttrib];
FYCoreTextImageData *imageData = [[FYCoreTextImageData alloc] init];
imageData.name = dic[@"name"];
imageData.url = dic[@"url"];
imageData.position = mutableAttrib.length;
[imageArray addObject:imageData];
}else if([typeDic isEqualToString:@"link"]){//parser link text data
NSUInteger startLoc = mutableAttrib.length;
NSAttributedString *linkAttrib = [self parseAttributedContentFormDictionary:dic config:config];
[mutableAttrib appendAttributedString:linkAttrib];
FYCoreTextLinkData *linkData = [[FYCoreTextLinkData alloc] init];
NSUInteger len = mutableAttrib.length - startLoc;
NSRange range = NSMakeRange(startLoc, len);
linkData.range = range;
linkData.title = dic[@"content"];
linkData.url = dic[@"url"];
[linkArray addObject:linkData];
}
}
}
}
return mutableAttrib;
}
//Step 3
+ (NSAttributedString *)parseAttributedContentFormDictionary:(NSDictionary *)dict config:(FYFrameParserConfig *)config {
NSMutableDictionary *attribDic = [[self attributesWithConfig:config] mutableCopy];
//Color
UIColor *color = [UIColor colorFromHexString:dict[@"color"]];
if (color) {
attribDic[(id)kCTForegroundColorAttributeName] = (__bridge id)color.CGColor;
}
//Size
CGFloat fontSize = [dict[@"size"] floatValue];
if (fontSize > 0) {
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
attribDic[(id)kCTFontAttributeName] = (__bridge id)fontRef;
CFRelease(fontRef);
}
NSString *content = dict[@"content"];
return [[NSAttributedString alloc] initWithString:content attributes:attribDic];
}
//Step 4
+ (NSAttributedString *)parseImageDataFromDictionary:(NSDictionary *)dict
config:(FYFrameParserConfig *)config {
CTRunDelegateCallbacks callbacks;
memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
callbacks.version = kCTRunDelegateVersion1;
callbacks.getAscent = ascentCallback;
callbacks.getDescent = descentCallback;
callbacks.getWidth = widthCallback;
CTRunDelegateRef delegateRef = CTRunDelegateCreate(&callbacks, (__bridge void *)(dict));
unichar objectReplaceChar = 0xFFCC;
NSString *conetnt = [NSString stringWithCharacters:&objectReplaceChar length:1];
NSDictionary *attrib = [self attributesWithConfig:config];
NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:conetnt attributes:attrib];
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegateRef);
CFRelease(delegateRef);
return space;
}
//Step 5
+ (FYCoreTextData *)parseAttributeContent:(NSAttributedString *)attribContent config:(FYFrameParserConfig *)config {
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef) attribContent);
//绘制高度
CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, CFRangeMake(0, 0), nil, restrictSize, nil);
CGFloat textHeight = coreTextSize.height;
//生成 CTFrameRef
CTFrameRef frameRef = [self createFrameRefWithFrameSetter:frameSetter config:config height:textHeight];
FYCoreTextData *coreTextData = [[FYCoreTextData alloc] init];
coreTextData.ctFrame = frameRef;
coreTextData.height = textHeight;
CFRelease(frameRef);
CFRelease(frameSetter);
return coreTextData;
}
//Step 6
+ (CTFrameRef)createFrameRefWithFrameSetter:(CTFramesetterRef)frameSetterref
config:(FYFrameParserConfig *)config
height:(CGFloat)height {
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, config.width, height));
CTFrameRef frameRef = CTFramesetterCreateFrame(frameSetterref, CFRangeMake(0, 0), path, NULL);
CFRelease(path);
return frameRef;
}
上面是模拟网络请求数据模板加载本地模板生成显示载体(即画布)过程。
Step 1: 由Step 2
加载本地模拟数据,然后由Step 5
来计算绘制画布的区域和路径生成对应画布交给模型类在自定义的UIKit
控件drawRect:
完成绘制;
Step 2: 获取本地的数据然后解码,遍历其中数据内容解析对应Text
、Image
和LinkText
数据。如果是Text
类型由Step 3
来根据内容配置来生成对应的NSAttributedString
数据,如果是Image
类型就交由Step 4
临时填充单个字符串并且通过CTRunDelegateRef
来在运行时设置当前上下缩进,如果是LinkText
类型的数据还是由Step 3
来完成处理,但是记录当前NSAttributedString
的Start Index
和End Index
位置;
Step 3:此步骤是解析Text
类型的数据,根据设置的FYFrameConfig
配置的基础信息对数据进行解析生成对应NSAttributedString
类型的数据;
Step 4:此步骤是对Image
类型的数据类型对应的CTRunRef
进行临时赋值一个字符,然后通过CTRunDelegateCallbacks
来设置Run
(块)在实际绘制时Ascent
(排版上升) 和Descent
(排版下降)间距以此来作为图片绘制时的高度。然后在实际绘制过程中遍历当前Run
(块) 计算当前Image
绘制的区域,获取图片后采用CGContextDrawImage(CGContext, CGRect, CGImage)
绘制当前图片;
Step 5:通过NSAttributedString
来生成CTFrameRef
画布工厂,计算当前需要绘制内容高度由Step 6
来生成CTFrameRef
(画布)然后赋值给FYCoreTextData
模型类画布;
Step 6: 利用最小单元中通过设置路径使用CFFramesetterRef
(绘制🏭)来生成对应需要排版的CTFrameRef
(画布)。
实际绘制
//FYEvolveDisplayView
//Step 1
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGContextRef contextRef = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(contextRef, CGAffineTransformIdentity);
CGContextTranslateCTM(contextRef, 0, self.bounds.size.height);
CGContextScaleCTM(contextRef, 1.0, -1.0);
if (self.coreTextData) {
CTFrameDraw(self.coreTextData.ctFrame, contextRef);
}
for (FYCoreTextImageData *imageData in self.coreTextData.imageArray) {
UIImage *image = [UIImage imageNamed:imageData.name];
if (image) {
//CGRect rect = imageData.imageRect;
//NSLog(@"image data rect in display view: x:%f y:%f height:%f width:%f", rect.origin.x, rect.origin.y, rect.size.height, rect.size.width);
CGContextDrawImage(contextRef, imageData.imageRect, image.CGImage);
}
}
}
//FYCoreTextData
//Step 2
- (void)fillReplaceCharWithImagePosition {
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);
NSInteger imgIndex = 0;
FYCoreTextImageData *imageData = self.imageArray[0];
for (int i = 0; i < lineCount; i++) {
if (imageData == nil) { break; }
CTLineRef lineRef = (__bridge CTLineRef)lines[i];
NSArray *runsArray = (NSArray *)CTLineGetGlyphRuns(lineRef);
for (id runObj in runsArray) {
CTRunRef runRef = (__bridge CTRunRef)runObj;
NSDictionary *runAttribs = (NSDictionary *)CTRunGetAttributes(runRef);
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttribs 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(runRef, CFRangeMake(0, 0), &ascent, &descent, NULL);
runBounds.size.height = ascent + descent;
CGFloat xOffset = CTLineGetOffsetForStringIndex(lineRef, CTRunGetStringRange(runRef).location, NULL);
runBounds.origin.x = lineOrigins[i].x + xOffset;
runBounds.origin.y = lineOrigins[i].y;
runBounds.origin.y -= descent;
CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
CGRect colRect = CGPathGetBoundingBox(pathRef);
CGRect delegateRect = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
imageData.imageRect = delegateRect;
imgIndex++;
if(imgIndex == self.imageArray.count) {
imageData = nil;
break;
}else {
imageData = self.imageArray[imgIndex];
}
}
}
}
Step 1:上面是在
FYEvolveDisplayView
中drawRect:
采用CTFrameDraw(CTFrameRef, CGContext)
绘制在UIView
上,然后从FYCoreTextData
中逐个绘制FYCoreTextImageData
取出在Step 2
计算当前Image
对一个的区域。然后使用CGContextDrawImage(CGContext, CGRect, CGImageRef)
绘制;
Step 2:这里在上文中有展示。主要一点:CTFrameRef
获取Line
每行,然后在遍历每行的Run
(块)判断其CTRunDelegateRef
对应的协议计算当前Image
对应的区域。
图片和链接文字点击
//FYEvolveDisplayView
//Step 1
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self deployGestureRecognizer];
}
return self;
}
- (void)clickComposeImageInDisplayView:(UIGestureRecognizer *)recognizer {
CGPoint point = [recognizer locationInView:self];
for (FYCoreTextImageData *imageData in self.coreTextData.imageArray) {
CGRect imageRect = imageData.imageRect;
CGPoint imagePosCoreText = imageRect.origin;
//转换 `Core Text`(左下)坐标到 `UIKit`(左上)坐标
imagePosCoreText.y = self.bounds.size.height - imagePosCoreText.y - imageRect.size.height;
CGRect rectInScreen = CGRectMake(imagePosCoreText.x, imagePosCoreText.y, imageRect.size.width, imageRect.size.height);
if (CGRectContainsPoint(rectInScreen, point)) {
NSLog(@"Click image of name: %@ url: %@", imageData.name, imageData.url);
}
}
//
FYCoreTextLinkData *linkData = [FYCoreTextLinkUtils touchLinkTextInDisplayView:self atPoint:point data:self.coreTextData];
if (linkData) {
NSLog(@"Click Link Text. The title is: %@, The Url is %@", linkData.title, linkData.url);
}
}
- (UIGestureRecognizer *)tapGestureRecognizer {
if (_tapGestureRecognizer == nil) {
_tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(clickComposeImageInDisplayView:)];
_tapGestureRecognizer.delegate = self;
}
return _tapGestureRecognizer;
}
//FYCoreTextLinkUtils
//Step 2
+ (FYCoreTextLinkData *)touchLinkTextInDisplayView:(UIView *)view atPoint:(CGPoint)point data:(FYCoreTextData *)textData {
CTFrameRef frameRef = textData.ctFrame;
CFArrayRef lines = CTFrameGetLines(frameRef);
if (!lines) { return nil; }
CFIndex count = CFArrayGetCount(lines);
CGPoint origins[count];
CTFrameGetLineOrigins(frameRef, CFRangeMake(0, 0), origins);
//Translateform coordinate
CGAffineTransform transform = CGAffineTransformMakeTranslation(0, view.bounds.size.height);
transform = CGAffineTransformScale(transform, 1.0f, -1.0f);
for (int i = 0; i < count; i++) {
CGPoint linePoint = origins[i];
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
CGRect lineRect = [self gainLineBounds:line linePoint:linePoint];
CGRect flippedRect = CGRectApplyAffineTransform(lineRect, transform);
if (CGRectContainsPoint(flippedRect, point)) {
CGPoint relativePoint = CGPointMake(point.x - CGRectGetMinX(flippedRect), point.y - CGRectGetMinY(flippedRect));
CFIndex index = CTLineGetStringIndexForPosition(line, relativePoint);
return [self linkTextAtIndex:index linkArray:textData.linkArray];
}
}
return nil;
}
//Step 3
+ (FYCoreTextLinkData *)linkTextAtIndex:(CFIndex)index linkArray:(NSArray *)linkArray {
FYCoreTextLinkData *linkData = nil;
for (FYCoreTextLinkData *data in linkArray) {
if (NSLocationInRange(index, data.range)) {
linkData = data;
break;
}
}
return linkData;
}
//Step 4
+ (CGRect)gainLineBounds:(CTLineRef)line linePoint:(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 - descent, width, height);
}
Step 1:在当前
FYEvoloveDisplayView
添加UITapGestureRecognizer
手势识别,来识别点击的位置。遍历对应FYCoreTextData
中图片Rect
来查找点击位置是否对应Image
,同时执行Step 2
来查找点击是否对应链接文字;
Step 2:在CTFrameRef
遍历每一行由Step 4
来获取对应行Rect
转变为UIKit
坐标,在该行Rect
在当前包含点击Point
时,在由Step 3
遍历对应输出FYCoreTextLinkData
链接文字;
Step 3:在点击每行Line
中通过点击文字Line
中的Index
遍历链接文字对应Range
获取对应点击FYCoreTextLinkData
;
Step 4:通过当前行Start Point
然后获取当前Run
对应Rect
然后计算出对应Line
的Rect
(区域)。
上面代码的 Demo
本地
在本地获取图文混排的数据,具体内容绘制的以 YYText
实现来进行讲述。这里仅仅展示 YYText
的类图,然后贴出提出几个问题。
YYText
类图
下面主要解决一下问题
(1)怎么提供后台绘制的能力?
(2)怎么计算String
、Image
和UIView
实现图文排序?
(3)怎么实现String
设置Text
各种格式设置?
(4)怎么实现具体绘制?
(5)怎么实现富文本上点击事件?
怎么提供后台绘制的能力
//YYText
+ (Class)layerClass {
// 重写 TextAsyncLayer 也即是重新定义 Layer 类
// 实现对重定向
return [YYTextAsyncLayer class];
}
//YYTextAsyncLayer 异步后台绘制
if (task.willDisplay) task.willDisplay(self);
_YYTextSentinel *sentinel = _sentinel;
int32_t value = sentinel.value;
BOOL (^isCancelled)() = ^BOOL() {
return value != sentinel.value;
};
CGSize size = self.bounds.size;
BOOL opaque = self.opaque;
CGFloat scale = self.contentsScale;
//生成背景颜色的 CGTypeRef 类型
CGColorRef backgroundColor = (opaque && self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL;
//判断当前 size 宽高 | 如果不满住条件 | 就要返回当前 Layer Contents 桥接
//实际绘制 获取获取当前绘制线程,在当前线程中绘制 | 提供取消机制,取消就展示 | 在设置的 CGContext 获取绘制 Image | 判断取消的基础上在 main thread 绘制
if (size.width < 1 || size.height < 1) {
CGImageRef image = (__bridge_retained CGImageRef)(self.contents);
self.contents = nil;
if (image) {
dispatch_async(YYTextAsyncLayerGetReleaseQueue(), ^{
//释放 image
CFRelease(image);
});
}
if (task.didDisplay) task.didDisplay(self, YES);
CGColorRelease(backgroundColor);
return;
}
//获取后台配置的线程 |
dispatch_async(YYTextAsyncLayerGetDisplayQueue(), ^{
if (isCancelled()) {
CGColorRelease(backgroundColor);
return;
}
//配置 Context 上下文
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
if (opaque && context) {
//将当前图形状态的 Copy 加入图形堆栈中
CGContextSaveGState(context); {
if (!backgroundColor || CGColorGetAlpha(backgroundColor) < 1) {
//设置填充颜色 | 设置绘制路径 | Path 填充
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));
CGContextFillPath(context);
}
if (backgroundColor) {
CGContextSetFillColorWithColor(context, backgroundColor);
CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));
CGContextFillPath(context);
}
}
//从堆栈中取出设置 | 针对指针 retain +1
CGContextRestoreGState(context);
CGColorRelease(backgroundColor);
}
task.display(context, size, isCancelled);
if (isCancelled()) { //如果取消 | 结束 |
UIGraphicsEndImageContext();
//在主线程展示回调
dispatch_async(dispatch_get_main_queue(), ^{
if (task.didDisplay) task.didDisplay(self, NO);
});
return;
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (isCancelled()) {
dispatch_async(dispatch_get_main_queue(), ^{
if (task.didDisplay) task.didDisplay(self, NO);
});
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
if (isCancelled()) {
if (task.didDisplay) task.didDisplay(self, NO);
} else {
//如果没有取消 | 把 image -> contents
//然后返回展示
self.contents = (__bridge id)(image.CGImage);
if (task.didDisplay) task.didDisplay(self, YES);
}
});
});
//YYTextAsyncLayer 绘制
[_sentinel increase];
if (task.willDisplay) task.willDisplay(self);
//通过 CGContext 设置当前绘制条件 | 调用 YYTextLayout 绘制 | CGContext 绘制绘制生成 Image | 展示
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, self.contentsScale);
CGContextRef context = UIGraphicsGetCurrentContext();
if (self.opaque && context) {
CGSize size = self.bounds.size;
size.width *= self.contentsScale;
size.height *= self.contentsScale;
CGContextSaveGState(context); {
if (!self.backgroundColor || CGColorGetAlpha(self.backgroundColor) < 1) {
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
CGContextFillPath(context);
}
if (self.backgroundColor) {
CGContextSetFillColorWithColor(context, self.backgroundColor);
CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
CGContextFillPath(context);
}
} CGContextRestoreGState(context);
}
//调用展示 display 方法 | 显示展示的内容
task.display(context, self.bounds.size, ^{return NO;});
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.contents = (__bridge id)(image.CGImage);
//把绘制的内容 生成 image 然后赋值给 content
//调用 展示函数回调
if (task.didDisplay) task.didDisplay(self, YES);
}
上面贴出基于
YYTextAsyncLayer
实现在main thread
和background thread
绘制代码。可以看出两者在实际绘制的核心代码是一样的,只是在实现后台异步绘制的时候添加cancel
机制。然后通过在UIGraphicsGetImageFromCurrentImageContext()
获取当前YYText
通过YYTextLayout
绘制之后生成对应的Image
在main thread
实现didDisplay
的回调绘制。
计算 String
、Image
和 UIView
实现图文排序
这里贴出在计算 String
和 Image
,UIView
(附件) 在实际计算的实现类,由于代码量较大就不一一列出给出下面两个类。
//YYTextLayout
+ (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range {
...
//Step 9.4
//Line 291
YYTextLine *line = [YYTextLine lineWithCTLine:ctLine position:position vertical:isVerticalForm];
...
}
//YYTextLine
- (void)setCTLine:(_Nonnull CTLineRef)CTLine {
if (_CTLine != CTLine) {
//
if (CTLine) CFRetain(CTLine);
if (_CTLine) CFRelease(_CTLine);
//
_CTLine = CTLine;
if (_CTLine) {
//获取当前 Line 的宽度值 || 获取当前 line 的 ascent|descent|leading
_lineWidth = CTLineGetTypographicBounds(_CTLine, &_ascent, &_descent, &_leading);
//获取当前 Line 的 Range
CFRange range = CTLineGetStringRange(_CTLine);
_range = NSMakeRange(range.location, range.length);
if (CTLineGetGlyphCount(_CTLine) > 0) {//Line 中字形的数量, 即 CTRun
CFArrayRef runs = CTLineGetGlyphRuns(_CTLine);
CTRunRef run = CFArrayGetValueAtIndex(runs, 0);
//获取第一个 run 字形 | 复制到用户提供的数据缓冲区 |
CGPoint pos;
CTRunGetPositions(run, CFRangeMake(0, 1), &pos);
_firstGlyphPos = pos.x;
} else {
_firstGlyphPos = 0;
}
//
_trailingWhitespaceWidth = CTLineGetTrailingWhitespaceWidth(_CTLine);
} else {
_lineWidth = _ascent = _descent = _leading = _firstGlyphPos = _trailingWhitespaceWidth = 0;
_range = NSMakeRange(0, 0);
}
[self reloadBounds];
}
}
- (void)reloadBounds {
...
//获取 当前行所有的 字形 | 当前字形的数量
CFArrayRef runs = CTLineGetGlyphRuns(_CTLine);
NSUInteger runCount = CFArrayGetCount(runs);
if (runCount == 0) return;
NSMutableArray *attachments = [NSMutableArray new];
NSMutableArray *attachmentRanges = [NSMutableArray new];
NSMutableArray *attachmentRects = [NSMutableArray new];
for (NSUInteger r = 0; r < runCount; r++) {
CTRunRef run = CFArrayGetValueAtIndex(runs, r);
CFIndex glyphCount = CTRunGetGlyphCount(run);
if (glyphCount == 0) continue;
//根据 run 字形块获取所在 Attributes 基本设置
NSDictionary *attrs = (id)CTRunGetAttributes(run);
//获取不知道什么👻? Attachment 可能是 UIImage|UIView|CALayer
//TODO
YYTextAttachment *attachment = attrs[YYTextAttachmentAttributeName];
if (attachment) {
CGPoint runPosition = CGPointZero;
CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition);
///判断当前 run 是否是 Attachment 然后获取当前 run 的上升、下移和缩进
CGFloat ascent, descent, leading, runWidth;
CGRect runTypoBounds;
runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading);
if (_vertical) {
//交换两者之间的坐标
YYTEXT_SWAP(runPosition.x, runPosition.y);
runPosition.y = _position.y + runPosition.y;
runTypoBounds = CGRectMake(_position.x + runPosition.x - descent, runPosition.y , ascent + descent, runWidth);
} else {
// {x, y, width, height}
//line 中 position | run 中 runWidth 和 ascent/descent
runPosition.x += _position.x;
runPosition.y = _position.y - runPosition.y;
runTypoBounds = CGRectMake(runPosition.x, runPosition.y - ascent, runWidth, ascent + descent);
}
NSRange runRange = YYTextNSRangeFromCFRange(CTRunGetStringRange(run));
//添加附件 | 当前附件 range | 当前 line 的 rect
[attachments addObject:attachment];
[attachmentRanges addObject:[NSValue valueWithRange:runRange]];
[attachmentRects addObject:[NSValue valueWithCGRect:runTypoBounds]];
}
}
//附件复制
_attachments = attachments.count ? attachments : nil;
_attachmentRanges = attachmentRanges.count ? attachmentRanges : nil;
_attachmentRects = attachmentRects.count ? attachmentRects : nil;
...
}
在
YYTextLayout
中调用YYTextLine
实例方式计算当前CTFrameRef
中Each Line
相关参数。
在YYTextLine
中可以看出以Line
为单位计算当前绘制参数,然后通过reloadBounds
来遍历当前行的Run
获取在初始化Attachment
找出当前Line
中的UIView
,CALayer
和UIImage
来设置当前Attachment
绘制区域和范围。
详细信息可以参考代码注释
实现 String
设置 Text
各种格式设置
//YYTextAttribute
//设置当前字体显示类型
NSString *const YYTextBackedStringAttributeName = @"YYTextBackedString";
NSString *const YYTextBindingAttributeName = @"YYTextBinding";
NSString *const YYTextShadowAttributeName = @"YYTextShadow";
NSString *const YYTextInnerShadowAttributeName = @"YYTextInnerShadow";
NSString *const YYTextUnderlineAttributeName = @"YYTextUnderline";
NSString *const YYTextStrikethroughAttributeName = @"YYTextStrikethrough";
//对文字进行描边
NSString *const YYTextBorderAttributeName = @"YYTextBorder";
NSString *const YYTextBackgroundBorderAttributeName = @"YYTextBackgroundBorder";
NSString *const YYTextBlockBorderAttributeName = @"YYTextBlockBorder";
//
NSString *const YYTextAttachmentAttributeName = @"YYTextAttachment";
NSString *const YYTextHighlightAttributeName = @"YYTextHighlight";
NSString *const YYTextGlyphTransformAttributeName = @"YYTextGlyphTransform";
//NSAttributedString+YYText
- (YYTextShadow *)yy_textInnerShadowAtIndex:(NSUInteger)index {
return [self yy_attribute:YYTextInnerShadowAttributeName atIndex:index];
}
- (id)yy_attribute:(NSString *)attributeName atIndex:(NSUInteger)index {
if (!attributeName) return nil;
if (index > self.length || self.length == 0) return nil;
if (self.length > 0 && index == self.length) index--;
return [self attribute:attributeName atIndex:index effectiveRange:NULL];
}
+ (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range {
...
if (visibleRange.length > 0) {
layout.needDrawText = YES;
//TODO TODO
void (^block)(NSDictionary *attrs, NSRange range, BOOL *stop) = ^(NSDictionary *attrs, NSRange range, BOOL *stop) {
if (attrs[YYTextHighlightAttributeName]) layout.containsHighlight = YES;
if (attrs[YYTextBlockBorderAttributeName]) layout.needDrawBlockBorder = YES;
if (attrs[YYTextBackgroundBorderAttributeName]) layout.needDrawBackgroundBorder = YES;
if (attrs[YYTextShadowAttributeName] || attrs[NSShadowAttributeName]) layout.needDrawShadow = YES;
if (attrs[YYTextUnderlineAttributeName]) layout.needDrawUnderline = YES;
if (attrs[YYTextAttachmentAttributeName]) layout.needDrawAttachment = YES;
if (attrs[YYTextInnerShadowAttributeName]) layout.needDrawInnerShadow = YES;
if (attrs[YYTextStrikethroughAttributeName]) layout.needDrawStrikethrough = YES;
if (attrs[YYTextBorderAttributeName]) layout.needDrawBorder = YES;
};
...
}
}
...
}
在初始化
String
设置字体特定的格式,然后根据提供的Key
值类型来保存在当前String
转化为NSAttributedString
类型的格式中。设置当前Line
所属的YYTextLayout
标记当前状态,在绘制时来执行在改Line
的Run
执行绘制(后面在绘制中会讲述)。
实现具体绘制?
//YYLabel
- (YYTextAsyncLayerDisplayTask *)newAsyncDisplayTask {
...
task.display = ^(CGContextRef context, CGSize size, BOOL (^isCancelled)(void)) {
...
//Layout 绘制当前 UIView 界面
[drawLayout drawInContext:nil size:size point:point view:view layer:layer debug:nil cancel:NULL];
//跟新当前的
///当前 Label 的附件 此附件归属于 Layout 中 attachments 属性
for (YYTextAttachment *a in drawLayout.attachments) {
if ([a.content isKindOfClass:[UIView class]]) [attachmentViews addObject:a.content];
else if ([a.content isKindOfClass:[CALayer class]]) [attachmentLayers addObject:a.content];
}
...
}
}
//YYTextLayout
//Line 3469
//TODO 绘制方法调用
- (void)drawInContext:(CGContextRef)context
size:(CGSize)size
point:(CGPoint)point
view:(UIView *)view
layer:(CALayer *)layer
debug:(YYTextDebugOption *)debug
cancel:(BOOL (^)(void))cancel{
@autoreleasepool {
...
}
}
实现富文本上点击事件
//YYLabel
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
...
if (_highlight || _textTapAction || _textLongPressAction) {
_touchBeganPoint = point;
_state.trackingTouch = YES;
_state.swallowTouch = YES;
_state.touchMoved = NO;
//添加定时器
[self _startLongPressTimer];
if (_highlight) [self _showHighlightAnimated:NO];
}
...
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
...
if (_state.trackingTouch) {
if (!_state.touchMoved) {
...
if (_state.touchMoved) {
[self _endLongPressTimer];
}
}
...
}
...
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
...
if (_state.trackingTouch) {
[self _endLongPressTimer];
if (!_state.touchMoved && _textTapAction) {
...
_textTapAction(self, _innerText, range, rect);
}
...
}
...
}
参考资料:
TextKit Best Practices
Introducing Text Kit
Advanced Text Layouts and Effects with Text Kit
About Core Text
About the Cocoa Text System
About Text Handling in iOS
The Layout Manager
Advanced Text Processing
Graver
Emoji Unicode Tables
新大陆:AsyncDisplayKit
Optimising Autolayout
iOS 保持界面流畅的技巧
WebView性能、体验分析与优化
Text Kit 学习笔记
初识 TextKit