從零到一擼個YYLabel

前言

在學習YYText過程中,分析完YYLabel原理后一時手癢,自己擼了個JKRichLabel,寫文記錄,也算功德圓滿。相較于YYLabel,JKRichLabel更適合初學者通過閱讀源碼學習技術,畢竟大神的東西不好懂,有精力的童鞋強烈建議閱讀YYLabel源碼(雖然JKRichLabel更好懂,但是功力離YY大神差太遠)

為保證界面流暢,各種技術層出不窮。JKRichLabel繼承自UIView,基本復原了UILabel的功能特性,在此基礎上采用壓縮圖層,異步繪制,可以更好的解決卡頓問題,并且內部通過core text繪制,支持圖文混排。

JKRichLabel還很脆弱,歡迎感興趣的童鞋一起完善ta

正文

效果圖
Example.gif
設計思路
設計思路.png

以JKRichLabel為載體,JKAsyncLayer為核心,在JKRichLabelLayout中通過core text進行繪制。JKRichLabelLine是CTLine的拓展,包含一行要繪制的信息。JKTextInfo包含屬性文本的基本信息,類似于CTRun。JKTextInfoContainer是JKTextInfo的容器,并且JKTextInfoContainer可以合并JKTextInfoContainer。同時,JKTextInfoContainer負責判斷是否可以響應用戶交互

@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對部分邏輯進行調整,其余邏輯基本相同。JKAsyncLayer是整個流程中異步繪制的核心。

JKAsyncLayer繼承自CALayer,UIView內部持有CALayer,JKRichLabel繼承自UIView。因此,只要將JKRichLabel內部的layer替換成JKAsyncLayer就可以完成異步繪制。

+ (Class)layerClass {
    return [JKAsyncLayer class];
}

JKAsyncLayer繪制核心思想:在異步線程中獲取context上下文,繪制背景色,生成image context,跳回主線程將image賦值給layer.contents。異步線程確保界面的流暢性,生成圖片后賦值給contents可以壓縮圖層,同樣能夠提高界面的流暢性

self.contents = (__bridge id _Nullable)(img.CGImage);
JKRichLabel

JKRichLabel內部含有text與attributedText屬性,分別支持普通文本與屬性文本,不管是哪種文本,內部都轉成屬性文本_innerText,并通過_innerText進行繪制

- (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是繪制具體內容的核心,通過core text可以完成attachment的繪制

  • 簡單說下Core Text:
    Core Text是Apple的文字渲染引擎,坐標系為自然坐標系,即左下角為坐標原點,而iOS坐標原點在左上角。所以,在iOS上用Core Text繪制文字時,需要轉換坐標系
    • CTFrameSetter、CTFrame、CTLine與CTRun
      _frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)_text);
      _frame = CTFramesetterCreateFrame(_frameSetter, range, path.CGPath, NULL);
    
    CTFrameSetter通過CFAttributedStringRef初始化,CFAttributedStringRef即要繪制內容。通過CTFrameSetter提供繪制內容,結合繪制區(qū)域生成CTFrame。CTFrame包含一個或多個CTLine,CTLine包含一個或多個CTRun。CTLine為繪制區(qū)域中一行的內容,CTRun為一行中相鄰相同屬性的內容。
    CTFrame、CTLine與CTRun都提供繪制接口,不管調用哪個接口,最終都是通過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轉換成字符串

  • attachment繪制思路
    查詢Unicode字符列表可知:U+FFFC 取代無法顯示字符的“OBJ” 。因此,可以用\uFFFC占位,所占位置大小即為attachment大小,在繪制過程中通過core text接口繪制文字,取出attachment單獨繪制即可
Unicode字符列表.png
  • attachment繪制流程
    core text雖然無法直接繪制attachment,但提供了另一個接口CTRunDelegateRef。CTRunDelegateRef通過CTRunDelegateCallbacks創(chuàng)建,CTRunDelegateCallbacks可提供一系列函數(shù)用于返回CTRunRef的ascent、descent、width,通過ascent、descent、width即可確定當前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暫不支持NSLineBreakByTruncatingHeadNSLineBreakByTruncatingMiddle,如果賦值為這兩種屬性,會自動轉換為NSLineBreakByTruncatingTail

  • 原因
    如果是純文本支持這兩種屬性很簡單,由于label中可能包含attachment,如果numberOfLines為多行,支持這兩種屬性需要獲取CTFrame的最后一行并且attachment比較惡心(如,attachment剛好在添加省略號的位置,attachment的size又比較大,將attachment替換為省略號后還需動態(tài)改變行高,吧啦吧啦諸如此類),然后通過CTLineCreateTruncatedLine創(chuàng)建truncatedLine,受numberOfLines所限,繪制過程中可能不需要繪制到最后一行。當然,這些都不是事兒,加幾句條件判斷再改動一下邏輯還是可以實現(xiàn)的。由于這兩種屬性使用較少,比較雞肋,so...偷個懶
    另外,由于不支持這兩種屬性,truncatedLine沒通過CTLineCreateTruncatedLine生成,而是直接在末尾添加省略號生成新的CTLine
Long Text說明

效果圖中有Long Text的例子,label外套scrollview,將scrollview的contentSize設置為label的size,label的size通過sizeToFit自動計算。如果文字足夠長,這種方案就over了

Demo

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評論 19 139
  • 發(fā)現(xiàn) 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 15,073評論 4 61
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,781評論 25 709
  • “我跟你說,我做了一個好奇怪的夢。我夢見自己在宿舍睡覺,然后做夢去圖書館找依然,但是一進圖書館就趴著睡著了,接著夢...
    盲心橋閱讀 529評論 0 0
  • 作者:夏汐蕊?想看其他作品請點擊這里?簡書連載風云錄前情回顧《我的愛只屬于你(16)》 【第十七章】走進唐風齋,一...
    夏汐蕊閱讀 477評論 0 8

友情鏈接更多精彩內容