YYText 简单介绍
YYText 是YYKit中的一个富文本显示,编辑组件,拥有YYLabel,YYTextView 两个控件。其中YYLabel类似于UILabel,但功能更为强大,支持异步文本渲染,更丰富的效果显示,支持UIImage,UIView, CALayer 文本附件,自定义强调文本范围,支持垂直文本显示等等。YYTextView 类似UITextView,除了兼容UITextView API,扩展了更多的CoreText 效果属性,支持高亮链接,支持自定义内部文本路径形状,支持图片拷贝,粘贴等等。
下面是YYText 与 TextKit 的比较图:
YYLabel
YYLabel
的实现,是基于CoreText
框架 在 Context
上进行绘制,通过设置NSMutableAttributedString
实现文本各种效果属性的展现。下面是YYLabel 主要相关的部分:
- YYAsyncLayer: YYLabel的异步渲染,通过YYAsyncLayerDisplayTask 回调渲染
- YYTextLayout: YYLabel的布局管理类,也负责绘制
- YYTextContainer: YYLabel的布局类
- NSAttributedString+YYText: YYLabel 所有效果属性设置
YYAsyncLayer 的异步实现
YYAsyncLayer
是 CALayer的子类,通过设置 YYLabel 类方法 layerClass
返回自定义的 YYAsyncLayer
,重写了父类的 setNeedsDisplay
, display
实现 contents
自定义刷新。YYAsyncLayerDelegate
返回新的刷新任务 newAsyncDisplayTask
用于更新过程回调,返回到 YYLabel 进行文本渲染。其中 YYSentinel
是一个线程安全的原子递增计数器,用于判断更新是否取消。
YYTextLayout
YYLabel
实现了 YYAsyncLayerDelegate
代理方法 newAsyncDisplayTask
,回调处理3种文本渲染状态willDisplay ,display,didDisplay 。在渲染之前,移除不需要的文本附件,渲染完成后,添加需要的文本附件。渲染时,首先获取YYTextLayout
, 一般包含了 YYTextContainer
和 NSAttributedString
两部分, 分别负责文本展示的形状和内容。不管是渲染时和渲染完成后,最后都需要调用 YYTextLayout
的
1 2 3 4 5 6 7
| - (void) drawInContext:(CGContextRef)context size:(CGSize)size point:(CGPoint)point view:(UIView *)view layer:(CALayer *)layer debug:(YYTextDebugOption *)debug cancel:(BOOL (^)(void))cancel{
|
其中 context
是图形上下文,文本的绘制在它上面进行,size
是 context 的大小,point
是绘制的起始点,view
,layer
是添加文本附件的视图(层),debug
调试选项,cancel
取消绘制,根据传入的文本是否需要绘制YYText自定义属性效果依次进行相应的绘制。有以下这些属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ///< Has highlight attribute @property (nonatomic, readonly) BOOL containsHighlight; ///< Has block border attribute @property (nonatomic, readonly) BOOL needDrawBlockBorder; ///< Has background border attribute @property (nonatomic, readonly) BOOL needDrawBackgroundBorder; ///< Has shadow attribute @property (nonatomic, readonly) BOOL needDrawShadow; ///< Has underline attribute @property (nonatomic, readonly) BOOL needDrawUnderline; ///< Has visible text @property (nonatomic, readonly) BOOL needDrawText; ///< Has attachment attribute @property (nonatomic, readonly) BOOL needDrawAttachment; ///< Has inner shadow attribute @property (nonatomic, readonly) BOOL needDrawInnerShadow; ///< Has strickthrough attribute @property (nonatomic, readonly) BOOL needDrawStrikethrough; ///< Has border attribute @property (nonatomic, readonly) BOOL needDrawBorder;
|
以其中的 needDrawShadow
为例,在Demo中YYTextAttributeExample.m
, 为文本 Shadow
自定义了 textShadow
, 在NSAttributedString+YYText.m
中 调用 addAttribute:(NSString *)name value:(id)value range:(NSRange)range
添加了此文本属性。可以在 YYTextAttribute
查看所有属性信息
最后在 YYTextLayout
的初始化方法中获取,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| layout.needDrawText = YES; 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; }; [layout.text enumerateAttributesInRange:visibleRange options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:block];
|
根据YYTextContainer
和 NSAttributedString
生成 YYTextLayout
,接下来就是渲染了, 具体的渲染由 CoreText
来实现。
CoreText
CoreText
是iOS/OSX里的文字渲染引擎,在iOS/OSX上看到的所有文字在底层都是由CoreText去渲染。
一个 NSAttributeString
通过CoreText的CTFramesetterCreateWithAttributedString
生成CTFramesetter
,它是创建 CTFrame
的工厂,为 CTFramesetter
提供一个 CGPath
,它就会通过它持有的 CTTypesetter
生成 CTFrame
,CTFrame
里面包含了 CTLine
CTLine
中包含了此行所有的 CTRun
,然后就可以绘制到画布上。CTFrame
,CTLine
,CTRun
都提供了渲染接口,但前两者是封装,最后实际都是调用到 CTRun
的渲染接口去绘制。
在YYTextlayout 中的代码体现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| // create CoreText objects ctSetter = CTFramesetterCreateWithAttributedString((CFTypeRef)text); if (!ctSetter) goto fail; ctFrame = CTFramesetterCreateFrame(ctSetter, YYCFRangeFromNSRange(range), cgPath, (CFTypeRef)frameAttrs); if (!ctFrame) goto fail; lines = [NSMutableArray new]; ctLines = CTFrameGetLines(ctFrame); lineCount = CFArrayGetCount(ctLines); if (lineCount > 0) { lineOrigins = malloc(lineCount * sizeof(CGPoint)); if (lineOrigins == NULL) goto fail; CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, lineCount), lineOrigins); } CGRect textBoundingRect = CGRectZero; CGSize textBoundingSize = CGSizeZero; NSInteger rowIdx = -1; NSUInteger rowCount = 0; CGRect lastRect = CGRectMake(0, -FLT_MAX, 0, 0); CGPoint lastPosition = CGPointMake(0, -FLT_MAX); if (isVerticalForm) { lastRect = CGRectMake(FLT_MAX, 0, 0, 0); lastPosition = CGPointMake(FLT_MAX, 0); }
|
在这之前,需要生成 CGPath
根据 YYTextContainer
生成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| // set cgPath and cgPathBox if (container.path == nil && container.exclusionPaths.count == 0) { if (container.size.width <= 0 || container.size.height <= 0) goto fail; CGRect rect = (CGRect) {CGPointZero, container.size }; if (needFixLayoutSizeBug) { constraintSizeIsExtended = YES; constraintRectBeforeExtended = UIEdgeInsetsInsetRect(rect, container.insets); constraintRectBeforeExtended = CGRectStandardize(constraintRectBeforeExtended); if (container.isVerticalForm) { rect.size.width = YYTextContainerMaxSize.width; } else { rect.size.height = YYTextContainerMaxSize.height; } } rect = UIEdgeInsetsInsetRect(rect, container.insets); rect = CGRectStandardize(rect); cgPathBox = rect; rect = CGRectApplyAffineTransform(rect, CGAffineTransformMakeScale(1, -1)); cgPath = CGPathCreateWithRect(rect, NULL); // let CGPathIsRect() returns true } else if (container.path && CGPathIsRect(container.path.CGPath, &cgPathBox) && container.exclusionPaths.count == 0) { CGRect rect = CGRectApplyAffineTransform(cgPathBox, CGAffineTransformMakeScale(1, -1)); cgPath = CGPathCreateWithRect(rect, NULL); // let CGPathIsRect() returns true } else { rowMaySeparated = YES; CGMutablePathRef path = NULL; if (container.path) { path = CGPathCreateMutableCopy(container.path.CGPath); } else { CGRect rect = (CGRect) {CGPointZero, container.size }; rect = UIEdgeInsetsInsetRect(rect, container.insets); CGPathRef rectPath = CGPathCreateWithRect(rect, NULL); if (rectPath) { path = CGPathCreateMutableCopy(rectPath); CGPathRelease(rectPath); } } if (path) { [layout.container.exclusionPaths enumerateObjectsUsingBlock: ^(UIBezierPath *onePath, NSUInteger idx, BOOL *stop) { CGPathAddPath(path, NULL, onePath.CGPath); }]; cgPathBox = CGPathGetPathBoundingBox(path); CGAffineTransform trans = CGAffineTransformMakeScale(1, -1); CGMutablePathRef transPath = CGPathCreateMutableCopyByTransformingPath(path, &trans); CGPathRelease(path); path = transPath; } cgPath = path; }
|
YYTextContainer
有下面两个属性,可以用来自定义path
1 2 3 4 5
| /// Custom constrained path. Set this property to ignore `size` and `insets`. Default is nil. @property (nullable, copy) UIBezierPath *path; /// An array of `UIBezierPath` for path exclusion. Default is nil. @property (nullable, copy) NSArray<UIBezierPath *> *exclusionPaths;
|
复杂的就是每行的frame的计算, calculate line frame 部分,看的有点晕~~~
需要进行坐标系的转化,YYTextLine
是对 CTLine
的进一步封装。
最终文字的绘制都会调用
1
| YYTextDrawText(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, BOOL (^cancel)(void))
|
1 2 3 4
| //坐标系变化 CGContextTranslateCTM(context, point.x, point.y); CGContextTranslateCTM(context, 0, size.height); CGContextScaleCTM(context, 1, -1);
|
1 2 3
| CGContextSaveGState(context); { // 所有的渲染有关代码 } CGContextRestoreGState(context);
|
最后都需要调用 YYTextDrawRun
进行绘制 CTRunDraw(run, context, CFRangeMake(0, 0));
YYTextView
其绘制原理同 YYLabel,其中 YYTextContainerView 是文本显示的视图,其layout
属性用来绘制文本。