YYText框架 图片并排的源码实现

有时我们有的UI效果图如果是文字加图片混合在一起的, 如果使用UIImageView来拼接UILable的话后期扩展维护起来困难,这个时候我们可以使用富文本来实现:
图标文字混合.png
一、使用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显示添加了图片attachmentNSMutableAttributedString.

二、YYText创建NSMutableAttributedString的方式
  1. 首先看拼接方法:
+ (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);
    }
}

从上面代码可以知道,绘制的步骤是:

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

推荐阅读更多精彩内容