iOS-阅读器系列

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

2021-03-15  本文已影响0人  Sweet丶
有时我们有的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中的?
// @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

// 重写了- (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];
};
// 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];
        }
    }
}
上一篇下一篇

猜你喜欢

热点阅读