CoreText那些事兒

一乓梨、前言

當(dāng)我們的產(chǎn)品某天站在你身后拍著你的肩膀說:"Hi guys,看看這個效果可以做不遣疯,對雄可,這段文字的首字大寫,文字主體呢用黑色缠犀,而文字中某些詞句可以顯示為紅色..."数苫,你可能會沉思一會,考慮是否用WebView+html或者直接制定一套規(guī)則然后用CoreGraphics去畫辨液,采用WebView+html的方式不失為一個辦法虐急,但是如果需求是TableView中每個Cell都要這么顯示,并且內(nèi)容不定室梅,cell高度可變戏仓,哈哈,光管理每個Cell中WebView的加載和高度計算就夠頭疼了亡鼠,還要考慮內(nèi)存問題赏殃,你懂的,WebView可是個吃內(nèi)存的大戶间涵;那么CoreGraphics+規(guī)則呢仁热?拜托,你是要山寨一個Word嗎勾哩?其實(shí)呢抗蠢,蘋果已經(jīng)為我們提供了一種富文本的解決方案:CoreText!

二思劳、一些概念

本著嚴(yán)謹(jǐn)?shù)闹螌W(xué)態(tài)度迅矛,先來了解一下一些基本概念,這些概念有助于讓我們知道屏幕上出現(xiàn)的文字是如何顯示潜叛,如果覺得只想知道CoreText是怎么用的秽褒,那么可以直接跳過這一章節(jié)壶硅。

我們平時接觸最多的排版系統(tǒng)應(yīng)該就是微軟的Word了吧,作為一名程序員销斟,我習(xí)慣在使用一些軟件的時候思考他們是怎么做的庐椒,對于Word這種軟件,如果我來開發(fā)蚂踊,簡單的方案就是字形+字號+前景色+背景色進(jìn)行圖像繪制(暫時不考慮下劃線约谈、刪除線和陰影等附加效果),比如一行文字犁钟,首先根據(jù)設(shè)置的字體獲得各個字的字形描述棱诱,字形描述可以是一個矩陣,矩陣中每個點(diǎn)有兩個值涝动,代表顯示前景色或背景色军俊,然后根據(jù)設(shè)置的字號按一定的算法縮放矩陣,最后根據(jù)前景色和背景色繪制出這個字的圖像捧存,當(dāng)這一行所有的字的圖像生成好后,那么這一行的高度應(yīng)該是這一行中高度最大的字圖像的高度担败,然后加上行間距昔穴。當(dāng)然現(xiàn)實(shí)情況是,Word比這個復(fù)雜的多的多提前,可以先看一下下面一些概念性的東西吗货,會更好的理解排版系統(tǒng)是怎么工作的(我覺得不管是Word還是CoreText,原理大同小異)狈网。

2.1 文字排版的概念

  • <B>字體(Font):</B>和我們平時說的字體不同宙搬,計算機(jī)意義上的字體表示的是同一大小,同一樣式(Style)字形的集合拓哺。從這個意義上來說勇垛,當(dāng)我們?yōu)槲淖衷O(shè)置粗體,斜體時其實(shí)是使用了另外一種字體(下劃線不算)士鸥。而平時我們所說的字體只是具有相同設(shè)計屬性的字體集合闲孤,即Font Family或typeface。

  • <B>字符(Character)和字形(Glyphs):</B>排版過程中一個重要的步驟就是從字符到字形的轉(zhuǎn)換烤礁,字符表示信息本身讼积,而字形是它的圖形表現(xiàn)形式。字符一般就是指某種編碼脚仔,如Unicode編碼勤众,而字形則是這些編碼對應(yīng)的圖片。但是他們之間不是一一對應(yīng)關(guān)系鲤脏,同個字符的不同字體族们颜,不同字體大小吕朵,不同字體樣式都對應(yīng)了不同的字形。而由于連寫(Ligatures)的存在掌桩,多個字符也會存在對應(yīng)一個字形的情況边锁。

<div style='text-align:center'>
連字

</div>

  • <B>字形描述集(Glyphs Metris):</B>即字形的各個參數(shù)。如下圖所示:

<div style='text-align:center'><img src="
http://7jpr6l.com1.z0.glb.clouddn.com/blog_img_coretext_glyphs1.png" alt="連字" /></div>

<div style='text-align:center'>
連字

</div>

  • <B>邊框(Bounding Box):</B>一個假想的邊框波岛,盡可能地容納整個字形茅坛。

  • <B>基線(Baseline):</B>一條假想的參照線,以此為基礎(chǔ)進(jìn)行字形的渲染则拷。一般來說是一條橫線贡蓖。

  • <B>基礎(chǔ)原點(diǎn)(Origin):</B>基線上最左側(cè)的點(diǎn)。

  • <B>行間距(Leading):</B>行與行之間的間距煌茬。

  • <B>字間距(Kerning):</B>字與字之間的距離斥铺,為了排版的美觀,并不是所有的字形之間的距離都是一致的坛善,但是這個基本步影響到我們的文字排版晾蜘。

  • <B>上行高度(Ascent)和下行高度(Decent):</B>一個字形最高點(diǎn)和最低點(diǎn)到基線的距離,前者為正數(shù)眠屎,而后者為負(fù)數(shù)剔交。當(dāng)同一行內(nèi)有不同字體的文字時,就取最大值作為相應(yīng)的值改衩。這樣岖常,行高LineHeight = Ascent + |Descent| + Leading

2.2 CoreText

CoreText提供了一些低級的API用于富文本排版,它的數(shù)據(jù)源是NSAttributedString葫督。它可以根據(jù)NSAttributedString的定義的每個range的subNSAttributedString的樣式進(jìn)行對字符串的渲染竭鞍。可以這樣說橄镜,這是一個富文本渲染器偎快。

iOS/OSX中用于描述富文本的類是NSAttributedString,顧名思義洽胶,它比NSString多了Attribute的概念滨砍。它可以包含很多屬性,粗體妖异,斜體惋戏,下劃線,顏色他膳,背景色等等响逢,每個屬性都有其對應(yīng)的字符區(qū)域。在OSX上我們只需解析完畢相應(yīng)的數(shù)據(jù)棕孙,準(zhǔn)備好NSAttributedString即可舔亭,底層的繪制完全可以交給相應(yīng)的控件完成些膨。但是在iOS6.0之前就沒有這么方便,想要繪制Attributed String就需要用到CoreText了钦铺,iOS6.0以及以后的版本订雾,UILabel和UITextView提供了一個attributedText屬性,供我們設(shè)置AttributeString矛洞,然后UILabel和UITextView會調(diào)用CoreText進(jìn)行排版洼哎。

2.2.1 CoreText工作流

<div style='text-align:center'>
連字

</div>

使用CoreText進(jìn)行NSAttributedString的繪制,最重要的兩個概念就是CTFrameSetter和CTFrame沼本。

其中CTFramesetter是由CFAttributedString(NSAttributedString)初始化而來噩峦,可以認(rèn)為它是CTFrame的一個Factory,通過傳入CGPath生成相應(yīng)的CTFrame并使用它進(jìn)行渲染:直接以CTFrame為參數(shù)使用CTFrameDraw繪制或者從CTFrame中獲取CTLine進(jìn)行微調(diào)后使用CTLineDraw進(jìn)行繪制抽兆。

一個CTFrame是由一行一行的CLine組成识补,每個CTLine又會包含若干個CTRun(既字形繪制的最小單元),通過相應(yīng)的方法可以獲取到不同位置的CTRun和CTLine辫红,以實(shí)現(xiàn)對不同位置touch事件的響應(yīng)凭涂。CTFrame可以認(rèn)為是一個整體的畫布(Canvas),

2.2.2 Attribute String

使用Attribute String主要有兩種途徑贴妻,一個是使用Foundation框架中的NSAttributeString或NSMutableAttributeString類导盅,另一個則是直接使用較為底層的API創(chuàng)建CFAttributedStringRef變量。我們可以猜測NSAttributeString和NSMutableAttributeString是對底層CFAttributedStringRef的封裝揍瑟。

使用NSAttributeString或NSMutableAttributeString相對簡單,如下:

NSString *string = @"這是一張小圖";
NSDictionary *attr = @{
                           NSFontAttributeName:[UIFont systemFontOfSize:14],
                           NSForegroundColorAttributeName:[UIColor redColor]
                      };
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];

我們可以看到初始化方法中有一個attributes參數(shù)乍炉,類型為NSDictionary绢片,也就是說在設(shè)置一段文字的排版時,Attribute主要是通過該參數(shù)以key-value的形式進(jìn)行設(shè)置岛琼。Foundation框架在iOS6.0之后提供NSString類型的key供我們使用底循,而且6.0之前,則需要使用CFStringRef類型的key槐瑞,我們需要對其進(jìn)行bridge轉(zhuǎn)換轉(zhuǎn)為NSString進(jìn)行使用熙涤。本文寫作時最高iOS版本為9.3,而且當(dāng)前絕大多數(shù)App已經(jīng)不支持iOS6以下甚至iOS7以下的設(shè)備困檩,所以我們著重看一下Foundation提供的NSString類型的Attribute Key祠挫。

NSString類型的Attribute Key有如下可選:

UIKIT_EXTERN NSString * const NSFontAttributeName NS_AVAILABLE(10_0, 6_0);                // 字體,UIFont類型, 默認(rèn)為 Helvetica(Neue) 12

UIKIT_EXTERN NSString * const NSParagraphStyleAttributeName NS_AVAILABLE(10_0, 6_0);      // 段落樣式,值為NSParagraphStyle類型, 默認(rèn)為defaultParagraphStyle

UIKIT_EXTERN NSString * const NSForegroundColorAttributeName NS_AVAILABLE(10_0, 6_0);     // 前景色(文字顏色), UIColor類型悼沿, 默認(rèn)為黑色

UIKIT_EXTERN NSString * const NSBackgroundColorAttributeName NS_AVAILABLE(10_0, 6_0);     // 背景色等舔, UIColor類型,默認(rèn)為nil

UIKIT_EXTERN NSString * const NSLigatureAttributeName NS_AVAILABLE(10_0, 6_0);    // 連體屬性糟趾,取值為NSNumber 對象(整數(shù))慌植,0 表示沒有連體字符甚牲,1 表示使用默認(rèn)的連體字符

UIKIT_EXTERN NSString * const NSKernAttributeName NS_AVAILABLE(10_0, 6_0);                // 字間距,float的NSNumber裝箱

UIKIT_EXTERN NSString * const NSStrikethroughStyleAttributeName NS_AVAILABLE(10_0, 6_0);  // 設(shè)置刪除線蝶柿,取值為 NSNumber 對象(整數(shù))丈钙,枚舉常量

UIKIT_EXTERN NSString * const NSUnderlineStyleAttributeName NS_AVAILABLE(10_0, 6_0);      // 設(shè)置下劃線,取值為 NSNumber 對象(整數(shù))交汤,0為沒有下劃線

UIKIT_EXTERN NSString * const NSStrokeColorAttributeName NS_AVAILABLE(10_0, 6_0);         // 填充部分顏色雏赦,不是字體顏色,取值為 UIColor 對象

UIKIT_EXTERN NSString * const NSStrokeWidthAttributeName NS_AVAILABLE(10_0, 6_0);         // 設(shè)置筆畫寬度蜻展,取值為 NSNumber 對象(整數(shù))喉誊,負(fù)值填充效果,正值中空效果

UIKIT_EXTERN NSString * const NSShadowAttributeName NS_AVAILABLE(10_0, 6_0);              // 設(shè)置陰影屬性纵顾,取值為 NSShadow 對象

UIKIT_EXTERN NSString *const NSTextEffectAttributeName NS_AVAILABLE(10_10, 7_0);          // 設(shè)置文本特殊效果伍茄,取值為 NSString 對象,(圖版印刷效果)

UIKIT_EXTERN NSString * const NSAttachmentAttributeName NS_AVAILABLE(10_0, 7_0);          // 文本附件,取值為NSTextAttachment對象,常用于文字圖片混排

UIKIT_EXTERN NSString * const NSLinkAttributeName NS_AVAILABLE(10_0, 7_0);                // 設(shè)置鏈接屬性施逾,點(diǎn)擊后調(diào)用瀏覽器打開指定URL地址

UIKIT_EXTERN NSString * const NSBaselineOffsetAttributeName NS_AVAILABLE(10_0, 7_0);      // 設(shè)置基線偏移值敷矫,取值為 NSNumber (float),正值上偏,負(fù)值下偏

UIKIT_EXTERN NSString * const NSUnderlineColorAttributeName NS_AVAILABLE(10_0, 7_0);      // 下劃線顏色

UIKIT_EXTERN NSString * const NSStrikethroughColorAttributeName NS_AVAILABLE(10_0, 7_0);  // 刪除線顏色

UIKIT_EXTERN NSString * const NSObliquenessAttributeName NS_AVAILABLE(10_0, 7_0);         // 字形傾斜度汉额,取值為 NSNumber (float),正值右傾曹仗,負(fù)值左傾

UIKIT_EXTERN NSString * const NSExpansionAttributeName NS_AVAILABLE(10_0, 7_0);           // 文本橫向拉伸屬性,取值為 NSNumber (float),正值橫向拉伸文本蠕搜,負(fù)值橫向壓縮文本

UIKIT_EXTERN NSString * const NSWritingDirectionAttributeName NS_AVAILABLE(10_6, 7_0);    // 文字書寫方向

UIKIT_EXTERN NSString * const NSVerticalGlyphFormAttributeName NS_AVAILABLE(10_7, 6_0);   // 文字排版方向怎茫,取值為 NSNumber 對象(整數(shù)),0 表示橫排文本妓灌,1 表示豎排文本(目前iOS總是橫排文本)

三轨蛤、CoreText的使用

對于CoreText使用,對于我們程序員來說虫埂,應(yīng)該更喜歡簡單粗暴地直接上代碼祥山,如果把代碼分開按片段來講解,反而會使思維有斷層掉伏。

創(chuàng)建項目以及新建導(dǎo)航結(jié)構(gòu)不是本文重點(diǎn)缝呕,先略過了,針對CoreText的使用斧散,我簡單的從三個方面來演示:

  1. 簡單文本的文字排版供常,使用CoreText和CoteGraphics API
  2. 圖文混編加支持事件響應(yīng),使用CoreText和CoteGraphics API
  3. 圖文混編加支持事件響應(yīng)鸡捐,使用Foundation和UIKit框架提供的高級API

下面開始對上述內(nèi)容逐一進(jìn)行演示话侧,<font color="red"><b>注意:</b>前方代碼簡陋,沒有重用闯参,更無設(shè)計感瞻鹏,只是盡可能展示用法悲立,當(dāng)然用法也不可能在一篇文章中窮盡,請舉一反三新博!</font>

3.1 簡單文本的文字排版

新建一個類CTBaseView薪夕,內(nèi)容如下(有備注但不多,請自行腦補(bǔ)):

@implementation CTBaseView
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = SHRGB(0xee, 0xee, 0xee);
    }
    return self;
}

- (void)drawRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSaveGState(ctx);
    
    // 1.垂直翻轉(zhuǎn)畫布
    CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
    CGContextTranslateCTM(ctx, 0, self.height);
    CGContextScaleCTM(ctx, 1.0, -1.0);
    
    // 2.創(chuàng)建path
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
    
    
    // 3.創(chuàng)建Attribute String
    NSMutableAttributedString *muAttrString = [[NSMutableAttributedString alloc]init];
    
    // 3.1
    NSString *string = @"H";
    NSDictionary *attr = @{
                           NSFontAttributeName:[UIFont systemFontOfSize:20],
                           NSForegroundColorAttributeName:[UIColor redColor]
                           };
    NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.2
    string = @"ello 復(fù)仇者聯(lián)盟赫悄!\n";
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:14],
             NSForegroundColorAttributeName:[UIColor blueColor]
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.3
    string = @"我們的主頁是:";
    attrString = [[NSAttributedString alloc] initWithString:string];
    [muAttrString appendAttributedString:attrString];
    
    // 3.4
    string = @"http://www.dianping.com\n";
    attr = @{NSBackgroundColorAttributeName:[UIColor yellowColor],
             NSForegroundColorAttributeName:SHRGB(0x72, 0xAC, 0xE3),
             NSUnderlineStyleAttributeName:@(NSUnderlineStyleSingle),
             NSLinkAttributeName:@"http://www.baidu.com"};
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.5
    string = @"《復(fù)仇者聯(lián)盟》(Marvel's The Avengers)是漫威影業(yè)出品的一部科幻動作電影原献,取材自漫威漫畫,是漫威電影宇宙的第六部電影埂淮,同時也是第一階段的收官作品姑隅。由喬斯·韋登執(zhí)導(dǎo),小羅伯特·唐尼倔撞、克里斯·埃文斯讲仰、克里斯·海姆斯沃斯、馬克·魯法洛痪蝇、斯嘉麗·約翰遜鄙陡、杰瑞米·雷納和湯姆·希德勒斯頓聯(lián)袂出演。\n影片講述了神盾局指揮官尼克·弗瑞為了對付《雷神》中被流放的洛基躏啰,積極奔走尋找最強(qiáng)者趁矾,在神盾局斡旋下將鋼鐵俠、美國隊長给僵、雷神托爾毫捣、綠巨人、黑寡婦和鷹眼俠六位超級英雄集結(jié)在一起帝际,組成了復(fù)仇者聯(lián)盟蔓同,共同攜手應(yīng)對邪神洛基。\n影片于2012年5月5日在中國內(nèi)地正式上映胡本。";
    NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc]init];
    style.lineSpacing = 5;
    style.paragraphSpacing = 20;
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:14],
             NSForegroundColorAttributeName:[UIColor orangeColor],
             NSParagraphStyleAttributeName:style
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 4.創(chuàng)建Framesetter和Frame
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)muAttrString);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, muAttrString.length), path, NULL);
    
    // 5.打印Frame中l(wèi)ine和run信息
    NSArray *lines = (NSArray*)CTFrameGetLines(frame);
    NSInteger lineCount = lines.count;
    CGPoint lineOrigin[lineCount];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigin);
    for (NSInteger i=0; i<lineCount; i++) {
        CTLineRef line = (__bridge CTLineRef)lines[i];
        NSArray *runs = (NSArray*)CTLineGetGlyphRuns(line);
        NSInteger runCount = runs.count;
        for (NSInteger j=0; j<runCount; j++) {
            CTRunRef run = (__bridge CTRunRef)runs[j];
            CFRange range = CTRunGetStringRange(run);
            NSLog(@"==> line:%@ - run:%@ [%@ , %@]", @(i),@(j),@(range.location),@(range.length));
        }
    }
    
    CTFrameDraw(frame, ctx);
    CFRelease(frame);
    CFRelease(framesetter);
    CFRelease(path);
    CGContextRestoreGState(ctx);
}
@end

展示效果如下:

<div style='text-align:center'>
連字

</div>

3.2 圖文混編加支持事件響應(yīng)

新建一個類CTMixView,內(nèi)容如下(有備注但不多畸悬,請自行腦補(bǔ)):

static CGFloat ascentCallback(void *ref)
{
    return [[((__bridge NSDictionary*)ref) objectForKey:@"height"] floatValue];
}

static CGFloat descentCallback(void *ref)
{
    return 0;
}

static CGFloat widthCallback(void *ref)
{
    return [[((__bridge NSDictionary*)ref) objectForKey:@"width"] floatValue];
}

@interface CTMixView ()

@property (nonatomic, strong) NSMutableArray *imageRects;

@end

@implementation CTMixView
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = SHRGB(0xee, 0xee, 0xee);
        self.imageRects = [NSMutableArray new];
        UIGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
        [self addGestureRecognizer:gesture];
        self.userInteractionEnabled = YES;
    }
    return self;
}

- (void)drawRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSaveGState(ctx);
    
    // 垂直翻轉(zhuǎn)畫布
    CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
    CGContextTranslateCTM(ctx, 0, self.height);
    CGContextScaleCTM(ctx, 1.0, -1.0);
    
    // 2.創(chuàng)建path
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
    
    // 3.創(chuàng)建Attribute String
    NSMutableAttributedString *muAttrString = [[NSMutableAttributedString alloc]init];
    
    // 3.1
    NSString *string = @"這是一張小圖";
    NSDictionary *attr = @{
                           NSFontAttributeName:[UIFont systemFontOfSize:14],
                           NSForegroundColorAttributeName:[UIColor redColor]
                           };
    NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.2 添加一張小圖
    [self addImageWithWidth:50 height:50 toAttrString:muAttrString];
    
    // 3.3
    string = @"侧甫,這是一張大圖";
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:14],
             NSForegroundColorAttributeName:[UIColor redColor]
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.4 添加一張大圖
    [self addImageWithWidth:200 height:200 toAttrString:muAttrString];
    
    // 3.5
    string = @"后面沒圖了";
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:20],
             NSForegroundColorAttributeName:[UIColor blueColor]
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 4.創(chuàng)建Framesetter和Frame
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)muAttrString);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, muAttrString.length), path, NULL);
    
    
    // 5.獲取添加的圖片的Rect
    NSArray *lines = (NSArray*)CTFrameGetLines(frame);
    NSInteger lineCount = lines.count;
    CGPoint lineOrigin[lineCount];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigin);
    for (NSInteger i=0; i<lineCount; i++) {
        CTLineRef line = (__bridge CTLineRef)lines[i];
        NSArray *runs = (NSArray*)CTLineGetGlyphRuns(line);
        NSInteger runCount = runs.count;
        for (NSInteger j=0; j<runCount; j++) {
            CTRunRef run = (__bridge CTRunRef)runs[j];
            NSDictionary *dicAttr = (NSDictionary*)CTRunGetAttributes(run);
            CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[dicAttr objectForKey:(id)kCTRunDelegateAttributeName];
            if (delegate == nil) {
                continue;
            }
            NSDictionary *metaData = CTRunDelegateGetRefCon(delegate);
            if (metaData == nil) {
                continue;
            }
            CGRect rect;
            CGFloat ascent;
            CGFloat descent;
            rect.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
            rect.size.height = ascent + descent;
            
            CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
            rect.origin.x = lineOrigin[i].x + xOffset;
            rect.origin.y = lineOrigin[i].y - descent;
            
            CGPathRef tempPath = CTFrameGetPath(frame);
            CGRect colRect = CGPathGetBoundingBox(tempPath);
            rect = CGRectOffset(rect, colRect.origin.x, colRect.origin.y);
            NSValue *value = [NSValue valueWithCGRect:rect];
            [self.imageRects addObject:value];
        }
    }
    
    // 6.繪制文本,釋放內(nèi)存
    CTFrameDraw(frame, ctx);
    CFRelease(frame);
    CFRelease(framesetter);
    CFRelease(path);
    
    // 7.繪制圖片
    // 7.1 繪制小圖
    UIImage *image = [UIImage imageNamed:@"50x50"];
    NSValue *value = self.imageRects[0];
    CGRect imgRect = [value CGRectValue];
    CGContextDrawImage(ctx, imgRect, image.CGImage);
    
    // 7.2 繪制大圖
    image = [UIImage imageNamed:@"200x200"];
    value = self.imageRects[1];
    imgRect = [value CGRectValue];
    CGContextDrawImage(ctx, imgRect, image.CGImage);
    
    // 恢復(fù)context
    CGContextRestoreGState(ctx);
}

- (void)addImageWithWidth:(CGFloat)width height:(CGFloat)height toAttrString:(NSMutableAttributedString*)string
{
    CTRunDelegateCallbacks callbacks;
    memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
    callbacks.version = kCTRunDelegateVersion1;
    callbacks.getAscent = ascentCallback;
    callbacks.getWidth = widthCallback;
    callbacks.getDescent = descentCallback;
    CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callbacks, (__bridge void*)@{@"width":@(width),@"height":@(height)});
    
    unichar placeHolder = 0xFFFC;
    NSString *strPlaceHolder = [[NSString alloc] initWithCharacters:&placeHolder length:1];
    NSMutableAttributedString *placeHolerAttr = [[NSMutableAttributedString alloc] initWithString:strPlaceHolder];
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolerAttr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate);
    CFRelease(runDelegate);
    [string appendAttributedString:placeHolerAttr];
}

- (void)onTap:(UIGestureRecognizer*)gesture
{
    CGPoint point = [gesture locationInView:self];
    [self.imageRects enumerateObjectsUsingBlock:^(NSValue *value, NSUInteger idx, BOOL * _Nonnull stop) {
        CGRect rect = value.CGRectValue;
        CGFloat y = self.height - rect.origin.y - rect.size.height;
        CGRect imgRect = CGRectMake(rect.origin.x, y, rect.size.width, rect.size.height);
        if (CGRectContainsPoint(imgRect, point)) {
            NSString *text = idx == 0 ? @"點(diǎn)擊了小圖" : @"點(diǎn)擊了大圖";
            UIAlertView *av = [[UIAlertView alloc]initWithTitle:nil message:text delegate:nil cancelButtonTitle:@"確定" otherButtonTitles:nil];
            [av show];
            *stop = YES;
        }
    }];
}
@end

展示效果如下:

<div style='text-align:center'>
連字

</div>

3.3 圖文混編加支持事件響應(yīng)(高級接口)

新建一個類CTMixView蹋宦,內(nèi)容如下(有備注但不多披粟,請自行腦補(bǔ)):

@interface CTHightLevelMixView ()

@property (nonatomic, strong) NSMutableArray *positons;

@end

@implementation CTHightLevelMixView
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.positons = [NSMutableArray new];
        self.backgroundColor = SHRGB(0xee, 0xee, 0xee);
        
        [self displayContent];
        UIGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
        [self addGestureRecognizer:gesture];
        self.userInteractionEnabled = YES;
    }
    return self;
}

- (void)displayContent
{
    NSMutableAttributedString *muAttrString = [[NSMutableAttributedString alloc]init];
    
    NSString *string = @"這是一張小圖";
    NSDictionary *attr = @{
                           NSFontAttributeName:[UIFont systemFontOfSize:14],
                           NSForegroundColorAttributeName:[UIColor redColor]
                           };
    NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.2 添加一張小圖
    [self addImage:@"50x50" width:50 height:50 toAttrString:muAttrString];
    
    // 3.3
    string = @",這是一張大圖";
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:14],
             NSForegroundColorAttributeName:[UIColor redColor]
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.4 添加一張大圖
    [self addImage:@"200x200" width:200 height:200 toAttrString:muAttrString];
    
    // 3.5
    string = @"后面沒圖了";
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:20],
             NSForegroundColorAttributeName:[UIColor blueColor]
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    self.attributedText = muAttrString;
}

- (void)addImage:(NSString*)imageName width:(CGFloat)width height:(CGFloat)height toAttrString:(NSMutableAttributedString*)attrString
{
    NSTextAttachment *imageAttachment = [[NSTextAttachment alloc]init];
    imageAttachment.image = [UIImage imageNamed:imageName];
    imageAttachment.bounds = CGRectMake(0, 0, width, height);
    
    NSAttributedString *string = [NSAttributedString attributedStringWithAttachment:imageAttachment];
    NSRange range = NSMakeRange(attrString.length, string.length);
    NSValue *rangeValue = [NSValue valueWithRange:range];
    [self.positons addObject:rangeValue];
    [attrString appendAttributedString:string];
}

- (void)onTap:(UIGestureRecognizer*)gesture
{
    CGPoint point = [gesture locationInView:self];
    [self.positons enumerateObjectsUsingBlock:^(NSValue *value, NSUInteger idx, BOOL * _Nonnull stop) {
        NSRange range = [value rangeValue];
        self.selectedRange = range;
        NSArray *rects = [self selectionRectsForRange:self.selectedTextRange];
        self.selectedRange = NSMakeRange(0, 0);
        for (UITextSelectionRect *textRect in rects) {
            CGRect rect = textRect.rect;
            if (CGRectContainsPoint(rect, point)) {
                NSString *text = idx == 0 ? @"點(diǎn)擊了小圖" : @"點(diǎn)擊了大圖";
                UIAlertView *av = [[UIAlertView alloc]initWithTitle:nil message:text delegate:nil cancelButtonTitle:@"確定" otherButtonTitles:nil];
                [av show];
                *stop = YES;
            }
        }
    }];
}
@end

展示效果如下:

<div style='text-align:center'>
連字

</div>

本文Demo下載:Demo源碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末冷冗,一起剝皮案震驚了整個濱河市守屉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蒿辙,老刑警劉巖拇泛,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件滨巴,死亡現(xiàn)場離奇詭異,居然都是意外死亡俺叭,警方通過查閱死者的電腦和手機(jī)恭取,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來熄守,“玉大人蜈垮,你說我怎么就攤上這事≡U眨” “怎么了攒发?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長晋南。 經(jīng)常有香客問我惠猿,道長,這世上最難降的妖魔是什么搬俊? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任紊扬,我火速辦了婚禮,結(jié)果婚禮上唉擂,老公的妹妹穿的比我還像新娘餐屎。我一直安慰自己,他們只是感情好玩祟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布腹缩。 她就那樣靜靜地躺著,像睡著了一般空扎。 火紅的嫁衣襯著肌膚如雪藏鹊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天转锈,我揣著相機(jī)與錄音盘寡,去河邊找鬼。 笑死撮慨,一個胖子當(dāng)著我的面吹牛竿痰,可吹牛的內(nèi)容都是我干的企锌。 我是一名探鬼主播瓦侮,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼喘落!你這毒婦竟也來了规伐?” 一聲冷哼從身側(cè)響起蟹倾,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鲜棠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肌厨,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年岔留,在試婚紗的時候發(fā)現(xiàn)自己被綠了夏哭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡献联,死狀恐怖竖配,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情里逆,我是刑警寧澤进胯,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站原押,受9級特大地震影響胁镐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜诸衔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一盯漂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧笨农,春花似錦就缆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至份招,卻和暖如春切揭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背锁摔。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工廓旬, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谐腰。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓孕豹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親怔蚌。 傳聞我的和親對象是個殘疾皇子巩步,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,689評論 2 354

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

  • CoreText是一個進(jìn)階的比較底層的布局文本和處理字體的技術(shù)旁赊,CoreText API在OS X v10.5 和...
    smalldu閱讀 13,443評論 18 129
  • 1.iOS中的round桦踊、ceil、floor函數(shù)略解 round如果參數(shù)是小數(shù)终畅,則求本身的四舍五入.ceil如果...
    K_Gopher閱讀 1,184評論 1 0
  • 文字排版的基礎(chǔ)概念 字體(Font):和我們平時說的字體不同籍胯,計算機(jī)意義上的字體表示的是同一大小竟闪,同一樣式(Sty...
    iOS白水閱讀 673評論 0 0
  • 健身器材銷售產(chǎn)品演示做的好,可以給顧客留下深刻的印象炼蛤。 銷售員:"先生,您平時有沒有運(yùn)動的習(xí)慣?" 顧客:"沒有時...
    八百誠閱讀 495評論 0 0
  • 將軍 又喝高了 他重復(fù)他的自由 大家們的‘遺留’是他舉起的大劍 語言是新鮮傷口上不斷冒出的血漿 粘稠 連那種紅色也...
    能有誰比我知道閱讀 225評論 0 0