在一個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後邊拼接上寬高信息的方式來處理。
