从零到一撸个YYLabel

作者: 01_Jack | 来源:发表于2017-11-09 11:45 被阅读1079次

    前言

    在学习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提供绘制内容,结合绘制区域生成CTFrame。CTFrame包含一个或多个CTLine,CTLine包含一个或多个CTRun。CTLine为绘制区域中一行的内容,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创建,CTRunDelegateCallbacks可提供一系列函数用于返回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替换为省略号后还需动态改变行高,吧啦吧啦诸如此类),然后通过CTLineCreateTruncatedLine创建truncatedLine,受numberOfLines所限,绘制过程中可能不需要绘制到最后一行。当然,这些都不是事儿,加几句条件判断再改动一下逻辑还是可以实现的。由于这两种属性使用较少,比较鸡肋,so...偷个懒
      另外,由于不支持这两种属性,truncatedLine没通过CTLineCreateTruncatedLine生成,而是直接在末尾添加省略号生成新的CTLine
    Long Text说明

    效果图中有Long Text的例子,label外套scrollview,将scrollview的contentSize设置为label的size,label的size通过sizeToFit自动计算。如果文字足够长,这种方案就over了

    Demo

    相关文章

      网友评论

        本文标题:从零到一撸个YYLabel

        本文链接:https://www.haomeiwen.com/subject/ocvymxtx.html