CoreText 學(xué)習(xí)筆記(上)

唐巧原博客地址:
基于 CoreText 的排版引擎

CoreText是相對(duì)來(lái)說(shuō)非常底層的框架,在日常的iOS開(kāi)發(fā)過(guò)程中遇到諸如大量文本排版、圖文混合排版或者文本鏈接點(diǎn)擊等情況,選擇用CoreText去做框架底層還是相當(dāng)優(yōu)選的。

這些內(nèi)容在唐巧的博客中都詳細(xì)的給出了,有興趣的朋友可以去唐巧的博客里好好學(xué)習(xí)一下。我這里要寫(xiě)的是,在學(xué)習(xí)唐巧關(guān)于CoreText的文章時(shí)遇到的幾個(gè)問(wèn)題,結(jié)合原作者的文章,做個(gè)自我學(xué)習(xí)總結(jié)。

唐巧關(guān)于CoreText的介紹是循序漸進(jìn)的,先介紹的是純文本的排版,我也從這開(kāi)始,從不一樣的角度去看 CoreText 純文本排版。

CoreText 純文本排版

坐標(biāo)系

在使用CoreText時(shí)需要注意坐標(biāo)系的不同,在CoreText下坐標(biāo)系的原點(diǎn)為視圖的左下角,x軸向右為正方向,y軸向上為正方向。而我們平時(shí)的UIKit坐標(biāo)系原點(diǎn)則是視圖的左上角,x軸向右為正方向,y軸向下為正方向。如圖所示:


所以在確定繪制位置時(shí),要注意坐標(biāo)系的轉(zhuǎn)換,比如下面這個(gè)黑色圓點(diǎn)的位置,在兩個(gè)坐標(biāo)系中是不一樣的

CoreText使用的整體流程

首先,使用CoreText繪制純文本是在UIView中,整個(gè)調(diào)用流程的入口是UIView的 drawRect 方法,每次創(chuàng)建一個(gè)新的UIView系統(tǒng)都會(huì)給你預(yù)先寫(xiě)好的那部分代碼

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
    // Drawing code
}
*/

接下來(lái)就是在 drawRect 方法中實(shí)現(xiàn)繪制的代碼了,總體流程結(jié)構(gòu)如圖:

圖里面總結(jié)了基于 CoreText 的排版引擎原文中的架構(gòu),下面描述一下具體思路:

  • CoreText排版的入口是 drawRect 方法,所有繪制的代碼都要從這里開(kāi)始

  • 首先第一步要在 drawRect 方法中獲取繪制上下文

    CGContextRef context = UIGraphicsGetCurrentContext();
    
  • 第二步要反轉(zhuǎn)坐標(biāo)系

    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    
    • CGContextSetTextMatrix(context, CGAffineTransformIdentity);是初始化文本矩陣 Text Matrix,在繪制之前一定記得初始化文本矩陣 Text Matrix,否則,結(jié)果將是不可預(yù)測(cè)的,就像使用非初始化內(nèi)存一樣

    • CGContextTranslateCTM(context, 0, self.bounds.size.height);向上平移一個(gè)View高度

    • CGContextScaleCTM(context, 1.0, -1.0);將CoreText坐標(biāo)系的 y軸 反轉(zhuǎn)

  • 第三步要在繪制之前要計(jì)算出繪制區(qū)域的總高度,計(jì)算高度可以在下一步創(chuàng)建CTFrame時(shí)根據(jù)其參數(shù) CTFramesetter 獲得

  • 最后第四步要調(diào)用 CTFrameDraw() 函數(shù)進(jìn)行繪制,完整的函數(shù)描述為 CTFrameDraw(CTFrameRef _Nonnull frame, CGContextRef _Nonnull context),共需要兩個(gè)參數(shù):CTFrameCGContext。CGContext 是前面第一步獲取過(guò)的參數(shù),下一步重點(diǎn)要說(shuō)的就是最重要的參數(shù) CTFrame


創(chuàng)建CTFrame

創(chuàng)建 CTFrame 需要兩個(gè)參數(shù):CTFramesetterCGMutablePath。

創(chuàng)建 CTFramesetter 需要富文本字符串(NSAttributedString),這個(gè)富文本字符串可以根據(jù)我們的需求自行創(chuàng)建所需的文本(NSString)和樣式(attributes字典)。

NSDictionary *attributes = @{屬性字典};

NSAttributedString *content = [[NSAttributedString alloc] initWithString:@"要顯示的文本" attributes:attributes];

CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);

這樣 CTFramesetter 就創(chuàng)建好了,接下來(lái)要用 CTFramesetter 計(jì)算出整個(gè)繪制區(qū)域的高度:

// 獲得要繪制的區(qū)域的高度
CGSize restrictSize = CGSizeMake(自定義的寬度, CGFLOAT_MAX);
CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,0), nil, restrictSize, nil);
CGFloat textHeight = coreTextSize.height;

繪制區(qū)域的總高度就是 textHeight

接下來(lái)創(chuàng)建 CGMutablePath,創(chuàng)建 CGMutablePath需要兩個(gè)參數(shù):自定的寬度和計(jì)算好的高度

CGMutablePathRef path = CGPathCreateMutable();

CGPathAddRect(path, NULL, CGRectMake(0, 0, 自定寬度, textHeight));

CGMutablePath也有了,現(xiàn)在可以回頭創(chuàng)建 CTFrame

CTFrameRef ctFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);



有了 CTFrame 后即可以進(jìn)行 CoreText使用的整體流程 中的第四步:調(diào)用 CTFrameDraw() 函數(shù)進(jìn)行繪制。至此繪制純文本的架構(gòu)思路全部介紹完。

CTFrameDraw(ctFrame, context);


問(wèn)題:反轉(zhuǎn)坐標(biāo)系為什么要向上平移一個(gè)View高度?

我畫(huà)了幾張示意圖,來(lái)說(shuō)說(shuō)為什么要平移。

  • 黃色坐標(biāo)表示CoreText坐標(biāo)系
  • 紅色坐標(biāo)表示UIKit坐標(biāo)系
  • 灰色區(qū)域是手機(jī)屏幕
  • 藍(lán)色區(qū)域是自定義的View,就是我們用來(lái)繪制的View
  • 文本 Hello World! 所在的白色區(qū)域正是繪制區(qū)域

首先不反轉(zhuǎn)坐標(biāo)系的時(shí)候,繪制出來(lái)的圖像是倒轉(zhuǎn)的。

然后調(diào)用 CGContextSetTextMatrix(context, CGAffineTransformIdentity);初始化文本矩陣,并且調(diào)用CGContextScaleCTM(context, 1.0, -1.0);將CoreText坐標(biāo)系的 y軸 反轉(zhuǎn),會(huì)得到下面的圖像

可以看到,其實(shí)在反轉(zhuǎn)CoreText坐標(biāo)系的 y軸 后,圖像剛剛好被弄到View外面了,也就是說(shuō)黑色虛線位置就是View,藍(lán)色區(qū)域的實(shí)際圖像我們是看不到的,所以我們一定要把藍(lán)色區(qū)域向上平移一整個(gè)View的高度,才會(huì)回到原位,如下圖



牛刀小試

接下來(lái)根據(jù)上面的思路寫(xiě)一個(gè)小 demo,算是練練手。寫(xiě)這個(gè)demo暫不考慮代碼結(jié)構(gòu)的優(yōu)化,優(yōu)化的代碼結(jié)構(gòu)在基于 CoreText 的排版引擎可以找到。完全是為了快速記憶剛剛提到的那些邏輯,用最簡(jiǎn)單的方式全部回顧一遍。

創(chuàng)建一個(gè)繼承自UIView的類(lèi),用于繪制,取名 GCDisplayView,源代碼如下:

頭文件

//
//  GCDisplayView.h
//
//  Created by 崇 on 2018.
//  Copyright ? 2018 崇. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface GCDisplayView : UIView

@property (nonatomic, assign) CGFloat textHeight;

@end

實(shí)現(xiàn)文件

//
//  GCDisplayView.m
//
//  Created by 崇 on 2018.
//  Copyright ? 2018 崇. All rights reserved.
//

#import "GCDisplayView.h"
#import <CoreText/CoreText.h>

@interface GCDisplayView()

@property (nonatomic, assign) CTFramesetterRef framesetter;
@property (nonatomic, assign) CTFrameRef ctFrame;

@end

@implementation GCDisplayView

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        // 創(chuàng)建 CTFrame
        [self createCTFrame];
    }
    return self;
}

- (void)drawRect:(CGRect)rect {
    
    // 獲取繪制上下文
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 初始化文本矩陣
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    
    // 平移一個(gè)View高度
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    
    // 反轉(zhuǎn) y 軸
    CGContextScaleCTM(context, 1.0, -1.0);
    
    // 繪制
    CTFrameDraw(self.ctFrame, context);
    // 釋放
    CFRelease(self.ctFrame);
    CFRelease(self.framesetter);
}

- (void)createCTFrame {
    
    /*
     創(chuàng)建 CTFrame 需要兩個(gè)參數(shù):CTFramesetter 和 CGMutablePath。
     
     先創(chuàng)建 CTFramesetter,利用 CTFramesetter 計(jì)算出繪制區(qū)域高度后再創(chuàng)建 CGMutablePath。
     
     創(chuàng)建 CTFramesetter 需要先創(chuàng)建 NSAttributedString。
     */
    
    NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init];
    
    NSMutableAttributedString *aStr1 = [[NSMutableAttributedString alloc] initWithString:@"創(chuàng)建 CTFrame 需要兩個(gè)參數(shù):CTFramesetter 和 CGMutablePath。\n" attributes:@{(id)kCTForegroundColorAttributeName : [UIColor redColor]}];
    
    NSMutableAttributedString *aStr2 = [[NSMutableAttributedString alloc] initWithString:@"先創(chuàng)建 CTFramesetter,利用 CTFramesetter 計(jì)算出繪制區(qū)域高度后再創(chuàng)建 CGMutablePath。\n" attributes:@{(id)kCTForegroundColorAttributeName : [UIColor blueColor]}];
    
    NSMutableAttributedString *aStr3 = [[NSMutableAttributedString alloc] initWithString:@"創(chuàng)建 CTFramesetter 需要先創(chuàng)建NSAttributedString." attributes:@{(id)kCTForegroundColorAttributeName : [UIColor blackColor]}];
    
    [attributedString appendAttributedString:aStr1];
    [attributedString appendAttributedString:aStr2];
    [attributedString appendAttributedString:aStr3];
    
    // 用創(chuàng)建好的 attString 創(chuàng)建 framesetter
    self.framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
    
    // 獲得要繪制的區(qū)域的高度
    CGSize restrictSize = CGSizeMake(self.bounds.size.width, CGFLOAT_MAX);
    CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(self.framesetter, CFRangeMake(0,0), nil, restrictSize, nil);
    self.textHeight = coreTextSize.height;
    
    // 創(chuàng)建 CGMutablePath
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, CGRectMake(0, 0, self.bounds.size.width, self.textHeight));
    
    // 創(chuàng)建 ctFrame
    self.ctFrame = CTFramesetterCreateFrame(self.framesetter, CFRangeMake(0, 0), path, NULL);
    
    CFRelease(path);
}

@end

在ViewController的StoryBoard中拖入一個(gè)UIView,讓它繼承自 GCDisplayView

把StoryBoard中的這個(gè)View拖入到ViewController中作為屬性,設(shè)置它的高度

//
//  ViewController.m
//  CoreTextPureText
//
//  Created by 崇 on 2018/11/8.
//  Copyright ? 2018 崇. All rights reserved.
//

#import "ViewController.h"
#import "GCDisplayView.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet GCDisplayView *disView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 設(shè)置高度
    CGRect frame = CGRectMake(self.disView.frame.origin.x, self.disView.frame.origin.y, self.disView.frame.size.width, self.disView.textHeight);
    self.disView.frame = frame;
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}


@end

運(yùn)行結(jié)果如下圖





總結(jié)

如果沒(méi)有看過(guò)唐巧的原文而是先看我這篇文章,那你肯定會(huì)迷糊,因?yàn)槲胰サ袅撕芏嗟募?xì)節(jié),寫(xiě)的都是自己學(xué)習(xí)過(guò)后的心得,這些細(xì)節(jié)我要是搬過(guò)來(lái)就有點(diǎn)不搖碧蓮了,如果想要了解還是請(qǐng)移步 ==> 基于 CoreText 的排版引擎

唐巧的原文中將純文本繪制一直寫(xiě)到支持富文本,而且做了很優(yōu)雅的架構(gòu)設(shè)計(jì),將數(shù)據(jù)源就是源字符串和字體相關(guān)設(shè)置都做成JSON格式的文件,方便批量操作。

基于 CoreText 的排版引擎中寫(xiě)了幾個(gè)輔助類(lèi),主要就是把我寫(xiě)的 demo 中的 - (void)createCTFrame 方法提出去分別實(shí)現(xiàn)。其實(shí)CoreText繪制只需要有一個(gè)CTFrame就足夠了,這個(gè)CTFrame可以在本類(lèi)中實(shí)現(xiàn)和保存,也可以像唐巧一樣提煉出去,做更好的架構(gòu)。CTFrame誰(shuí)都不依賴(比如:drawRect 方法或者 context繪制上下文),而我們需要設(shè)置的所有文本的屬性又都會(huì)包含在CTFrame中,所以CTFrame完全可以拿出去,會(huì)顯得更加靈活。

另外就是要說(shuō)一下 drawRect 方法,當(dāng)時(shí)在看 基于 CoreText 的排版引擎的時(shí)候就有疑問(wèn),那就是代碼的執(zhí)行順序。由于沒(méi)怎么用過(guò) drawRect 所以去查了一下。它的調(diào)用時(shí)機(jī)很晚,對(duì)于本類(lèi)而言 drawRect 的調(diào)用在初始化完成以后,對(duì)于使用這個(gè)View的controller而言 drawRect 在viewDidLoad之后,快要顯示的時(shí)候才會(huì)調(diào)用。所以你大可以放心把 ctFrame 拿出去做各種設(shè)置, drawRect 方法不太可能會(huì)比你的方法先執(zhí)行。如果有對(duì) drawRect 執(zhí)行順序感興趣的朋友,可以到網(wǎng)上搜一搜,一大把有關(guān)的文章。

后面還會(huì)繼續(xù)介紹CoreText圖文混排。

續(xù)

Coming Soon ~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容