从零到一撸个YYLabel

前言

在学习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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,933评论 18 139
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,241评论 4 61
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 173,316评论 25 708
  • “我跟你说,我做了一个好奇怪的梦。我梦见自己在宿舍睡觉,然后做梦去图书馆找依然,但是一进图书馆就趴着睡着了,接着梦...
    盲心桥阅读 464评论 0 0
  • 作者:夏汐蕊☞想看其他作品请点击这里☺简书连载风云录前情回顾《我的爱只属于你(16)》 【第十七章】走进唐风斋,一...
    夏汐蕊阅读 429评论 0 8