YYText框架 圖片并排的源碼實現(xiàn)

有時我們有的UI效果圖如果是文字加圖片混合在一起的, 如果使用UIImageView來拼接UILable的話后期擴展維護起來困難,這個時候我們可以使用富文本來實現(xiàn):
圖標(biāo)文字混合.png
一、使用YYText框架實現(xiàn)

這里推薦使用YYText框架里面封裝的api來實現(xiàn),用別人已經(jīng)封裝得比較完善的會比較簡單,見代碼:

//   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;
}

由上面可以知道:
實現(xiàn)的方式是使用YYLable顯示添加了圖片attachmentNSMutableAttributedString.

二、YYText創(chuàng)建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.將附件內(nèi)容設(shè)置到atr中,內(nèi)部調(diào)用[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.創(chuàng)建CTRunDelegate設(shè)置到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 的內(nèi)部實現(xiàn)使用了YYTextAsyncLayer作為self.layer。
// @interface YYLabel : UIView
+ (Class)layerClass {
    return [YYTextAsyncLayer class];
}

- (void)setAttributedText:(NSAttributedString *)attributedText {
        省略... 
// 更新好屬性之后,調(diào)用_setLayoutNeedUpdate去執(zhí)行l(wèi)abel的內(nèi)容更新
        [self _setLayoutNeedUpdate];
}

- (void)_setLayoutNeedUpdate {
    _state.layoutNeedUpdate = YES;
    [self _clearInnerLayout];// 清除之前的布局
// 將layer設(shè)置為需要重繪(相當(dāng)于dirty),系統(tǒng)會調(diào)用layer的-display方法進行內(nèi)容重繪
    [self.layer setNeedsDisplay];
}

由上面可以知道,文字與附件attachment的繪制在YYTextAsyncLayer當(dāng)中的
iOS UIView和CALayer

  • YYTextAsyncLayer繪制步驟
// 重寫了- (void)display,這個方法在需要展示或者setNeedsDisplay時候會調(diào)用。
- (void)display {
    super.contents = super.contents;
    [self _displayAsync:_displaysAsynchronously];
}

- (void)_displayAsync:(BOOL)async {
// 1.創(chuàng)建DisplayTask任務(wù),這里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. 調(diào)用willDisplay(self)。
  2. 創(chuàng)建圖形上下文ImageContext,調(diào)用display這個block,將具體的內(nèi)容繪制到ImageContext。
  3. 將ImageContext的內(nèi)容設(shè)置為layer. contents
  4. 調(diào)用didDisplay(self, YES)。
  • 具體的繪制任務(wù)
task.display = ^(CGContextRef context, CGSize size, BOOL (^isCancelled)(void)) {
    YYTextLayout *drawLayout = layout;
    if (layoutNeedUpdate) {
// 1. 計算得出layout
        layout = [YYTextLayout layoutWithContainer:container text:text];
// 2. 根據(jù)文字行數(shù)去縮減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中繪制的東西比較多(邊框、背景色、陰影、下劃線等),這里挑出文字繪制和附件繪制函數(shù)來說明。
// 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);
// 內(nèi)部將文字根據(jù)字體、大小、顏色等屬性,調(diào)用相關(guān)方法繪制到上下文中,這里不展開
                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];
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容