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 属性用来绘制文本。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容