IOS CoreText --- 圖文混排之代碼封裝
上一節中,我詳細的講解了用物件導向的思想將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;}
5. 在loadTemplateFile方法添加支援image的代碼, 這樣,就將plist中img的相關資訊儲存到CoreTextImageData模型中了。
但是問題來了,Core Text本身並不支援對圖片的展示功能!但是,我們可以在要顯示文本的地方,用一個特殊的空白字元代替,同時設定該字型的CTRunDelegate資訊為要顯示的圖片的寬度和高度,這樣最後產生的CTFrame執行個體,就會在繪製時將圖片的位置預留下來。因為CTDisplayView的繪製代碼是在drawRect裡面的,所以我們可以方便的把需要繪製的圖片,用Quartz 2D的CGContextDrawImage方法直接繪製出來就行了。我這裡所描述的流程,就是在調用的parseImageDataFromNSDictionary中實現的。
+ (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;}
7. 在5,6 兩點的代碼執行完畢後,代碼會返回到第4點,執行下面這句代碼:
data.imageArray = imageArray;
它實際上就是重寫了CoreTextData中的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]; } } }}
8. 改造CTDisplayView中的代碼,完成繪製工作。
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); } }}