CoreText 學習筆記(上)

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

CoreText是相對來說非常底層的框架蘸际,在日常的iOS開發(fā)過程中遇到諸如大量文本排版绳匀、圖文混合排版或者文本鏈接點擊等情況询枚,選擇用CoreText去做框架底層還是相當優(yōu)選的台舱。

這些內(nèi)容在唐巧的博客中都詳細的給出了知市,有興趣的朋友可以去唐巧的博客里好好學習一下。我這里要寫的是活孩,在學習唐巧關于CoreText的文章時遇到的幾個問題物遇,結(jié)合原作者的文章,做個自我學習總結(jié)憾儒。

唐巧關于CoreText的介紹是循序漸進的询兴,先介紹的是純文本的排版,我也從這開始起趾,從不一樣的角度去看 CoreText 純文本排版诗舰。

CoreText 純文本排版

坐標系

在使用CoreText時需要注意坐標系的不同,在CoreText下坐標系的原點為視圖的左下角训裆,x軸向右為正方向眶根,y軸向上為正方向蜀铲。而我們平時的UIKit坐標系原點則是視圖的左上角,x軸向右為正方向属百,y軸向下為正方向记劝。如圖所示:


所以在確定繪制位置時,要注意坐標系的轉(zhuǎn)換族扰,比如下面這個黑色圓點的位置厌丑,在兩個坐標系中是不一樣的

CoreText使用的整體流程

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

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

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

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

  • CoreText排版的入口是 drawRect 方法耕驰,所有繪制的代碼都要從這里開始

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

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

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

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

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

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

  • 最后第四步要調(diào)用 CTFrameDraw() 函數(shù)進行繪制厚骗,完整的函數(shù)描述為 CTFrameDraw(CTFrameRef _Nonnull frame, CGContextRef _Nonnull context)示启,共需要兩個參數(shù):CTFrameCGContext兢哭。CGContext 是前面第一步獲取過的參數(shù),下一步重點要說的就是最重要的參數(shù) CTFrame


創(chuàng)建CTFrame

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

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

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

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

CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);

這樣 CTFramesetter 就創(chuàng)建好了舍咖,接下來要用 CTFramesetter 計算出整個繪制區(qū)域的高度:

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

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

接下來創(chuàng)建 CGMutablePath矩父,創(chuàng)建 CGMutablePath需要兩個參數(shù):自定的寬度和計算好的高度

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 后即可以進行 CoreText使用的整體流程 中的第四步:調(diào)用 CTFrameDraw() 函數(shù)進行繪制排霉。至此繪制純文本的架構思路全部介紹完窍株。

CTFrameDraw(ctFrame, context);


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

我畫了幾張示意圖攻柠,來說說為什么要平移球订。

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

首先不反轉(zhuǎn)坐標系的時候冒滩,繪制出來的圖像是倒轉(zhuǎn)的。

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

可以看到因苹,其實在反轉(zhuǎn)CoreText坐標系的 y軸 后,圖像剛剛好被弄到View外面了篇恒,也就是說黑色虛線位置就是View扶檐,藍色區(qū)域的實際圖像我們是看不到的,所以我們一定要把藍色區(qū)域向上平移一整個View的高度婚度,才會回到原位蘸秘,如下圖



牛刀小試

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

創(chuàng)建一個繼承自UIView的類,用于繪制饭寺,取名 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

實現(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);
    
    // 平移一個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 需要兩個參數(shù):CTFramesetter 和 CGMutablePath。
     
     先創(chuàng)建 CTFramesetter艰匙,利用 CTFramesetter 計算出繪制區(qū)域高度后再創(chuàng)建 CGMutablePath限煞。
     
     創(chuàng)建 CTFramesetter 需要先創(chuàng)建 NSAttributedString。
     */
    
    NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init];
    
    NSMutableAttributedString *aStr1 = [[NSMutableAttributedString alloc] initWithString:@"創(chuàng)建 CTFrame 需要兩個參數(shù):CTFramesetter 和 CGMutablePath员凝。\n" attributes:@{(id)kCTForegroundColorAttributeName : [UIColor redColor]}];
    
    NSMutableAttributedString *aStr2 = [[NSMutableAttributedString alloc] initWithString:@"先創(chuàng)建 CTFramesetter署驻,利用 CTFramesetter 計算出繪制區(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中拖入一個UIView健霹,讓它繼承自 GCDisplayView

把StoryBoard中的這個View拖入到ViewController中作為屬性旺上,設置它的高度

//
//  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];
    // 設置高度
    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

運行結(jié)果如下圖





總結(jié)

如果沒有看過唐巧的原文而是先看我這篇文章,那你肯定會迷糊糖埋,因為我去掉了很多的細節(jié)宣吱,寫的都是自己學習過后的心得,這些細節(jié)我要是搬過來就有點不搖碧蓮了瞳别,如果想要了解還是請移步 ==> 基于 CoreText 的排版引擎

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

基于 CoreText 的排版引擎中寫了幾個輔助類,主要就是把我寫的 demo 中的 - (void)createCTFrame 方法提出去分別實現(xiàn)垒棋。其實CoreText繪制只需要有一個CTFrame就足夠了卒煞,這個CTFrame可以在本類中實現(xiàn)和保存,也可以像唐巧一樣提煉出去叼架,做更好的架構畔裕。CTFrame誰都不依賴(比如:drawRect 方法或者 context繪制上下文)斩熊,而我們需要設置的所有文本的屬性又都會包含在CTFrame中聂受,所以CTFrame完全可以拿出去斥难,會顯得更加靈活贞让。

另外就是要說一下 drawRect 方法,當時在看 基于 CoreText 的排版引擎的時候就有疑問甜无,那就是代碼的執(zhí)行順序扛点。由于沒怎么用過 drawRect 所以去查了一下。它的調(diào)用時機很晚岂丘,對于本類而言 drawRect 的調(diào)用在初始化完成以后陵究,對于使用這個View的controller而言 drawRect 在viewDidLoad之后,快要顯示的時候才會調(diào)用奥帘。所以你大可以放心把 ctFrame 拿出去做各種設置铜邮, drawRect 方法不太可能會比你的方法先執(zhí)行。如果有對 drawRect 執(zhí)行順序感興趣的朋友寨蹋,可以到網(wǎng)上搜一搜松蒜,一大把有關的文章。

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

續(xù)

Coming Soon ~~~

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末秸苗,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子运褪,更是在濱河造成了極大的恐慌惊楼,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吐句,死亡現(xiàn)場離奇詭異胁后,居然都是意外死亡店读,警方通過查閱死者的電腦和手機嗦枢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屯断,“玉大人文虏,你說我怎么就攤上這事≈逞荩” “怎么了氧秘?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長趴久。 經(jīng)常有香客問我丸相,道長,這世上最難降的妖魔是什么彼棍? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任灭忠,我火速辦了婚禮膳算,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘弛作。我一直安慰自己涕蜂,他們只是感情好,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布映琳。 她就那樣靜靜地躺著机隙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪萨西。 梳的紋絲不亂的頭發(fā)上有鹿,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機與錄音谎脯,去河邊找鬼印颤。 笑死,一個胖子當著我的面吹牛穿肄,可吹牛的內(nèi)容都是我干的年局。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼咸产,長吁一口氣:“原來是場噩夢啊……” “哼矢否!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起脑溢,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤僵朗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后屑彻,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體验庙,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年社牲,在試婚紗的時候發(fā)現(xiàn)自己被綠了粪薛。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡搏恤,死狀恐怖违寿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情熟空,我是刑警寧澤藤巢,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站息罗,受9級特大地震影響掂咒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一绍刮、第九天 我趴在偏房一處隱蔽的房頂上張望糜工。 院中可真熱鬧,春花似錦录淡、人聲如沸捌木。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽刨裆。三九已至,卻和暖如春彬檀,著一層夾襖步出監(jiān)牢的瞬間帆啃,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工窍帝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留努潘,地道東北人。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓坤学,卻偏偏與公主長得像疯坤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子深浮,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容