从零到一撸个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

    前言 在学习YYText过程中,分析完YYLabel原理后一时手痒,自己撸了个JKRichLabel,写文记录,也...

  • YYLabel 给超出文字变成省略号

    最近研究了一下yylabel,遇到一个问题 yylabel.lineBreakMode = NSLineBreak...

  • 揭秘深圳市趣渔乐APP是不是传销?趣渔乐能赚钱吗?趣渔乐是真的吗

    从趣步、亦跑、智慧晶开始,加v 68109440 这些所谓的零撸项目已经火了一年左右,所谓的零撸只是一个噱头,让你...

  • YYLabel富文本显示

    YYLabel实现方式 //1.简单显示label YYLabel *label = [YYLabel new];...

  • 来来来,零撸速度走起

    速度搞起啦,各位。不需要投钱,不需要锁仓,完全零撸,零撸,零撸,重要的事情说三遍。每天一次零撸分享,每天一次零撸渠...

  • 从零到一

    从零到一 或者说从计划到成果 这是我对自己刚刚过去一年的一个大的方面的总结 从十点读书会年会结束到现在,我...

  • 《从零到一》

    《from zero to one》彼得-蒂格 第一章《The Challenge of the Future》:...

  • 从零到一

    看很多朋友在简书上写作,于是决定开始写作,谁都知道写作是一件多么重要但不紧急的事,不写,永远有明天。一天不忙下来,...

  • 从"零"到"一"

    借用一本书的标题,对我而言"零"指的是对西点的制作一无所知,"一"指的是我终于学会了一款。自从买了烤箱,探索美食的...

  • 从零到一

    随着A1犁煤器照明灯的方位调好,电话那头运行说可以了,我无法掩饰自己内心的激动,装作拍手上的灰给自己鼓了两次手。这...

网友评论

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

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