YYText框架 图片并排的源码实现
2021-03-15 本文已影响0人
Sweet丶
有时我们有的UI效果图如果是文字加图片混合在一起的, 如果使用UIImageView来拼接UILable的话后期扩展维护起来困难,这个时候我们可以使用富文本来实现:
图标文字混合.png
二、YYText创建
三、 YYText如何绘制
一、使用YYText框架实现
这里推荐使用YYText
框架里面封装的api来实现,用别人已经封装得比较完善的会比较简单,见代码:
// pod 'YYText', '~> 1.0.7'
- (void)viewDidLoad {
[super viewDidLoad];
NSString *leftDiamond = [NSString stringWithFormat:@"蓝钻余额:%@ ", @(600)];
UIImage *image = [UIImage imageNamed:@"privacyChat_diamond"];
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
style.alignment = NSTextAlignmentCenter;
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:leftDiamond attributes:@{NSForegroundColorAttributeName : [UIColor orangeColor], NSFontAttributeName : self.diamondLabel.font, NSParagraphStyleAttributeName : style}];
NSAttributedString *attrStr_image = [NSAttributedString yy_attachmentStringWithContent:image contentMode:UIViewContentModeScaleAspectFit attachmentSize:CGSizeMake(16, 16) alignToFont:self.diamondLabel.font alignment:YYTextVerticalAlignmentCenter];
[attrStr appendAttributedString:attrStr_image];
self.diamondLabel.attributedText = attrStr;
}
- (YYLabel *)diamondLabel
{
if (_diamondLabel == nil) {
_diamondLabel = [[YYLabel alloc] initWithFrame:CGRectMake(10, 300, [UIScreen mainScreen].bounds.size.width - 20, 30)];
_diamondLabel.userInteractionEnabled = YES;
_diamondLabel.numberOfLines = 1;
_diamondLabel.font = [UIFont systemFontOfSize:16];
_diamondLabel.textVerticalAlignment = YYTextVerticalAlignmentCenter;
_diamondLabel.backgroundColor = [UIColor clearColor];
}
return _diamondLabel;
}
由上面可以知道:
实现的方式是使用YYLable
显示添加了图片attachment
的NSMutableAttributedString
.
二、YYText创建NSMutableAttributedString
的方式
- 首先看拼接方法:
+ (NSMutableAttributedString *)yy_attachmentStringWithContent:(id)content
contentMode:(UIViewContentMode)contentMode
attachmentSize:(CGSize)attachmentSize
alignToFont:(UIFont *)font
alignment:(YYTextVerticalAlignment)alignment{
// 1.初始化AttributedString为占位符YYTextAttachmentToken (= @"\uFFFC");
NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:YYTextAttachmentToken];
YYTextAttachment *attach = [YYTextAttachment new];
attach.content = content;
attach.contentMode = contentMode;
// 2.将附件内容设置到atr中,内部调用[self yy_setAttribute:YYTextAttachmentAttributeName value:textAttachment range:range];
[atr yy_setTextAttachment:attach range:NSMakeRange(0, atr.length)];
// 3.将附件大小及与文字对齐封装在YYTextRunDelegate中
YYTextRunDelegate *delegate = [YYTextRunDelegate new];
delegate.width = attachmentSize.width;
...
// 4.创建CTRunDelegate设置到atr中
CTRunDelegateRef delegateRef = delegate.CTRunDelegate;
[atr yy_setRunDelegate:delegateRef range:NSMakeRange(0, atr.length)];
if (delegate) CFRelease(delegateRef);
return atr;
}
文字对齐情况.png
三、 YYText如何绘制attachment
和文字到YYLable
中的?
- YYLabel 的内部实现使用了
YYTextAsyncLayer
作为self.layer。
// @interface YYLabel : UIView
+ (Class)layerClass {
return [YYTextAsyncLayer class];
}
- (void)setAttributedText:(NSAttributedString *)attributedText {
省略...
// 更新好属性之后,调用_setLayoutNeedUpdate去执行label的内容更新
[self _setLayoutNeedUpdate];
}
- (void)_setLayoutNeedUpdate {
_state.layoutNeedUpdate = YES;
[self _clearInnerLayout];// 清除之前的布局
// 将layer设置为需要重绘(相当于dirty),系统会调用layer的-display方法进行内容重绘
[self.layer setNeedsDisplay];
}
由上面可以知道,文字与附件attachment
的绘制在YYTextAsyncLayer当中的
iOS UIView和CALayer
YYTextAsyncLayer
绘制步骤
// 重写了- (void)display,这个方法在需要展示或者setNeedsDisplay时候会调用。
- (void)display {
super.contents = super.contents;
[self _displayAsync:_displaysAsynchronously];
}
- (void)_displayAsync:(BOOL)async {
// 1.创建DisplayTask任务,这里delegate是YYLable
YYTextAsyncLayerDisplayTask *task = [delegate newAsyncDisplayTask];
if (async) {// 如果是异步绘制
...
}else{// 同步绘制
if (task.willDisplay) task.willDisplay(self);
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, self.contentsScale);
CGContextRef context = UIGraphicsGetCurrentContext();
task.display(context, self.bounds.size, ^{return NO;});
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.contents = (__bridge id)(image.CGImage);
if (task.didDisplay) task.didDisplay(self, YES);
}
}
从上面代码可以知道,绘制的步骤是:
- 调用willDisplay(self)。
- 创建图形上下文ImageContext,调用display这个block,将具体的内容绘制到ImageContext。
- 将ImageContext的内容设置为layer. contents
- 调用didDisplay(self, YES)。
- 具体的绘制任务
task.display = ^(CGContextRef context, CGSize size, BOOL (^isCancelled)(void)) {
YYTextLayout *drawLayout = layout;
if (layoutNeedUpdate) {
// 1. 计算得出layout
layout = [YYTextLayout layoutWithContainer:container text:text];
// 2. 根据文字行数去缩减layout
shrinkLayout = [YYLabel _shrinkLayoutWithLayout:layout];
if (isCancelled()) return;
layoutUpdated = YES;
drawLayout = shrinkLayout ? shrinkLayout : layout;
}
CGSize boundingSize = drawLayout.textBoundingSize;
CGPoint point = CGPointZero;
if (verticalAlignment == YYTextVerticalAlignmentCenter) {
...
} else if (verticalAlignment == YYTextVerticalAlignmentBottom) {
...
}
point = YYTextCGPointPixelRound(point);
//3. 将drawLayout绘制到context中
[drawLayout drawInContext:context size:size point:point view:nil layer:nil debug:debug cancel:isCancelled];
};
-
绘制到context中具体做的什么
因为YYLable中绘制的东西比较多(边框、背景色、阴影、下划线等),这里挑出文字绘制和附件绘制函数来说明。
// 1. 文字
static void YYTextDrawText(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, BOOL (^cancel)(void)) {
CGContextSaveGState(context); {
CGContextTranslateCTM(context, point.x, point.y);
CGContextTranslateCTM(context, 0, size.height);
CGContextScaleCTM(context, 1, -1);
// ...
NSArray *lines = layout.lines;
for (NSUInteger l = 0, lMax = lines.count; l < lMax; l++) {
YYTextLine *line = lines[l];
if (layout.truncatedLine && layout.truncatedLine.index == line.index) line = layout.truncatedLine;
NSArray *lineRunRanges = line.verticalRotateRange;
CGFloat posX = line.position.x + verticalOffset;
CGFloat posY = size.height - line.position.y;
CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine);
for (NSUInteger r = 0, rMax = CFArrayGetCount(runs); r < rMax; r++) {
CTRunRef run = CFArrayGetValueAtIndex(runs, r);
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextSetTextPosition(context, posX, posY);
// 内部将文字根据字体、大小、颜色等属性,调用相关方法绘制到上下文中,这里不展开
YYTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset);
}
if (cancel && cancel()) break;
}
} CGContextRestoreGState(context);
}
// 2. 附件attchment:如果是图片则绘制到上下文中;如果是view和layer则添加到子视图中
static void YYTextDrawAttachment(YYTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, UIView *targetView, CALayer *targetLayer, BOOL (^cancel)(void)) {
for (NSUInteger i = 0, max = layout.attachments.count; i < max; i++) {
YYTextAttachment *a = layout.attachments[i];
if (!a.content) continue;
UIImage *image = nil;
UIView *view = nil;
CALayer *layer = nil;
if ([a.content isKindOfClass:[UIImage class]]) {
image = a.content;
} else if ([a.content isKindOfClass:[UIView class]]) {
view = a.content;
} else if ([a.content isKindOfClass:[CALayer class]]) {
layer = a.content;
}
if (!image && !view && !layer) continue;
if (image && !context) continue;
if (view && !targetView) continue;
if (layer && !targetLayer) continue;
if (cancel && cancel()) break;
CGSize asize = image ? image.size : view ? view.frame.size : layer.frame.size;
CGRect rect = ((NSValue *)layout.attachmentRects[i]).CGRectValue;
if (isVertical) {
rect = UIEdgeInsetsInsetRect(rect, UIEdgeInsetRotateVertical(a.contentInsets));
} else {
rect = UIEdgeInsetsInsetRect(rect, a.contentInsets);
}
rect = YYTextCGRectFitWithContentMode(rect, asize, a.contentMode);
rect = YYTextCGRectPixelRound(rect);
rect = CGRectStandardize(rect);
rect.origin.x += point.x + verticalOffset;
rect.origin.y += point.y;
if (image) {
CGImageRef ref = image.CGImage;
if (ref) {
CGContextSaveGState(context);
CGContextTranslateCTM(context, 0, CGRectGetMaxY(rect) + CGRectGetMinY(rect));
CGContextScaleCTM(context, 1, -1);
CGContextDrawImage(context, rect, ref);
CGContextRestoreGState(context);
}
} else if (view) {
view.frame = rect;
[targetView addSubview:view];
} else if (layer) {
layer.frame = rect;
[targetLayer addSublayer:layer];
}
}
}
-
文字
YYTextLayout
的计算
YYText 源码剖析:CoreText 与异步绘制