YYText源碼分析

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 的比較圖:

YYText VS 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, 一般包含了 YYTextContainerNSAttributedString 兩部分缚甩, 分別負(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ù)YYTextContainerNSAttributedString生成 YYTextLayout绅络,接下來就是渲染了月培, 具體的渲染由 CoreText來實(shí)現(xiàn)。

CoreText

CoreText是iOS/OSX里的文字渲染引擎恩急,在iOS/OSX上看到的所有文字在底層都是由CoreText去渲染杉畜。

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 屬性用來繪制文本遂唧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末芙代,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子盖彭,更是在濱河造成了極大的恐慌纹烹,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件召边,死亡現(xiàn)場離奇詭異铺呵,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)隧熙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門片挂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人贞盯,你說我怎么就攤上這事音念。” “怎么了躏敢?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵闷愤,是天一觀的道長。 經(jīng)常有香客問我件余,道長讥脐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任啼器,我火速辦了婚禮旬渠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘端壳。我一直安慰自己坟漱,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布更哄。 她就那樣靜靜地躺著芋齿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪成翩。 梳的紋絲不亂的頭發(fā)上觅捆,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音麻敌,去河邊找鬼栅炒。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的赢赊。 我是一名探鬼主播乙漓,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼释移!你這毒婦竟也來了叭披?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤玩讳,失蹤者是張志新(化名)和其女友劉穎涩蜘,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體熏纯,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡同诫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了樟澜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片误窖。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖秩贰,靈堂內(nèi)的尸體忽然破棺而出贩猎,到底是詐尸還是另有隱情,我是刑警寧澤萍膛,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布吭服,位于F島的核電站,受9級(jí)特大地震影響蝗罗,放射性物質(zhì)發(fā)生泄漏艇棕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一串塑、第九天 我趴在偏房一處隱蔽的房頂上張望沼琉。 院中可真熱鬧,春花似錦桩匪、人聲如沸打瘪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽闺骚。三九已至,卻和暖如春妆档,著一層夾襖步出監(jiān)牢的瞬間僻爽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國打工贾惦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留胸梆,地道東北人敦捧。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像碰镜,于是被迫代替她去往敵國和親兢卵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

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

  • 系列文章: CoreText實(shí)現(xiàn)圖文混排 CoreText實(shí)現(xiàn)圖文混排之點(diǎn)擊事件 CoreText實(shí)現(xiàn)圖文混排之文...
    老司機(jī)Wicky閱讀 40,167評(píng)論 221 432
  • CoreText是一個(gè)進(jìn)階的比較底層的布局文本和處理字體的技術(shù)绪颖,CoreText API在OS X v10.5 和...
    smalldu閱讀 13,446評(píng)論 18 129
  • Apple 的相關(guān)博客:Text Programming Guide for iOSCore Text Progr...
    Laughingg閱讀 402評(píng)論 0 0
  • 話說?有一富豪買了塊地秽荤,修了別墅,后院更有多棵百年荔枝樹菠发,當(dāng)初買地時(shí)他看中了的是這些荔枝樹王滤,因?yàn)樗掀畔矚g吃荔枝贺嫂。...
    信時(shí)光閱讀 163評(píng)論 0 0
  • 大boss扔來一堆外文資料讓我翻譯滓鸠。 有一種命令,沒有時(shí)間限制第喳,也就是說糜俗,你妹,快給我做曲饱! 不知道老板什么時(shí)間會(huì)要...
    信則有_有有塔羅閱讀 480評(píng)論 0 1