前言
在學(xué)習(xí)YYText過程中拭抬,分析完YYLabel原理后一時(shí)手癢,自己擼了個(gè)JKRichLabel侵蒙,寫文記錄造虎,也算功德圓滿。相較于YYLabel蘑志,JKRichLabel更適合初學(xué)者通過閱讀源碼學(xué)習(xí)技術(shù)累奈,畢竟大神的東西不好懂贬派,有精力的童鞋強(qiáng)烈建議閱讀YYLabel源碼(雖然JKRichLabel更好懂急但,但是功力離YY大神差太遠(yuǎn))
為保證界面流暢,各種技術(shù)層出不窮搞乏。JKRichLabel繼承自UIView波桩,基本復(fù)原了UILabel的功能特性,在此基礎(chǔ)上采用壓縮圖層请敦,異步繪制镐躲,可以更好的解決卡頓問題,并且內(nèi)部通過core text繪制侍筛,支持圖文混排萤皂。
JKRichLabel還很脆弱,歡迎感興趣的童鞋一起完善ta
正文
效果圖
設(shè)計(jì)思路
以JKRichLabel為載體匣椰,JKAsyncLayer為核心裆熙,在JKRichLabelLayout中通過core text進(jìn)行繪制。JKRichLabelLine是CTLine的拓展禽笑,包含一行要繪制的信息入录。JKTextInfo包含屬性文本的基本信息,類似于CTRun佳镜。JKTextInfoContainer是JKTextInfo的容器僚稿,并且JKTextInfoContainer可以合并JKTextInfoContainer。同時(shí)蟀伸,JKTextInfoContainer負(fù)責(zé)判斷是否可以響應(yīng)用戶交互
@interface JKTextInfo : NSObject
@property (nonatomic, strong) NSAttributedString *text;
@property (nonatomic, strong) NSValue *rectValue;
@property (nonatomic, strong) NSValue *rangeValue;
@property (nullable, nonatomic, strong) JKTextAttachment *attachment;
@property (nullable, nonatomic, copy) JKTextBlock singleTap;
@property (nullable, nonatomic, copy) JKTextBlock longPress;
@property (nullable, nonatomic, strong) JKTextHighlight *highlight;
@property (nullable, nonatomic, strong) JKTextBorder *border;
@end
@interface JKTextInfoContainer : NSObject
@property (nonatomic, strong, readonly) NSArray<NSAttributedString *> *texts;
@property (nonatomic, strong, readonly) NSArray<NSValue *> *rects;
@property (nonatomic, strong, readonly) NSArray<NSValue *> *ranges;
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, JKTextAttachment *> *attachmentDict;
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, JKTextBlock> *singleTapDict;
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, JKTextBlock> *longPressDict;
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, JKTextHighlight *> *highlightDict;
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, JKTextBorder *> *borderDict;
@property (nullable, nonatomic, strong, readonly) JKTextInfo *responseInfo;
+ (instancetype)infoContainer;
- (void)addObjectFromInfo:(JKTextInfo *)info;
- (void)addObjectFromInfoContainer:(JKTextInfoContainer *)infoContainer;
- (BOOL)canResponseUserActionAtPoint:(CGPoint)point;
@end
JKAsyncLayer
JKAsyncLayer相較于YYTextAsyncLayer對部分邏輯進(jìn)行調(diào)整蚀同,其余邏輯基本相同缅刽。JKAsyncLayer是整個(gè)流程中異步繪制的核心。
JKAsyncLayer繼承自CALayer蠢络,UIView內(nèi)部持有CALayer拷恨,JKRichLabel繼承自UIView。因此谢肾,只要將JKRichLabel內(nèi)部的layer替換成JKAsyncLayer就可以完成異步繪制腕侄。
+ (Class)layerClass {
return [JKAsyncLayer class];
}
JKAsyncLayer繪制核心思想:在異步線程中獲取context上下文,繪制背景色芦疏,生成image context冕杠,跳回主線程將image賦值給layer.contents。異步線程確保界面的流暢性酸茴,生成圖片后賦值給contents可以壓縮圖層分预,同樣能夠提高界面的流暢性
self.contents = (__bridge id _Nullable)(img.CGImage);
JKRichLabel
JKRichLabel內(nèi)部含有text與attributedText屬性,分別支持普通文本與屬性文本薪捍,不管是哪種文本笼痹,內(nèi)部都轉(zhuǎn)成屬性文本_innerText,并通過_innerText進(jìn)行繪制
- (void)setText:(NSString *)text {
if (_text == text || [_text isEqualToString:text]) return;
_text = text.copy;
[_innerText replaceCharactersInRange:NSMakeRange(0, _innerText.length) withString:text];
[self _update];
}
- (void)setAttributedText:(NSAttributedString *)attributedText {
if (_attributedText == attributedText || [_attributedText isEqualToAttributedString:attributedText]) return;
_attributedText = attributedText;
_innerText = attributedText.mutableCopy;
[self _update];
}
JKRichLabelLayout
JKRichLabelLayout是繪制具體內(nèi)容的核心酪穿,通過core text可以完成attachment的繪制
- 簡單說下Core Text:
Core Text是Apple的文字渲染引擎凳干,坐標(biāo)系為自然坐標(biāo)系,即左下角為坐標(biāo)原點(diǎn)被济,而iOS坐標(biāo)原點(diǎn)在左上角救赐。所以,在iOS上用Core Text繪制文字時(shí)只磷,需要轉(zhuǎn)換坐標(biāo)系- CTFrameSetter经磅、CTFrame、CTLine與CTRun
CTFrameSetter通過CFAttributedStringRef初始化钮追,CFAttributedStringRef即要繪制內(nèi)容预厌。通過CTFrameSetter提供繪制內(nèi)容,結(jié)合繪制區(qū)域生成CTFrame元媚。CTFrame包含一個(gè)或多個(gè)CTLine轧叽,CTLine包含一個(gè)或多個(gè)CTRun。CTLine為繪制區(qū)域中一行的內(nèi)容惠毁,CTRun為一行中相鄰相同屬性的內(nèi)容犹芹。_frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)_text); _frame = CTFramesetterCreateFrame(_frameSetter, range, path.CGPath, NULL);
CTFrame、CTLine與CTRun都提供繪制接口鞠绰,不管調(diào)用哪個(gè)接口腰埂,最終都是通過CTRun接口繪制
CTFrameDraw(<#CTFrameRef _Nonnull frame#>, <#CGContextRef _Nonnull context#>)
CTLineDraw(<#CTLineRef _Nonnull line#>, <#CGContextRef _Nonnull context#>)
CTRunDraw(<#CTRunRef _Nonnull run#>, <#CGContextRef _Nonnull context#>, <#CFRange range#>)
可見,繪制圖文混排必然要將attachment添加到CFAttributedStringRef中蜈膨,然而并沒有接口可以將attachment轉(zhuǎn)換成字符串
- attachment繪制思路
查詢Unicode字符列表可知:U+FFFC 取代無法顯示字符的“OBJ” 屿笼。因此牺荠,可以用\uFFFC
占位,所占位置大小即為attachment大小驴一,在繪制過程中通過core text接口繪制文字休雌,取出attachment單獨(dú)繪制即可
- attachment繪制流程
core text雖然無法直接繪制attachment,但提供了另一個(gè)接口CTRunDelegateRef肝断。CTRunDelegateRef通過CTRunDelegateCallbacks創(chuàng)建杈曲,CTRunDelegateCallbacks可提供一系列函數(shù)用于返回CTRunRef的ascent、descent胸懈、width担扑,通過ascent、descent趣钱、width即可確定當(dāng)前CTRunRef的Size
- (CTRunDelegateRef)runDelegate {
CTRunDelegateCallbacks callbacks;
callbacks.version = kCTRunDelegateCurrentVersion;
callbacks.dealloc = JKTextRunDelegateDeallocCallback;
callbacks.getAscent = JKTextRunDelegateGetAscentCallback;
callbacks.getDescent = JKTextRunDelegateGetDescentCallback;
callbacks.getWidth = JKTextRunDelegateGetWidthCallback;
return CTRunDelegateCreate(&callbacks, (__bridge void *)self);
}
省略號說明
JKRichLabel的lineBreakMode暫不支持NSLineBreakByTruncatingHead
和NSLineBreakByTruncatingMiddle
涌献,如果賦值為這兩種屬性,會自動轉(zhuǎn)換為NSLineBreakByTruncatingTail
- 原因
如果是純文本支持這兩種屬性很簡單首有,由于label中可能包含attachment燕垃,如果numberOfLines為多行,支持這兩種屬性需要獲取CTFrame的最后一行并且attachment比較惡心(如井联,attachment剛好在添加省略號的位置卜壕,attachment的size又比較大,將attachment替換為省略號后還需動態(tài)改變行高低矮,吧啦吧啦諸如此類)印叁,然后通過CTLineCreateTruncatedLine創(chuàng)建truncatedLine,受numberOfLines所限军掂,繪制過程中可能不需要繪制到最后一行。當(dāng)然昨悼,這些都不是事兒蝗锥,加幾句條件判斷再改動一下邏輯還是可以實(shí)現(xiàn)的。由于這兩種屬性使用較少率触,比較雞肋终议,so...偷個(gè)懶
另外,由于不支持這兩種屬性葱蝗,truncatedLine沒通過CTLineCreateTruncatedLine生成穴张,而是直接在末尾添加省略號生成新的CTLine
Long Text說明
效果圖中有Long Text的例子,label外套scrollview两曼,將scrollview的contentSize設(shè)置為label的size皂甘,label的size通過sizeToFit自動計(jì)算。如果文字足夠長悼凑,這種方案就over了