CoreText入門(二)-繪製圖片

本文的主要內容是如何使用在CoreText繪製的文本內容中添加圖片的繪製,實現一個簡單的圖文混排。此外,由於圖文的混排複雜度上會比單純的文本繪製高一些,涉及到的CoreText的一些概念的API也會在這篇文章有進行詳細的講解,輔助對代碼的理解。git

其它文章:
CoreText入門(一)-文本繪製
CoreText入門(二)-繪製圖片
CoreText進階(三)-事件處理
CoreText進階(四)-文字行數限制和顯示更多
CoreText進階(五)- 文字排版樣式和效果
CoreText進階(六)-內容大小計算和自動佈局
CoreText進階(七)-添加自定義View和對其數組

本文的主要內容框架

  • CoreText框架中重要的類
    • CTFrame
    • CTLine
    • CTRun
    • CTRunDelegate
  • 繪製圖片
    • 計算圖片位置流程圖
    • 關鍵代碼
  • 一些問題
    • CF對象 vs OC對象
    • 手動釋放內存

Demo:CoreTextDemo佈局

CoreText框架中重要的類

CoreText框架中重要的類示例圖
CoreText框架中重要的類示例圖.net

CTFrame

如上圖中最外層(藍色框)的內容區域對應的就是CTFrame,繪製的是一整段的內容,CTFrame有如下幾個經常使用的方法代理

  • CTFrameGetLines 獲取CTFrame中包含全部的CTLine
  • CTFrameGetLineOrigins 獲取CTFrame中每一行的其實座標,結果保存在返回參數中
  • CTFrameDraw 把CTFrame繪製到CGContext上下文

CTLine

如上圖紅色框中的內容就是CTLine,一共有三個CTLine對象,CTLine有如下幾個經常使用的方法code

  • CTLineGetGlyphRuns 獲取CTLine包含的全部的CTRun
  • CTLineGetOffsetForStringIndex 獲取CTRun的起始位置

CTRun

如上圖綠色框中的內容就是CTRun,每一行中相同格式的一塊內容是一個CTRun,一行中能夠存在多個CTRun,CTRun有如下幾個經常使用的方法orm

  • CTRunGetAttributes 獲取CTRun保存的屬性,獲取到的內容哦是經過CFAttributedStringSetAttribute方法設置給圖片屬性字符串的NSDictionary,key爲kCTRunDelegateAttributeName,值爲CTRunDelegateRef ,更具體的內容查看下面講解
  • CTRunGetTypographicBounds 獲取CTRun的繪製屬性ascentdesent,返回值是CTRun的寬度
  • CTRunGetStringRange 獲取CTRun字符串的Range

CTRunDelegate

CTRunDelegate和CTRun是緊密聯繫的,CTFrame初始化的時候須要用到的圖片信息是經過CTRunDelegate的callback得到到的,更具體的內容查看下面講解,CTRunDelegate有如下幾個經常使用的方法對象

  • CTRunDelegateCreate 建立CTRunDelegate對象,須要傳遞CTRunDelegateCallbacks對象,使用CFAttributedStringSetAttribute方法把CTRunDelegate對象和NSAttributedString對象綁定,在CTFrame初始化的時候回調用CTRunDelegate對象裏面CTRunDelegateCallbacks對象的回調方法返回AscentDescentWidth信息

建立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;
}
  • CTRunDelegateGetRefCon 獲取到CTRunDelegateCreate初始時候設置的元數據,以下代碼中的自動變量metaData
// 2 建立CTRunDelegateRef
    NSDictionary *metaData = @{@"width": @120, @"height": @140};
    CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callback, (__bridge_retained void *)(metaData));

繪製圖片

繪製圖片最重要的一個步驟就是計算圖片所在的位置,最後是在drawRect繪製方法中使用CGContextDrawImage方法進行繪製圖片便可

計算圖片位置流程圖

計算圖片位置流程圖
計算圖片位置流程圖

效果圖

效果圖

關鍵代碼

建立CTRunDelegate對象,傳遞callback和參數的代碼,建立CTFrame對象的時候會經過CTRunDelegatecallbak的幾個回調方法getDescentgetDescentgetWidth返回繪製的圖片的信息,方法getDescentgetDescentgetWidth中的參數是CTRunDelegateCreate(&callback, (__bridge_retained void *)(metaData))方法中的metaData參數,特別地,這裏的參數須要把全部權交給CF對象,而不能使用簡單的橋接,防止ARC模式下的OC對象自動釋放,在方法getDescentgetDescentgetWidth訪問會出現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);
    }
}

一些問題

CF對象 vs OC對象

關於OC對象和CF對象之間的橋接轉換的問題能夠查看這篇文章上的講解 OC對象 vs CF對象

這裏有個主意的地方是建立CTRunDelegateRef對象的時候,這裏的參數須要把全部權交給CF對象,須要使用__bridge_retained,而不能使用簡單的橋接,防止ARC模式下的OC對象自動釋放,在方法getDescentgetDescentgetWidth訪問會出現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;
}
相關文章
相關標籤/搜索