YYText源碼分析

YYText 簡單介紹

YYText 是YYKit中的一個富文本顯示,編輯組件,擁有YYLabel,YYTextView 兩個控件。其中YYLabel類似于UILabel,但功能更為強大,支持異步文本渲染,更豐富的效果顯示,支持UIImage,UIView, CALayer 文本附件,自定義強調文本范圍,支持垂直文本顯示等等。YYTextView 類似UITextView,除了兼容UITextView API,擴展了更多的CoreText 效果屬性,支持高亮鏈接,支持自定義內部文本路徑形狀,支持圖片拷貝,粘貼等等。下面是YYText 與 TextKit 的比較圖:

YYText VS TextKit

YYLabel

YYLabel 的實現,是基于CoreText 框架 在 Context 上進行繪制,通過設置NSMutableAttributedString實現文本各種效果屬性的展現。下面是YYLabel 主要相關的部分:

  • YYAsyncLayer: YYLabel的異步渲染,通過YYAsyncLayerDisplayTask 回調渲染
  • YYTextLayout: YYLabel的布局管理類,也負責繪制
  • YYTextContainer: YYLabel的布局類
  • NSAttributedString+YYText: YYLabel 所有效果屬性設置

YYAsyncLayer 的異步實現

YYAsyncLayer 是 CALayer的子類,通過設置 YYLabel 類方法 layerClass
返回自定義的 YYAsyncLayer ,重寫了父類的 setNeedsDisplay , display 實現 contents 自定義刷新。YYAsyncLayerDelegate返回新的刷新任務 newAsyncDisplayTask 用于更新過程回調,返回到 YYLabel 進行文本渲染。其中 YYSentinel是一個線程安全的原子遞增計數器,用于判斷更新是否取消。

YYTextLayout

YYLabel 實現了 YYAsyncLayerDelegate 代理方法 newAsyncDisplayTask,回調處理3種文本渲染狀態willDisplay ,display,didDisplay 。在渲染之前,移除不需要的文本附件,渲染完成后,添加需要的文本附件。渲染時,首先獲取YYTextLayout, 一般包含了 YYTextContainerNSAttributedString 兩部分, 分別負責文本展示的形狀和內容。不管是渲染時和渲染完成后,最后都需要調用 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是圖形上下文,文本的繪制在它上面進行,size是 context 的大小,point 是繪制的起始點,view,layer 是添加文本附件的視圖(層),debug 調試選項,cancel 取消繪制,根據傳入的文本是否需要繪制YYText自定義屬性效果依次進行相應的繪制。有以下這些屬性:


///< 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 中 調用 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];

根據YYTextContainerNSAttributedString生成 YYTextLayout,接下來就是渲染了, 具體的渲染由 CoreText來實現。

CoreText

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

CoreText

一個 NSAttributeString通過CoreText的CTFramesetterCreateWithAttributedString生成CTFramesetter,它是創建 CTFrame的工廠,為 CTFramesetter 提供一個 CGPath,它就會通過它持有的 CTTypesetter生成 CTFrameCTFrame里面包含了 CTLine CTLine 中包含了此行所有的 CTRun,然后就可以繪制到畫布上。CTFrame,CTLine,CTRun都提供了渲染接口,但前兩者是封裝,最后實際都是調用到 CTRun的渲染接口去繪制。

在YYTextlayout 中的代碼體現

  // 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 根據 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有下面兩個屬性,可以用來自定義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;

復雜的就是每行的frame的計算, calculate line frame 部分,看的有點暈~~~
需要進行坐標系的轉化,YYTextLine是對 CTLine的進一步封裝。

最終文字的繪制都會調用

 YYTextDrawText(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, BOOL (^cancel)(void)) 
    //坐標系變化
    CGContextTranslateCTM(context, point.x, point.y);
    CGContextTranslateCTM(context, 0, size.height);
    CGContextScaleCTM(context, 1, -1);
CGContextSaveGState(context); {  
 // 所有的渲染有關代碼    
} CGContextRestoreGState(context);

最后都需要調用 YYTextDrawRun 進行繪制 CTRunDraw(run, context, CFRangeMake(0, 0));

YYTextView

其繪制原理同 YYLabel,其中 YYTextContainerView 是文本顯示的視圖,其layout 屬性用來繪制文本。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容