從零到一擼個(gè)YYLabel

前言

在學(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

正文

效果圖
Example.gif
設(shè)計(jì)思路
設(shè)計(jì)思路.png

以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
      _frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)_text);
      _frame = CTFramesetterCreateFrame(_frameSetter, range, path.CGPath, NULL);
    
    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)容犹芹。
    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ú)繪制即可
Unicode字符列表.png
  • 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暫不支持NSLineBreakByTruncatingHeadNSLineBreakByTruncatingMiddle涌献,如果賦值為這兩種屬性,會自動轉(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了

Demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末偿枕,一起剝皮案震驚了整個(gè)濱河市璧瞬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌渐夸,老刑警劉巖嗤锉,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異墓塌,居然都是意外死亡瘟忱,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進(jìn)店門苫幢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來酷誓,“玉大人,你說我怎么就攤上這事态坦⊙问” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵伞梯,是天一觀的道長玫氢。 經(jīng)常有香客問我,道長谜诫,這世上最難降的妖魔是什么漾峡? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮喻旷,結(jié)果婚禮上生逸,老公的妹妹穿的比我還像新娘。我一直安慰自己且预,他們只是感情好槽袄,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锋谐,像睡著了一般遍尺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上涮拗,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天乾戏,我揣著相機(jī)與錄音,去河邊找鬼三热。 笑死鼓择,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的就漾。 我是一名探鬼主播呐能,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼从藤!你這毒婦竟也來了催跪?” 一聲冷哼從身側(cè)響起锁蠕,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎懊蒸,沒想到半個(gè)月后荣倾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡骑丸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年舌仍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片通危。...
    茶點(diǎn)故事閱讀 40,424評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡铸豁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出菊碟,到底是詐尸還是另有隱情节芥,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布逆害,位于F島的核電站头镊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏魄幕。R本人自食惡果不足惜相艇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望纯陨。 院中可真熱鬧坛芽,春花似錦、人聲如沸翼抠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽机久。三九已至臭墨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間膘盖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工尤误, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留侠畔,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓损晤,卻偏偏與公主長得像软棺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子尤勋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理喘落,服務(wù)發(fā)現(xiàn)茵宪,斷路器,智...
    卡卡羅2017閱讀 134,693評論 18 139
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫瘦棋、插件稀火、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,117評論 4 61
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,262評論 25 707
  • “我跟你說,我做了一個(gè)好奇怪的夢赌朋。我夢見自己在宿舍睡覺凰狞,然后做夢去圖書館找依然,但是一進(jìn)圖書館就趴著睡著了沛慢,接著夢...
    盲心橋閱讀 451評論 0 0
  • 作者:夏汐蕊?想看其他作品請點(diǎn)擊這里?簡書連載風(fēng)云錄前情回顧《我的愛只屬于你(16)》 【第十七章】走進(jìn)唐風(fēng)齋赡若,一...
    夏汐蕊閱讀 415評論 0 8