本文的主要內容是如何使用在CoreText繪製的文本內容中添加圖片的繪製,實現一個簡單的圖文混排。此外,由於圖文的混排複雜度上會比單純的文本繪製高一些,涉及到的CoreText的一些概念的API也會在這篇文章有進行詳細的講解,輔助對代碼的理解。git
其它文章:
CoreText入門(一)-文本繪製
CoreText入門(二)-繪製圖片
CoreText進階(三)-事件處理
CoreText進階(四)-文字行數限制和顯示更多
CoreText進階(五)- 文字排版樣式和效果
CoreText進階(六)-內容大小計算和自動佈局
CoreText進階(七)-添加自定義View和對其數組
本文的主要內容框架
Demo:CoreTextDemo佈局
CoreText框架中重要的類示例圖
 .net
如上圖中最外層(藍色框)的內容區域對應的就是CTFrame,繪製的是一整段的內容,CTFrame有如下幾個經常使用的方法代理
CGContext
上下文如上圖紅色框中的內容就是CTLine,一共有三個CTLine對象,CTLine有如下幾個經常使用的方法code
如上圖綠色框中的內容就是CTRun,每一行中相同格式的一塊內容是一個CTRun,一行中能夠存在多個CTRun,CTRun有如下幾個經常使用的方法orm
CFAttributedStringSetAttribute
方法設置給圖片屬性字符串的NSDictionary,key爲kCTRunDelegateAttributeName
,值爲CTRunDelegateRef
,更具體的內容查看下面講解ascent
、desent
,返回值是CTRun的寬度CTRunDelegate和CTRun是緊密聯繫的,CTFrame初始化的時候須要用到的圖片信息是經過CTRunDelegate的callback得到到的,更具體的內容查看下面講解,CTRunDelegate有如下幾個經常使用的方法對象
Ascent
、Descent
、Width
信息建立CTRunDelegate對象,傳遞callback和參數的代碼:blog
- (NSAttributedString *)imageAttributeString { // 1 建立CTRunDelegateCallbacks CTRunDelegateCallbacks callback; memset(&callback, 0, sizeof(CTRunDelegateCallbacks)); callback.getAscent = getAscent; callback.getDescent = getDescent; callback.getWidth = getWidth; // 2 建立CTRunDelegateRef NSDictionary *metaData = @{@"width": @120, @"height": @140}; CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callback, (__bridge_retained void *)(metaData)); // 3 設置佔位使用的圖片屬性字符串 // 參考:https://en.wikipedia.org/wiki/Specials_(Unicode_block) U+FFFC OBJECT REPLACEMENT CHARACTER, placeholder in the text for another unspecified object, for example in a compound document. unichar objectReplacementChar = 0xFFFC; NSMutableAttributedString *imagePlaceHolderAttributeString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithCharacters:&objectReplacementChar length:1] attributes:[self defaultTextAttributes]]; // 4 設置RunDelegate代理 CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imagePlaceHolderAttributeString, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate); CFRelease(runDelegate); return imagePlaceHolderAttributeString; }
metaData
// 2 建立CTRunDelegateRef NSDictionary *metaData = @{@"width": @120, @"height": @140}; CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callback, (__bridge_retained void *)(metaData));
繪製圖片最重要的一個步驟就是計算圖片所在的位置,最後是在drawRect
繪製方法中使用CGContextDrawImage
方法進行繪製圖片便可
計算圖片位置流程圖

效果圖

建立CTRunDelegate對象,傳遞callback和參數的代碼,建立CTFrame對象的時候會經過CTRunDelegate
中callbak
的幾個回調方法getDescent
、getDescent
、getWidth
返回繪製的圖片的信息,方法getDescent
、getDescent
、getWidth
中的參數是CTRunDelegateCreate(&callback, (__bridge_retained void *)(metaData))
方法中的metaData
參數,特別地,這裏的參數須要把全部權交給CF對象,而不能使用簡單的橋接,防止ARC模式下的OC對象自動釋放,在方法getDescent
、getDescent
、getWidth
訪問會出現BAD_ACCESS的錯誤
- (NSAttributedString *)imageAttributeString { // 1 建立CTRunDelegateCallbacks CTRunDelegateCallbacks callback; memset(&callback, 0, sizeof(CTRunDelegateCallbacks)); callback.getAscent = getAscent; callback.getDescent = getDescent; callback.getWidth = getWidth; // 2 建立CTRunDelegateRef NSDictionary *metaData = @{@"width": @120, @"height": @140}; CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callback, (__bridge_retained void *)(metaData)); // 3 設置佔位使用的圖片屬性字符串 // 參考:https://en.wikipedia.org/wiki/Specials_(Unicode_block) U+FFFC OBJECT REPLACEMENT CHARACTER, placeholder in the text for another unspecified object, for example in a compound document. unichar objectReplacementChar = 0xFFFC; NSMutableAttributedString *imagePlaceHolderAttributeString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithCharacters:&objectReplacementChar length:1] attributes:[self defaultTextAttributes]]; // 4 設置RunDelegate代理 CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imagePlaceHolderAttributeString, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate); CFRelease(runDelegate); return imagePlaceHolderAttributeString; } // MARK: - CTRunDelegateCallbacks 回調方法 static CGFloat getAscent(void *ref) { float height = [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"height"] floatValue]; return height; } static CGFloat getDescent(void *ref) { return 0; } static CGFloat getWidth(void *ref) { float width = [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"width"] floatValue]; return width; }
計算圖片所在的位置的代碼:
- (void)calculateImagePosition { int imageIndex = 0; if (imageIndex >= self.richTextData.images.count) { return; } // CTFrameGetLines獲取但CTFrame內容的行數 NSArray *lines = (NSArray *)CTFrameGetLines(self.richTextData.ctFrame); // CTFrameGetLineOrigins獲取每一行的起始點,保存在lineOrigins數組中 CGPoint lineOrigins[lines.count]; CTFrameGetLineOrigins(self.richTextData.ctFrame, CFRangeMake(0, 0), lineOrigins); for (int i = 0; i < lines.count; i++) { CTLineRef line = (__bridge CTLineRef)lines[i]; NSArray *runs = (NSArray *)CTLineGetGlyphRuns(line); for (int j = 0; j < runs.count; j++) { CTRunRef run = (__bridge CTRunRef)(runs[j]); NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes(run); if (!attributes) { continue; } // 從屬性中獲取到建立屬性字符串使用CFAttributedStringSetAttribute設置的delegate值 CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName]; if (!delegate) { continue; } // CTRunDelegateGetRefCon方法從delegate中獲取使用CTRunDelegateCreate初始時候設置的元數據 NSDictionary *metaData = (NSDictionary *)CTRunDelegateGetRefCon(delegate); if (!metaData) { continue; } // 找到代理則開始計算圖片位置信息 CGFloat ascent; CGFloat desent; // 能夠直接從metaData獲取到圖片的寬度和高度信息 CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &desent, NULL); // CTLineGetOffsetForStringIndex獲取CTRun的起始位置 CGFloat xOffset = lineOrigins[i].x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL); CGFloat yOffset = lineOrigins[i].y; // 更新ImageItem對象的位置 ImageItem *imageItem = self.richTextData.images[imageIndex]; imageItem.frame = CGRectMake(xOffset, yOffset, width, ascent + desent); imageIndex ++; if (imageIndex >= self.richTextData.images.count) { return; } } } }
繪製圖片的代碼,使用CGContextDrawImage
方法繪製便可,圖片的位置信息就是上一步的代碼所得到的
- (void)drawRect:(CGRect)rect { [super drawRect:rect]; CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM(context, 1, -1); // 使用CTFrame在CGContextRef上下文上繪製 CTFrameDraw(self.data.ctFrame, context); // 在CGContextRef上下文上繪製圖片 for (int i = 0; i < self.data.images.count; i++) { ImageItem *imageItem = self.data.images[i]; CGContextDrawImage(context, imageItem.frame, [UIImage imageNamed:imageItem.imageName].CGImage); } }
關於OC對象和CF對象之間的橋接轉換的問題能夠查看這篇文章上的講解 OC對象 vs CF對象
這裏有個主意的地方是建立CTRunDelegateRef
對象的時候,這裏的參數須要把全部權交給CF對象,須要使用__bridge_retained
,而不能使用簡單的橋接,防止ARC模式下的OC對象自動釋放,在方法getDescent
、getDescent
、getWidth
訪問會出現BAD_ACCESS的錯誤
// 2 建立CTRunDelegateRef NSDictionary *metaData = @{@"width": @120, @"height": @140}; CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callback, (__bridge_retained void *)(metaData));
由於CoreText是屬於CF的,須要手動管理內存,好比下面建立的臨時變量須要使用CFRelease
及時釋放內存,不然會有內存溢出的問題
- (CTFrameRef)ctFrameWithAttributeString:(NSAttributedString *)attributeString frame:(CGRect)frame { // 繪製區域 CGMutablePathRef path = CGPathCreateMutable(); CGPathAddRect(path, NULL, (CGRect){{0, 0}, frame.size}); // 使用NSMutableAttributedString建立CTFrame CTFramesetterRef ctFramesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeString); CTFrameRef ctFrame = CTFramesetterCreateFrame(ctFramesetter, CFRangeMake(0, attributeString.length), path, NULL); CFRelease(ctFramesetter); CFRelease(path); return ctFrame; }