上一節中,我詳細的講解了用面向對象的思想將Core Text的純C語言的代碼進行了封裝。這一節,我將對“圖文混排”的效果也進行封裝工作。不過,這一節的代碼是基於上一節的,所以,如果你沒有浏覽過上一節的內容,請點擊這裡。先看看最終的效果圖:
現在,我們就來對上一節的代碼,繼續擴充。
1. 添加了圖片信息,所以我們需要修改數據源(plist)的結構
1)為每一項添加了type信息,“txt”表示純文本;“img”表示圖片;圖片信息包括name,width,height。 name就是圖片的地址,我這裡是存儲在沙盒中,實際開發的時候,可以加載遠程圖片。
2)一定要提供圖片的width和height信息,因為Core Text排版是要計算每一個元素的占位大小的。如果不提供圖片的width和height信息,客戶端在加載遠程圖片後,還要計算出width和height,效率低下,如果在網絡比較差的情況下,圖片一直加載不到,那麼Core Text排版就明顯混亂了;如果服務端數據提供了width和height信息,就算圖片沒有加載過來,也可以有同等大小的空白區域被占位著,不影響整體的布局。
2. 定義CoreTextImageData模型,用於存儲圖片的名稱及位置信息
@interface CoreTextImageData : NSObject @property (nonatomic,copy) NSString *name; // 此坐標是 CoreText 的坐標系,而不是UIKit的坐標系 @property (nonatomic,assign) CGRect imagePosition; @end
3. CoreTextData類中應該包含CoreTextImageData模型信息,這裡用的是數組imageArray,因為有可能包含多張圖片。所以改造一下CoreTextData類,CoreTextData.h代碼如下:
@interface CoreTextData : NSObject @property (nonatomic,assign) CTFrameRef ctFrame; @property (nonatomic,assign) CGFloat height; @property (nonatomic,strong) NSArray *imageArray; @end
4. 改造CTFrameParser類中的parseTemplateFile方法,使其包含CoreTextImageData信息
+ (CoreTextData *)parseTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config { NSMutableArray *imageArray = [NSMutableArray array]; NSAttributedString *content = [self loadTemplateFile:path config:config imageArray:imageArray]; CoreTextData *data = [self parseAttributedContent:content config:config]; data.imageArray = imageArray; return data; }
+ (NSAttributedString *)loadTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config imageArray:(NSMutableArray *)imageArray{ NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init]; // JSON方式獲取數據 // NSArray *array = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil]; NSArray *array = [NSArray arrayWithContentsOfFile:path]; if (array) { if ([array isKindOfClass:[NSArray class]]) { for (NSDictionary *dict in array) { NSString *type = dict[@"type"]; if ([type isEqualToString:@"txt"]) { NSAttributedString *as = [self parseAttributedContentFromNSDictionary:dict config:config]; [result appendAttributedString:as]; } else if ([type isEqualToString:@"img"]) { CoreTextImageData *imageData = [[CoreTextImageData alloc] init]; imageData.name = dict[@"name"]; [imageArray addObject:imageData]; NSAttributedString *as = [self parseImageDataFromNSDictionary:dict config:config]; [result appendAttributedString:as]; } } } } return result; }
6. 占位字符及設置占位字符的CTRunDelegate,代碼中是用'0xFFFC'這個字符進行占位的。
static CGFloat ascentCallback(void *ref) { return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"height"] floatValue]; } static CGFloat descentCallback(void *ref) { return 0; } static CGFloat widthCallback(void *ref) { return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"width"] floatValue]; } + (NSAttributedString *)parseImageDataFromNSDictionary:(NSDictionary *)dict config:(CTFrameParserConfig *)config { CTRunDelegateCallbacks callbacks; // memset將已開辟內存空間 callbacks 的首 n 個字節的值設為值 0, 相當於對CTRunDelegateCallbacks內存空間初始化 memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks)); callbacks.version = kCTRunDelegateVersion1; callbacks.getAscent = ascentCallback; callbacks.getDescent = descentCallback; callbacks.getWidth = widthCallback; CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)(dict)); // 使用0xFFFC 作為空白的占位符 unichar objectReplacementChar = 0xFFFC; NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:1]; NSDictionary *attributes = [self attributesWithConfig:config]; NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:content attributes:attributes]; CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate); CFRelease(delegate); return space; }
data.imageArray = imageArray;
1) 通過調用CTFrameGetLines方法獲得所有的CTLine。
2)通過調用CTFrameGetLineOrigins方法獲取每一行的起始坐標。
3)通過調用CTLineGetGlyphRuns方法,獲取每一行所有的CTRun。
4)通過CTRun的attributes信息找到key為CTRunDelegateAttributeName的信息,如果存在,表明他就是占位字符,否則的話直接過濾掉。
5)最終計算獲得每一個占位字符的實際尺寸大小。
- (void)setImageArray:(NSArray *)imageArray { _imageArray = imageArray; [self fillImagePosition]; } - (void)fillImagePosition { if (self.imageArray.count == 0) return; NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame); int lineCount = lines.count; // 每行的起始坐標 CGPoint lineOrigins[lineCount]; CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins); int imageIndex = 0; CoreTextImageData *imageData = self.imageArray[0]; for (int i = 0; i < lineCount; i++) { if (!imageData) break; CTLineRef line = (__bridge CTLineRef)(lines[i]); NSArray *runObjectArray = (NSArray *)CTLineGetGlyphRuns(line); for (id runObject in runObjectArray) { CTRunRef run = (__bridge CTRunRef)(runObject); NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run); CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)([runAttributes valueForKey:(id)kCTRunDelegateAttributeName]); // 如果delegate是空,表明不是圖片 if (!delegate) continue; NSDictionary *metaDict = CTRunDelegateGetRefCon(delegate); if (![metaDict isKindOfClass:[NSDictionary class]]) continue; /* 確定圖片run的frame */ CGRect runBounds; CGFloat ascent,descent; runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL); runBounds.size.height = ascent + descent; // 計算出圖片相對於每行起始位置x方向上面的偏移量 CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL); runBounds.origin.x = lineOrigins[i].x + xOffset; runBounds.origin.y = lineOrigins[i].y; runBounds.origin.y -= descent; imageData.imagePosition = runBounds; imageIndex++; if (imageIndex == self.imageArray.count) { imageData = nil; break; } else { imageData = self.imageArray[imageIndex]; } } } }
1)先調用CTFrameDraw方法完成整體的繪制,此時圖片區域就是圖片實際大小的一片空白顯示。
2)遍歷CoreTextData中的imageArray數組,使用CGContextDrawImage方法在對應的空白區域繪制圖片。
- (void)drawRect:(CGRect)rect { [super drawRect:rect]; CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM(context, 1.0, -1.0); // 先整體繪制 if (self.data) { CTFrameDraw(self.data.ctFrame, context); } // 繪制出圖片 for (CoreTextImageData *imageData in self.data.imageArray) { UIImage *image = [UIImage imageNamed:imageData.name]; if (image) { CGContextDrawImage(context, imageData.imagePosition, image.CGImage); } } }