CoreText簡介

處理文字和字體的底層技術。它直接和Core Graphics打交道,是iOS和OSX底層的告訴二維圖像渲染引擎。Quartz能夠直接處理字體和字形,將文字渲染到界面上,它是基礎庫中唯一能夠處理字形的模塊。因此CoreText為了排版,需要將顯示的文本內容、位置、字體等資訊傳遞給Quartz。與其他組件相比,具有更高效的排版功能。

UIWebView也可以作為處理復雜的文字排版的備選方案。兩者之間的比較:

1.CoreText佔用內存更少,渲染速度更快;UIWebView佔用內存更多,渲染速度較慢;
2.CoreText在渲染前就可以精確的獲取展示區域的高度(只要有了CTFrame即可);UIWebView只有在加載完成之後才能知道內容的高度(且得利用JavaScript);
3.CoreText的CTFrame可以在子線程渲染;UIWebView只能在主線程渲染;
4.CoreText渲染出得內容不能像UIWebView那樣方便的支持內容的復制;
5.基於CoreText來排版,需要自己處理很多復雜邏輯,包括圖文混排相關邏輯,點擊的操作。

相關概念

CGMutablePathRef --- CoreGraphics 下的CGPath。CGPath CGMutablePath 都定義了畫path的方法,要畫一個Quartz Path 到一個Context:需要通過方法 CGContextAddPath 添加path 到 graphics context,然後調用context的drawing(畫圖)方法。

CTFrameSetterRef --- CTFramesetter 類型用於生成text frames,CTFramesetter是CTFrame對象的對象工廠。CTFramesetter 獲取attributed類型對象和一個形狀描述對象,創建line 對象填充形狀。輸出是一個包含了一個line數組的frame對象,frame 可以直接把自己畫在graphic context。

CTFrameRef --- CoreText的frame,渲染區域;每個CTFrame對象代表著一個段落。這個frame 對象是由framesetter 對象生成的能夠畫整個text frame 到當前的graphic context,這個frame對象包含了多行數組,這些數組能夠檢索單個的渲染和字形資訊。

CTLineRef --- 見下方圖示;
CTRunRef --- 見下方圖示;

在CTFrame內部,是有多個CTLine類組成的,每一個CTLine代表一行,每個CTLine又是由多個CTRun來組成,每一個CTRun代表一組顯示風格一致的文本。我們不用手工管理CTLine和CTRun的創建過程。

CTLineRef和CTRunRef.jpg

Core Text對象運行時層級.png

framesetter 調用一個typesetter對象生成 frame, 在 frame 中放置文本, framesetter將段落樣式應用到這個 frame 上, 包括對齊方式, 製表符, 行間距, 縮進和斷句模式. typesetter 將屬性字元串中的字元轉換成字形, 並將字形填充到文本框的行中

每個 CTFrame 對象包括段落的行對象(CTLine). 每個行對象代表著一行文本. 一個 CTFrame對象可能只包含一行很長的CTLine 對象或者很多行. 在framesetting操作過程中, 會創建行對象. 這些行對象跟 frame 一樣, 可以直接將自己繪制到圖像上下文中.

每個行對象包含一個數組的glyph run(CTRun)對象. 一個glyph run 每一行對象都包含一系列連續不斷的字形,這些字形都包含相同的屬性和方向. typesetter會在從字元產生行時創建glyph run對象. 這就意味著, 一個行對象是由一個或多個glyphs run 構成. glyphs run 可以將自己繪入圖像上下文中, 若非必要時,大多數客戶端不需要直接跟 glyph run 打交道.

相關方法

CFArrayRef CTFrameGetLines(CTFrameRef frame) //獲取包含CTLineRef的數組
void CTFrameGetLineOrigins(CTFrameRef frame,CFRange range,CGPoint origins[])//獲取所有CTLineRef的原點
CFRange CTLineGetStringRange(CTLineRef line) //獲取line中文字在整段文字中的Range
CFArrayRef CTLineGetGlyphRuns(CTLineRef line)//獲取line中包含所有run的數組
CFRange CTRunGetStringRange(CTRunRef run)//獲取run在整段文字中的Range
CFIndex CTLineGetStringIndexForPosition(CTLineRef line,CGPoint position)//獲取點擊處position文字在整段文字中的index
CGFloat CTLineGetOffsetForStringIndex(CTLineRef line,CFIndex charIndex,CGFloat* secondaryOffset)//獲取整段文字中charIndex位置的字元相對line的原點的x值

最簡單的文本渲染

- (void)drawRect:(CGRect)rect {
    // Drawing code
    
    [super drawRect:rect];
    
    //1.創建上下文
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    //2.旋轉坐坐標系(默認和UIKit坐標是相反的)
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    
    //創建繪制區域
    CGMutablePathRef path1 = CGPathCreateMutable();
    //將繪制區域添加到rect中
//    CGPathAddRect(path, NULL, self.bounds);
    //因為坐標系是反的 所以再上面的段落rect需要注意一下
    CGPathAddRect(path1, NULL, CGRectMake(0, self.bounds.size.height/2, self.bounds.size.width, self.bounds.size.height/2));
    
    //設置繪制內容
    NSAttributedString *attString = [[NSAttributedString alloc] initWithString:@"凱文·加內特(Kevin Garnett),1976年5月19日出生在美國南卡羅來納,前美國職業籃球運動員,司職大前鋒/中鋒,綽號狼王(森林狼時期)、KG(名字縮寫)、The BIG TICKET、Da Kid。"
                                     "1995年NBA選秀,凱文·加內特首輪第五順位被明尼蘇達森林狼隊選中,2003-04賽季獲得常規賽MVP。2007年夏季轉會至波士頓凱爾特人,和雷·阿倫和保羅·皮爾斯一起形成了「凱爾特人三巨頭」,2008年的總決賽中擊敗湖人隊,獲得NBA總冠軍。2013年,加內特被交易至布魯克林籃網隊。2015年重回明尼蘇達森林狼隊。"];
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
    //第一個段落
    CTFrameRef frame1 = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [attString length]/2), path1, NULL);
    
    //開始繪制
    CTFrameDraw(frame1, context);
    
    CGMutablePathRef path2 = CGPathCreateMutable();
    CGPathAddRect(path2, NULL, CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height/2));
    //第二個段落
    CTFrameRef frame2 = CTFramesetterCreateFrame(framesetter, CFRangeMake([attString length]/2, [attString length]/2), path2, NULL);
    
    CTFrameDraw(frame2, context);
    
    //釋放資源
    CFRelease(framesetter);
    CFRelease(frame1);
    CFRelease(path1);
    CFRelease(frame2);
    CFRelease(path2);
}


@end

多段落效果圖.jpg

注意,quartz和OSX的坐標系都是右下角,而iOS的坐標系圓點是左上角,所以在iOS中使用時需要旋轉一下坐標系。

//2.旋轉坐坐標系(默認和UIKit坐標是相反的)
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);

步驟解釋:

1.獲得當前繪圖上下文;
2.旋轉坐標系;
3.創建繪制區域,CGMutablePath;
4.將繪制區域添加到Rect中,CGPathAddRect(path, NULL, self.bounds);
5.根據NSAttributedString繪制內容生成一個CTFramesetterRef對象;
6.分別得到兩個CTFrameRef對象,段落一和段落二;
7.繪制,CTFrameDraw(CTFrameRef , CGContextRef);
8.釋放用到的CGMutablePath,CTFramesetterRef,CTFrameRef對象;

這里注意,跟Quartz打交道的類基本都不支持ARC,需要手動釋放。

圖文混排

思路:其實對於圖片的排版,CoreText本身是不支持的,但是可以在需要插入圖片的地方,用一個空白字元代替,同時設置該字元的CTRunDelegate為要顯示圖片的寬高資訊,這樣生成的CTFrame對象就會在繪制時,把圖片的位置預留出來,之後,在drawRect方法中調用CGContextDrawImage方法直接繪制出來進行了。

主要代碼如下,完整代碼請看Demo。

+ (TBZCoreTextData *)parseTemplateFile:(NSString *)path config:(TBZFrameParserConfig *)config{
    NSMutableArray *mArr = [NSMutableArray array];
    NSAttributedString *attString = [self loadTemplateFile:path config:config imageArray:mArr];
    TBZCoreTextData *data = [self parseAttributedContent:attString config:config];
    data.imageArray = mArr;
    return data;
}

//方法二:讀取JSON文件內容,並且調用方法三獲得從NSDcitionay到NSAttributedString的轉換結果
+ (NSAttributedString *)loadTemplateFile:(NSString *)path config:(TBZFrameParserConfig *)config imageArray:(NSMutableArray *)imageArray{
    NSData *data = [NSData dataWithContentsOfFile:path];
    NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
    if (data) {
        
        NSArray *array = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
        
        if ([array isKindOfClass:[NSArray class]]) {
            for (NSDictionary *dict in array) {
                
                NSString *type = dict[@"type"];
                //區分文本和圖片
                if ([type isEqualToString:@"txt"]) {
                    
                    NSAttributedString *as = [self parseAttributeContentFromNSDictionary:dict config:config];
                    [result appendAttributedString:as];
                    
                }else if ([type isEqualToString:@"img"]){
                    
                    //創建TBZCoreImageData,保存圖片到imageArray數組中
                    TBZCoreImageData *imageData = [[TBZCoreImageData alloc] init];
                    //設置圖片的名字字元串;
                    imageData.name = dict[@"name"];
                    //設置圖片的插入位置
                    imageData.position = [result length];
                    [imageArray addObject:imageData];
                    
                    //創建空白佔位符,並且設置它的CTRunDelegate資訊
                    NSAttributedString *as = [self parseImageDataFromNSDictionary:dict config:config];
                    [result appendAttributedString:as];
                }
            }
        }
    }
    return  result;
}

//接受一個NSAttributedString和一個Config參數,將NSAttributedString轉換成CoreTextData返回
+ (TBZCoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(TBZFrameParserConfig *)config{
    
    //創建CTFrameStterRef實例
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
    
    //獲得要繪制的區域的高度
    CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
    CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, [content length]), nil, restrictSize, nil);
    CGFloat textHeight = coreTextSize.height;
    
    //生成CTFrameRef實例
    CTFrameRef frame = [self createFrameWithFramesetter:framesetter config:config height:textHeight];
    
    //將生成好的CTFrameRef實例和計算好的繪制高度保存到CoreTextData實例中,最後返回CoreTextData實例
    TBZCoreTextData *data = [[TBZCoreTextData alloc] init];
    data.ctFrame = frame;
    data.height = textHeight;
    
    //釋放內存
    CFRelease(framesetter);
    CFRelease(frame);
    
    return data;
}

#pragma mark - 添加設置CTRunDelegate資訊的方法
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:(TBZFrameParserConfig *)config{
    
    CTRunDelegateCallbacks callbacks;
    memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
    callbacks.version = kCTRunDelegateVersion1;
    callbacks.getAscent = ascentCallback;
    callbacks.getDescent = descentCallback;
    callbacks.getWidth = widthCallback;
    //將寬高資訊通過delegate返回
    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];
    //將CTRunDelegate對象跟CTAttributedString綁定
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
    CFRelease(delegate);
    return space;
}


//TBZCoreTextData.m

-(void)setImageArray:(NSArray *)imageArray{
    _imageArray = imageArray;
    [self fillImagePosition];
    
}
//填充圖片
-(void)fillImagePosition{
    if (self.imageArray.count==0) {
        return;
    }
    NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
    NSInteger lineCount = [lines count];
    CGPoint lineOrigins[lineCount];
    CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);
    
    int imgIndex = 0;
    TBZCoreImageData *imageData = self.imageArray[0];
    for (int i=0; i<lineCount; i++) {
        if (imageData==nil) {
            break;
        }
        //獲得line對象
        CTLineRef line = (__bridge CTLineRef)lines[i];
        NSArray *runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
        //遍歷該line對象中的run對象
        for (id runObj in runObjArray) {
            CTRunRef run = (__bridge CTRunRef)runObj;
            NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
            CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
            //如果該run對象沒有CTRunDelegate對象,則結束本次循環,繼續下一次循環
            if (delegate == nil) {
                continue;
            }
            //得到CTRunDelegate對象綁定的數據,CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)dict);
            NSDictionary *metaDic = CTRunDelegateGetRefCon(delegate);
            //驗證數據的格式
            if (![metaDic isKindOfClass:[NSDictionary class]]) {
                continue;
            }
            
            //計算圖片的rect
            CGRect runBounds;
            CGFloat ascent;
            CGFloat descent;
            runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
            runBounds.size.height = ascent + descent;
            
            CGFloat x0ffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
            runBounds.origin.x = lineOrigins[i].x + x0ffset;
            runBounds.origin.y = lineOrigins[i].y;
            runBounds.origin.y -= descent;
            
            CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
            CGRect colRect = CGPathGetBoundingBox(pathRef);
            CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
            
            imageData.imagePostion = delegateBounds;
            imgIndex ++;
            if (imgIndex == self.imageArray.count) {
                //所有圖片都處理完 結束遍歷
                imageData = nil;
                break;
            }else{
                imageData = self.imageArray[imgIndex];
            }
        }
    }
}

添加對圖片的點擊支持

需要給展示view添加單擊手勢,判斷手勢觸摸點是否在圖片中,實現該功能。

代碼如下:

- (instancetype)initWithFrame:(CGRect)frame{
    if ([super initWithFrame:frame]) {
        [self addGesture];
    }
    return self;
}

- (void)addGesture{
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGestureRecognizer:)];
    [self addGestureRecognizer:tap];
    self.userInteractionEnabled = YES;
}

- (void)tapGestureRecognizer:(UITapGestureRecognizer *)recognizer{
    CGPoint point = [recognizer locationInView:self];
    
    for (TBZCoreImageData *data in self.textData.imageArray) {
        //翻轉坐標系,因為ImageData中的坐標是CoreText的坐標系
        CGRect imageRect = data.imagePostion;
        CGPoint imagePosition = imageRect.origin;
        imagePosition.y = self.bounds.size.height - imageRect.origin.y - imageRect.size.height;
        CGRect rect = CGRectMake(imagePosition.x, imagePosition.y, imageRect.size.width, imageRect.size.height);
        
        //檢測點擊位置Point是否在rect之內
        if (CGRectContainsPoint(rect, point)) {
            //在這里處理點擊後的邏輯
            [self showTapImage:data];
            break;
        }
    }
}

- (void)showTapImage:(TBZCoreImageData *)data{
    UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
    
    //圖片
    tapImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:data.name]];
    tapImageView.frame = CGRectMake(0, 0, data.imagePostion.size.width, data.imagePostion.size.height);
    tapImageView.center = keyWindow.center;
    
    
    //蒙版
    coverView = [[UIView alloc] initWithFrame:keyWindow.bounds];
    [coverView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(cancel)]];
    coverView.backgroundColor = [UIColor colorWithRed:0/255.0 green:0/255.0 blue:0/255.0 alpha:0.6];
    coverView.userInteractionEnabled = YES;
    
    [keyWindow addSubview:coverView];
    [keyWindow addSubview:tapImageView];
}

- (void)cancel{
    [tapImageView removeFromSuperview];
    [coverView removeFromSuperview];
}

還是得注意坐標系是反的,很關鍵。

Demo下載


Better Late Than Never!
努力是為了當機會來臨時不會錯失機會。
共勉!