在一個UIView的子控件上實現圖文混排顯示,支持本地圖片和網絡圖片的顯示。
CoreText從繪制純文本到繪制圖片,依然是使用NSAttributedString,只不過圖片的實現方式是用一個空白字符作為在NSAttributedString中的占位符,然後設置代理,告訴CoreText給該占位字符留出一定的寬高。最後把圖片繪制到預留的位置上。
1、圖片的代理方法:
#pragma mark 圖片代理 void RunDelegateDeallocCallback(void *refCon){ NSLog(@"RunDelegate dealloc"); } CGFloat RunDelegateGetAscentCallback(void *refCon){ NSString *imageName = (__bridge NSString *)refCon; if ([imageName isKindOfClass:[NSString class]]){ // 對應本地圖片 return [UIImage imageNamed:imageName].size.height; } // 對應網絡圖片 return [[(__bridge NSDictionary *)refCon objectForKey:@"height"] floatValue]; } CGFloat RunDelegateGetDescentCallback(void *refCon){ return 0; } CGFloat RunDelegateGetWidthCallback(void *refCon){ NSString *imageName = (__bridge NSString *)refCon; if ([imageName isKindOfClass:[NSString class]]){ // 本地圖片 return [UIImage imageNamed:imageName].size.width; } // 對應網絡圖片 return [[(__bridge NSDictionary *)refCon objectForKey:@"width"] floatValue]; }
2、下載圖片的方法
- (void)downLoadImageWithURL:(NSURL *)url{ __weak typeof(self) weakSelf = self; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ SDWebImageOptions options = SDWebImageRetryFailed | SDWebImageHandleCookies | SDWebImageContinueInBackground; options = SDWebImageRetryFailed | SDWebImageContinueInBackground; [[SDWebImageManager sharedManager] downloadImageWithURL:url options:options progress:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { weakSelf.image = image; NSLog(@"%@",image); dispatch_async(dispatch_get_main_queue(), ^{ if (weakSelf.image) { [weakSelf setNeedsDisplay]; } }); }]; }); }
3、圖文混排
- (void)drawRect:(CGRect)rect { [super drawRect:rect]; NSString* title = @"在現實生活中,我們要不斷內外兼修,幾十載的人生旅途,看過這邊風景,必然錯過那邊彩虹,有所得,必然有所失。有時,我們只有徹底做到拿得起,放得下,才能擁有一份成熟,才會活得更加充實、坦然、輕松和自由。"; //步驟1:獲取上下文 CGContextRef contextRef = UIGraphicsGetCurrentContext(); // [a,b,c,d,tx,ty] NSLog(@"轉換前的坐標:%@",NSStringFromCGAffineTransform(CGContextGetCTM(contextRef))); //步驟2:翻轉坐標系; CGContextSetTextMatrix(contextRef, CGAffineTransformIdentity); CGContextTranslateCTM(contextRef, 0, self.bounds.size.height); CGContextScaleCTM(contextRef, 1.0, -1.0); NSLog(@"轉換後的坐標:%@",NSStringFromCGAffineTransform(CGContextGetCTM(contextRef))); //步驟3:創建NSAttributedString NSMutableAttributedString *attributed = [[NSMutableAttributedString alloc] initWithString:title]; //設置字體大小 [attributed addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:20] range:NSMakeRange(0, 5)]; //設置字體顏色 [attributed addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(3, 10)]; [attributed addAttribute:(id)kCTForegroundColorAttributeName value:(id)[UIColor greenColor].CGColor range:NSMakeRange(0, 2)]; // 設置行距等樣式 CGFloat lineSpace = 10; // 行距一般取決於這個值 CGFloat lineSpaceMax = 20; CGFloat lineSpaceMin = 2; const CFIndex kNumberOfSettings = 3; // 結構體數組 CTParagraphStyleSetting theSettings[kNumberOfSettings] = { {kCTParagraphStyleSpecifierLineSpacingAdjustment,sizeof(CGFloat),&lineSpace}, {kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(CGFloat),&lineSpaceMax}, {kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(CGFloat),&lineSpaceMin} }; CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings); // 單個元素的形式 // CTParagraphStyleSetting theSettings = {kCTParagraphStyleSpecifierLineSpacingAdjustment,sizeof(CGFloat),&lineSpace}; // CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(&theSettings, kNumberOfSettings); // 兩種方式皆可 // [attributed addAttribute:(id)kCTParagraphStyleAttributeName value:(__bridge id)theParagraphRef range:NSMakeRange(0, attributed.length)]; // 將設置的行距應用於整段文字 [attributed addAttribute:NSParagraphStyleAttributeName value:(__bridge id)(theParagraphRef) range:NSMakeRange(0, attributed.length)]; CFRelease(theParagraphRef); // 插入圖片部分 //為圖片設置CTRunDelegate,delegate決定留給圖片的空間大小 NSString *weicaiImageName = @"cloud.jpg"; CTRunDelegateCallbacks imageCallbacks; imageCallbacks.version = kCTRunDelegateVersion1; imageCallbacks.dealloc = RunDelegateDeallocCallback; imageCallbacks.getAscent = RunDelegateGetAscentCallback; imageCallbacks.getDescent = RunDelegateGetDescentCallback; imageCallbacks.getWidth = RunDelegateGetWidthCallback; // ①該方式適用於圖片在本地的情況 // 設置CTRun的代理 CTRunDelegateRef runDelegate = CTRunDelegateCreate(&imageCallbacks, (__bridge void *)(weicaiImageName)); NSMutableAttributedString *imageAttributedString = [[NSMutableAttributedString alloc] initWithString:@" "];//空格用於給圖片留位置 [imageAttributedString addAttribute:(NSString *)kCTRunDelegateAttributeName value:(__bridge id)runDelegate range:NSMakeRange(0, 1)]; CFRelease(runDelegate); [imageAttributedString addAttribute:@"imageName" value:weicaiImageName range:NSMakeRange(0, 1)]; // 在index處插入圖片,可插入多張 [attributed insertAttributedString:imageAttributedString atIndex:5]; // [attributed insertAttributedString:imageAttributedString atIndex:10]; // ②若圖片資源在網絡上,則需要使用0xFFFC作為占位符 // 圖片信息字典 NSString *picURL =@"https://www.baidu.com/img/bd_logo1.png"; UIImage* pImage = [UIImage imageNamed:@"123.png"]; NSDictionary *imgInfoDic = @{@"width":@(270),@"height":@(129)}; // 寬高跟具體圖片有關 // 設置CTRun的代理 CTRunDelegateRef delegate = CTRunDelegateCreate(&imageCallbacks, (__bridge void *)imgInfoDic); // 使用0xFFFC作為空白的占位符 unichar objectReplacementChar = 0xFFFC; NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:1]; NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:content]; CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate); CFRelease(delegate); // 將創建的空白AttributedString插入進當前的attrString中,位置可以隨便指定,不能越界 [attributed insertAttributedString:space atIndex:10]; //步驟4:根據NSAttributedString創建CTFramesetterRef CTFramesetterRef framesetterRef = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributed); //步驟5:創建繪制區域CGPathRef CGMutablePathRef pathRef = CGPathCreateMutable(); CGPathAddRect(pathRef, NULL, self.bounds); //步驟6:根據CTFramesetterRef和CGPathRef創建CTFrame; CTFrameRef frameRef = CTFramesetterCreateFrame(framesetterRef, CFRangeMake(0, [attributed length]), pathRef, NULL); //步驟7:CTFrameDraw繪制。 CTFrameDraw(frameRef, contextRef); // 處理繪制圖片的邏輯 CFArrayRef lines = CTFrameGetLines(frameRef); CGPoint lineOrigins[CFArrayGetCount(lines)]; // 把ctFrame裡每一行的初始坐標寫到數組裡 CTFrameGetLineOrigins(frameRef, CFRangeMake(0, 0), lineOrigins); // 遍歷CTRun找出圖片所在的CTRun並進行繪制 for (int i = 0; i < CFArrayGetCount(lines); i++) { // 遍歷每一行CTLine CTLineRef line = CFArrayGetValueAtIndex(lines, i); CGFloat lineAscent; CGFloat lineDescent; CGFloat lineLeading; // 行距 CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, &lineLeading); CFArrayRef runs = CTLineGetGlyphRuns(line); for (int j = 0; j < CFArrayGetCount(runs); j++) { // 遍歷每一個CTRun CGFloat runAscent; CGFloat runDescent; CGPoint lineOrigin = lineOrigins[i]; // 獲取該行的初始坐標 CTRunRef run = CFArrayGetValueAtIndex(runs, j); // 獲取當前的CTRun NSDictionary* attributes = (NSDictionary*)CTRunGetAttributes(run); CGRect runRect; runRect.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0,0), &runAscent, &runDescent, NULL); // 這一段可參考Nimbus的NIAttributedLabel runRect = CGRectMake(lineOrigin.x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL), lineOrigin.y - runDescent, runRect.size.width, runAscent + runDescent); NSString *imageName = [attributes objectForKey:@"imageName"]; if ([imageName isKindOfClass:[NSString class]]){ // 繪制本地圖片 UIImage *image = [UIImage imageNamed:imageName]; CGRect imageDrawRect; imageDrawRect.size = image.size; NSLog(@"%.2f",lineOrigin.x); // 該值是0,runRect已經計算過起始值 imageDrawRect.origin.x = runRect.origin.x;// + lineOrigin.x; imageDrawRect.origin.y = lineOrigin.y; CGContextDrawImage(contextRef, imageDrawRect, image.CGImage); } else { imageName = nil; CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes objectForKey:(__bridge id)kCTRunDelegateAttributeName]; if (!delegate){ continue; // 如果是非圖片的CTRun則跳過 } // 網絡圖片 UIImage *image; if (!self.image){ // 圖片未下載完成,使用占位圖片 image = pImage; // 去下載圖片 [self downLoadImageWithURL:[NSURL URLWithString:picURL]]; }else{ image = self.image; } // 繪制網絡圖片 CGRect imageDrawRect; imageDrawRect.size = image.size; NSLog(@"%.2f",lineOrigin.x); // 該值是0,runRect已經計算過起始值 imageDrawRect.origin.x = runRect.origin.x;// + lineOrigin.x; imageDrawRect.origin.y = lineOrigin.y; CGContextDrawImage(contextRef, imageDrawRect, image.CGImage); } } } //內存管理 CFRelease(frameRef); CFRelease(pathRef); CFRelease(framesetterRef); }
本文實現了同時繪制本地圖片和網絡圖片。大體思路是,網絡圖片還未下載時,先使用該圖片的占位圖片進行繪制(為了方便,占位圖直接使用了另一張本地圖片),然後使用SDWebImage框架提供的下載功能去下載網絡圖片,等下載完成時,調用UIView的setNeedDisplay方法進行重繪即可。
需要注意的一點就是,對於本地圖片,是可以直接拿到其寬高數據的,對於網絡的圖片,在下載完成之前不知道其寬高,我們往往會采取在其URL後邊拼接上寬高信息的方式來處理。