YYText 簡單介紹
YYText 是YYKit中的一個(gè)富文本顯示,編輯組件虾宇,擁有YYLabel凤类,YYTextView 兩個(gè)控件扛稽。其中YYLabel類似于UILabel柴灯,但功能更為強(qiáng)大物延,支持異步文本渲染宣旱,更豐富的效果顯示,支持UIImage叛薯,UIView, CALayer 文本附件浑吟,自定義強(qiáng)調(diào)文本范圍,支持垂直文本顯示等等耗溜。YYTextView 類似UITextView组力,除了兼容UITextView API,擴(kuò)展了更多的CoreText 效果屬性抖拴,支持高亮鏈接忿项,支持自定義內(nèi)部文本路徑形狀,支持圖片拷貝城舞,粘貼等等轩触。下面是YYText 與 TextKit 的比較圖:
YYLabel
YYLabel
的實(shí)現(xiàn),是基于CoreText
框架 在 Context
上進(jìn)行繪制家夺,通過設(shè)置NSMutableAttributedString
實(shí)現(xiàn)文本各種效果屬性的展現(xiàn)脱柱。下面是YYLabel 主要相關(guān)的部分:
- YYAsyncLayer: YYLabel的異步渲染,通過YYAsyncLayerDisplayTask 回調(diào)渲染
- YYTextLayout: YYLabel的布局管理類拉馋,也負(fù)責(zé)繪制
- YYTextContainer: YYLabel的布局類
- NSAttributedString+YYText: YYLabel 所有效果屬性設(shè)置
YYAsyncLayer 的異步實(shí)現(xiàn)
YYAsyncLayer
是 CALayer的子類榨为,通過設(shè)置 YYLabel 類方法 layerClass
返回自定義的 YYAsyncLayer
,重寫了父類的 setNeedsDisplay
, display
實(shí)現(xiàn) contents
自定義刷新煌茴。YYAsyncLayerDelegate
返回新的刷新任務(wù) newAsyncDisplayTask
用于更新過程回調(diào)随闺,返回到 YYLabel 進(jìn)行文本渲染。其中 YYSentinel
是一個(gè)線程安全的原子遞增計(jì)數(shù)器蔓腐,用于判斷更新是否取消矩乐。
YYTextLayout
YYLabel
實(shí)現(xiàn)了 YYAsyncLayerDelegate
代理方法 newAsyncDisplayTask
,回調(diào)處理3種文本渲染狀態(tài)willDisplay ,display散罕,didDisplay 分歇。在渲染之前,移除不需要的文本附件欧漱,渲染完成后职抡,添加需要的文本附件。渲染時(shí)误甚,首先獲取YYTextLayout
, 一般包含了 YYTextContainer
和 NSAttributedString
兩部分缚甩, 分別負(fù)責(zé)文本展示的形狀和內(nèi)容。不管是渲染時(shí)和渲染完成后窑邦,最后都需要調(diào)用 YYTextLayout
的
- (void) drawInContext:(CGContextRef)context
size:(CGSize)size
point:(CGPoint)point
view:(UIView *)view
layer:(CALayer *)layer
debug:(YYTextDebugOption *)debug
cancel:(BOOL (^)(void))cancel{
其中 context
是圖形上下文蹄胰,文本的繪制在它上面進(jìn)行,size
是 context 的大小奕翔,point
是繪制的起始點(diǎn),view
,layer
是添加文本附件的視圖(層)裕寨,debug
調(diào)試選項(xiàng),cancel
取消繪制派继,根據(jù)傳入的文本是否需要繪制YYText自定義屬性效果依次進(jìn)行相應(yīng)的繪制宾袜。有以下這些屬性:
///< 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
中 調(diào)用 addAttribute:(NSString *)name value:(id)value range:(NSRange)range
添加了此文本屬性驾窟∏烀ǎ可以在 YYTextAttribute
查看所有屬性信息
最后在 YYTextLayout
的初始化方法中獲取,代碼如下:
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];
根據(jù)YYTextContainer
和 NSAttributedString
生成 YYTextLayout
绅络,接下來就是渲染了月培, 具體的渲染由 CoreText
來實(shí)現(xiàn)。
CoreText
CoreText
是iOS/OSX里的文字渲染引擎恩急,在iOS/OSX上看到的所有文字在底層都是由CoreText去渲染杉畜。
一個(gè)
NSAttributeString
通過CoreText的CTFramesetterCreateWithAttributedString
生成CTFramesetter
,它是創(chuàng)建 CTFrame
的工廠,為 CTFramesetter
提供一個(gè) CGPath
衷恭,它就會(huì)通過它持有的 CTTypesetter
生成 CTFrame
此叠,CTFrame
里面包含了 CTLine
CTLine
中包含了此行所有的 CTRun
,然后就可以繪制到畫布上随珠。CTFrame
,CTLine
,CTRun
都提供了渲染接口灭袁,但前兩者是封裝,最后實(shí)際都是調(diào)用到 CTRun
的渲染接口去繪制窗看。
在YYTextlayout 中的代碼體現(xiàn)
// 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
根據(jù) YYTextContainer
生成
// 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
有下面兩個(gè)屬性,可以用來自定義path
/// 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;
復(fù)雜的就是每行的frame的計(jì)算显沈, calculate line frame 部分软瞎,看的有點(diǎn)暈~~~
需要進(jìn)行坐標(biāo)系的轉(zhuǎn)化,YYTextLine
是對(duì) CTLine
的進(jìn)一步封裝。
最終文字的繪制都會(huì)調(diào)用
YYTextDrawText(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, BOOL (^cancel)(void))
//坐標(biāo)系變化
CGContextTranslateCTM(context, point.x, point.y);
CGContextTranslateCTM(context, 0, size.height);
CGContextScaleCTM(context, 1, -1);
CGContextSaveGState(context); {
// 所有的渲染有關(guān)代碼
} CGContextRestoreGState(context);
最后都需要調(diào)用 YYTextDrawRun
進(jìn)行繪制 CTRunDraw(run, context, CFRangeMake(0, 0));
YYTextView
其繪制原理同 YYLabel铜涉,其中 YYTextContainerView 是文本顯示的視圖,其layout
屬性用來繪制文本遂唧。