文字排版—— 排版基礎(chǔ)现横、CoreText和圖文混排

一、排版概念

1阁最、Characters and Glyphs(字符和字形)

字符是文字的最小單元戒祠,以這段文字為例,每個字都是一個字符速种;需要注意姜盈,字符是一個抽象的概念;
當(dāng)文字真正繪制出來時需要選擇字體配阵,以“A”這個字母為例馏颂,當(dāng)字母'A'印刷出來或者顯示到屏幕,可能有多種字體闸餐,每種字體都有一種字形'A':

但是饱亮,字符和字形不是一一對應(yīng),也不是一對多的關(guān)系舍沙!
在某些字體中近上,相同的字符可能會包括多個的字形:
“é” = “e” + “′” (一個字符由兩個字形組合而成)
一個字形,也可以容納多個字符拂铡,如下:(右邊的字形是連寫ff壹无,包括兩個字符f)

上圖的連字符是一種上下文相關(guān)的字形葱绒,一個字符的字形由受到下一個字符的影響。

2斗锭、字型(Typefaces)和字體(Fonts)

Typeface指一系列風(fēng)格接近的字體地淀,而Font是一系列具有一致大小、樣式的字形組成的字體岖是;通常多個字體會組成一個字型帮毁,如圖:

這是多個字體組成的字型(字體族)

3、字體屬性

字體屬性指的是字符的字形大小和布局豺撑。同一字體中的字符屬性大致相同烈疚,常用屬性包括:baseline(字符基線)、ascent(字形最高點(diǎn)和baseline的距離)聪轿、descent(字形最低點(diǎn)和baseline的距離)爷肝、leading(行間距)等。

字符屬性的詳細(xì)介紹:
text direction:文字的排版順序陆错,像English是從左上角開始灯抛,從左到右;也有文字的排版是從右到左或者是從上到下的排版等音瓷;

line breaking:在字符串中找到一個點(diǎn)对嚼,截取出一段文本用于顯示一行;
baseline:所有字形的虛擬基準(zhǔn)線外莲,如下圖藍(lán)色部分:(也會有部分字形跨過基準(zhǔn)線猪半,比如說g)

left-side bearing:如圖,是字符之間默認(rèn)的間隙偷线;(同理還有right-side bearing
descent/ascent:字形的上下部分;
bounding rectangle:字形的可見部分沽甥;
kerning:文字默認(rèn)排版時声邦,寬度由advance width指定,默認(rèn)會留有一小部分間隔摆舟;也可以通過設(shè)置字間距(kerning)亥曹,手動調(diào)整字形之間的距離。

point size:ascent+descent就是字體的pointSize恨诱;
leading:兩行字形之間的距離媳瞪;
line height:行高,ascent+descent+leading=line height照宝;
margins:文字和邊界的距離蛇受;
Alignment:多行文字的對齊方式,常見有下面三種:

另外一種同樣常見的排版方式是兩端對齊(justified):

綜上厕鹃,常見的排版概念的分布如下:

二兢仰、NSAttributeString

在介紹NSAttributeString之前乍丈,我們先了解一個概念——Text Attributes(文本屬性),常見的屬性有:

  • character attributes:字體把将、顏色等具體到某個字符的屬性轻专,通常會鍵值對的方式存在NSDictionary中,例如@{NSFontAttributeName:[UIColor redColor]}察蹲;
  • temporary attributes:對于某種排版的臨時字符屬性请垛,不會持久化,比如說跨行的連字符'-'洽议;
  • paragraph attributes:行間距宗收、段間距、邊界margin等段落屬性绞铃,段落屬性會影響多行文本的排版镜雨,具體屬性可以見NSParagraphStyle;
  • glyph attributes:排版引擎渲染時的加粗等字形屬性儿捧,通常是一個integer值荚坞,代表字符在排版引擎中的具體使用值(開發(fā)者通常不需要關(guān)心);
  • document attributes:整個文檔(字符串)的屬性菲盾,例如說我們常用的屬性@{NSDocumentTypeDocumentAttribute:NSHTMLTextDocumentType}颓影,說明該字符是HTML的格式,類似的還有:
 NSAttributedStringDocumentType const NSPlainTextDocumentType;
 NSAttributedStringDocumentType const NSRTFTextDocumentType;
 NSAttributedStringDocumentType const NSRTFDTextDocumentType;
 NSAttributedStringDocumentType const NSHTMLTextDocumentType;

NSAttributeString就是上述屬性的結(jié)合體懒鉴,描述一段字符串的屬性诡挂。
如下圖,這里描述的是針對This is a這段字符的字體顏色和字體大小屬性:

NSAttributeString并不是NSString的子類临谱,但是卻有一個.string屬性可以獲取到富文本對應(yīng)的string璃俗;
那么,為什么NSAttributeString不做成NSString的子類悉默?
首先從字面意思來看城豁,NSAttributeString很容易就被當(dāng)成NSString的子類,導(dǎo)致會寫出NSAttributeString==NSString的這類判斷抄课,將NSAttributeString不寫成NSString子類可以避免這一類問題唱星;
更為重要的是,NSAttributeString是描述string的各種屬性跟磨,而NSString是單純的字符串间聊。

NSAttributeString的屬性在生成之后便無法修改,如果需要修改某些屬性抵拘,則需要使用NSMutableAttributeString哎榴。
NSAttributeString有兩種方法可以讀取對應(yīng)某個字符的屬性:

attributesAtIndex:effectiveRange:
attributesAtIndex:longestEffectiveRange:inRange:

為什么需要有兩種方法?
attribute是針對一段字符的屬性,而一個字符串往往會有多個attribute叹话;
如果把字符串中的每個字符看成一維數(shù)軸上的點(diǎn)偷遗,那么attribute就是一個點(diǎn)或者兩個點(diǎn)之間的線段,NSAttributeString由許許多多的線段組成驼壶;
當(dāng)我們獲取某個點(diǎn)的屬性氏豌,實(shí)際上就是詢問所有的線段中,經(jīng)過這個點(diǎn)的線段(屬性)有哪些热凹。
所以為了優(yōu)化速度泵喘,可以通過指定effectiveRange,縮小遍歷的范圍般妙。如果想知道該屬性的最大覆蓋范圍纪铺,則使用帶longestEffectiveRange的方法,但是需要手動設(shè)置遍歷range,否則會遍歷整個字符串的屬性。

如下狈定,4個點(diǎn)代表4個字符,一個紅色的線段表示一個(0, 3)的屬性芜繁,藍(lán)色的線段表示(1, 3)的屬性;
當(dāng)我們獲取第2個點(diǎn)的屬性時绒极,因?yàn)榧t色和藍(lán)色線段都經(jīng)過第2個點(diǎn)骏令,所以會返回兩個屬性;
當(dāng)我們獲取第1個點(diǎn)的屬性時垄提,只有紅色的線段經(jīng)過第1個點(diǎn)榔袋,則只會返回一個屬性;

最后注意铡俐,當(dāng)Attribute在賦值給NSAttributeString之后不應(yīng)該修改凰兑,比如說下面的段屬性設(shè)置,當(dāng)我們把這個dict賦值給NSAttributeString之后审丘,就不應(yīng)該修改paragraphStyle的值(否則可能會發(fā)生未知的問題)聪黎。

   NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
    paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
    paragraphStyle.lineSpacing = 1.0;
    paragraphStyle.alignment = NSTextAlignmentJustified;
    dict[NSParagraphStyleAttributeName] = paragraphStyle;

三、CoreText

在了解排版概念和NSAttributeString后备恤,再來看CoreText。
CoreText是一個高效處理字符和字形轉(zhuǎn)換和進(jìn)行文字排版的框架锦秒,API基于C語言露泊。
當(dāng)我們需要排版時,可以對字符串設(shè)置各種格式旅择,生成NSAttributeString惭笑;
然后用NSAttributeString去創(chuàng)建CTFramesetter類,CTFramesetter會處理排版信息,然后生成排版后的結(jié)果CTFrame沉噩;
CTFrame是一段或者多段文本捺宗,每段文本又由多行文字組成,每行的表示為CTLine川蒙;
CTLine是一行文本蚜厉,每行文本由多個CTRun組成,CTRun是一小段連續(xù)的字形畜眨;
CTTypeSetter負(fù)責(zé)上下文相關(guān)排版處理昼牛,比如說換行,每個CTFrame中都會有一個CTTypeSetter康聂;
他們之間的關(guān)系圖如下:

總的來說贰健,CTFramesetter是生成CTFrame的工廠類,初始化參數(shù)是attributed string恬汁,會在內(nèi)部創(chuàng)建CTTypesetter并進(jìn)行實(shí)際的排版伶椿;CTLine類似每一行的文字,CTRun是一行中具有相同屬性的連續(xù)字形氓侧,比如說“我正在分享閱讀器”脊另,就會由三個CTRun組成,分別是“我正在”甘苍、“分享”尝蠕、“閱讀器”(因?yàn)椤胺窒怼眱蓚€字加粗了,否則就會是一個CTRun)载庭。

1看彼、CTFont

CTFontRef是CoreText的字體,可以讀取字體的版權(quán)信息(copyright)囚聚、fontFamily靖榕、style等信息;
CTFontCreateWithName()用于創(chuàng)建字體顽铸,但是只有CTFontManager中已注冊的字體能夠返回(默認(rèn)字體大小12)茁计;
CTFont提供的方法還有很多,列舉一些比較常用的:
對字符和字形進(jìn)行轉(zhuǎn)換谓松,返回true代表全部轉(zhuǎn)換成功星压,返回false則代表字體不包含某些字形(沒有成功mapping的字形結(jié)果為0);

bool CTFontGetGlyphsForCharacters(
    CTFontRef       font,
    const UniChar   characters[_Nonnull],
    CGGlyph         glyphs[_Nonnull],
    CFIndex         count );

獲取字體的ascent鬼譬、descent娜膘、leading、bounding box等屬性:

CGFloat CTFontGetAscent( CTFontRef font );
CGFloat CTFontGetDescent( CTFontRef font );
CGFloat CTFontGetLeading( CTFontRef font );
CGRect CTFontGetBoundingBox( CTFontRef font );

可以直接對某些字形進(jìn)行渲染优质,參數(shù)font提供字體相關(guān)屬性竣贪,glyphs數(shù)組提供字形军洼,positions數(shù)組提供位置(可以通過CTLine生成);

void CTFontDrawGlyphs(
    CTFontRef       font, 
    const CGGlyph   glyphs[_Nonnull],
    const CGPoint   positions[_Nonnull],
    size_t          count, 
    CGContextRef    context );

2演怎、CTLineRef

行的排版數(shù)據(jù)匕争,一個很常見的生成方式是通過NSAttributeString;(這種方式的好處在于便捷爷耀,不需要手動創(chuàng)建typesetter甘桑;但是同樣因?yàn)闆]有顯式創(chuàng)建typesetter,無法配置換行參數(shù)畏纲;這個API多用于一行文本的排版)

CTLineRef CTLineCreateWithAttributedString(
    CFAttributedStringRef attrString );

有時候我們需要對CTLine進(jìn)行截斷扇住,下面的方法是對line進(jìn)行截斷,width是指定的行寬盗胀,truncationType是截斷的規(guī)則(A...B艘蹋,AB...和...AB這三種樣式),truncationToken是截斷用的填充符號(通常是...的省略號票灰,為Null時則只截斷女阀,不做填充)

CTLineRef _Nullable CTLineCreateTruncatedLine(
    CTLineRef line,
    double width,
    CTLineTruncationType truncationType,
    CTLineRef _Nullable truncationToken );

CTLine是由多個CTRun組成,每個CTRun又包括多個字形屑迂,下面三個方法可以獲取CTLine的一些數(shù)據(jù):

CFIndex CTLineGetGlyphCount(
    CTLineRef line ); // 獲取字形數(shù)量
CFArrayRef CTLineGetGlyphRuns(
    CTLineRef line ); // 獲取所有的CTLine
CFRange CTLineGetStringRange(
    CTLineRef line ); // 獲取創(chuàng)建CTLine時的range

在對一行排版的時候浸策,有時候我們希望兩端對齊,此時可以用下面的方法實(shí)現(xiàn):
line是需要對齊的行惹盼,justificationFactor是調(diào)整的系數(shù)(范圍0到1庸汗,假如文字長度是100,限定寬度是300手报,則填充的空白區(qū)域?yàn)?00*justificationFactor)蚯舱,justificationWidth是目標(biāo)寬度,如果line的長度超過了justificationWidth掩蛤,則會返回NULL枉昏;

CTLineRef _Nullable CTLineCreateJustifiedLine(
    CTLineRef line,
    CGFloat justificationFactor,
    double justificationWidth );

有時候我們只是希望對齊文本,并不想填充空白字符揍鸟,此時上面的方法并不合適兄裂,需要改用CTLineGetPenOffsetForFlush;line是要排版的行阳藻,flushFactor是0~1的浮點(diǎn)數(shù)(0表示靠左晰奖,1表示靠右,0.5表示居中)腥泥,flushWidth表示對齊的寬度畅涂;

double CTLineGetPenOffsetForFlush(
    CTLineRef line,
    CGFloat flushFactor,
    double flushWidth );

CTLine可以直接繪制,line是繪制的行道川,context是上下文;(CGContextSetTextPosition設(shè)置的位置對CTFrameDraw沒有作用,但是和CTLineDraw 配合使用則效果非常好)

void CTLineDraw(
    CTLineRef line,
    CGContextRef context ) ;

CTLine測量相關(guān)的方法冒萄,CTLineGetTypographicBounds可以獲取一個CTLine的大小臊岸,參數(shù)會回調(diào)ascent、descent尊流、leading的大小帅戒,返回值是寬度,綜合起來就是CTLine的大小崖技,如下面的getLineBounds方法逻住,可以獲取一行文本的Rect;

double CTLineGetTypographicBounds(
    CTLineRef line,
    CGFloat * _Nullable ascent,
    CGFloat * _Nullable descent,
    CGFloat * _Nullable leading );

// DEMO
+ (CGRect)getLineBounds:(CTLineRef)line point:(CGPoint)point {
    CGFloat ascent = 0.0f;
    CGFloat descent = 0.0f;
    CGFloat leading = 0.0f;
    CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
    CGFloat height = ascent + descent;
    return CGRectMake(point.x, point.y - descent, width, height);
}

另外一種獲取Rect的方案是直接用下面的方法:(options種類很多迎献,通常填0就好)

CGRect CTLineGetBoundsWithOptions(
    CTLineRef line,
    CTLineBoundsOptions options );

還有一種size是ImageBounds瞎访,表示計算這行文字繪制成圖片所需要的最小size;上面的size是用于排版的size(帶有各種邊距)吁恍,而imageBounds是盡可能小的size(理想狀態(tài))扒秸。

CGRect CTLineGetImageBounds(
    CTLineRef line,
    CGContextRef _Nullable context );

CoreText不是UIKit,點(diǎn)擊的事件需要自己手動計算和處理冀瓦,CTLineGetStringIndexForPosition傳入行信息和位置信息伴奥,可以傳出該位置對應(yīng)的字符索引;(注意位置是基于左下角原點(diǎn)坐標(biāo)系翼闽,如果是UIKit的坐標(biāo)則需要做坐標(biāo)系變換)

CFIndex CTLineGetStringIndexForPosition(
    CTLineRef line,
    CGPoint position );

計算當(dāng)行中拾徙,某個字符相對的x坐標(biāo);(secondaryOffset通常用不到感局,傳NULL即可)

CGFloat CTLineGetOffsetForStringIndex(
    CTLineRef line,
    CFIndex charIndex,
    CGFloat * _Nullable secondaryOffset ) ;

3尼啡、CTRunRef

排版時個格式相同的基礎(chǔ)單位,包括一個或多個字形蓝厌,通過CTRunGetGlyphCount可以獲刃;

CFIndex CTRunGetGlyphCount(
    CTRunRef run );

CTRunRef包括多個文字相關(guān)屬性拓提,可以通過CTRunGetAttributes獲榷潦选;(屬性可能是來自NSAttributeString代态,也可能來自于內(nèi)部排版引擎的生成)

CFDictionaryRef CTRunGetAttributes(
    CTRunRef run );

CTRunRef有一個很方便的地方寺惫,便是可以直接拿到字符對應(yīng)的字形:
CTRunGetGlyphsPtr可以拿到對應(yīng)字形列表(但是返回值可能為NULL,即使存在字形)蹦疑;
CTRunGetGlyphs是更推薦的做法西雀,創(chuàng)建buffer然后傳入CoreText,直接獲取對應(yīng)的字形歉摧;(字形其實(shí)就是一個unsigned short的類型)

const CGGlyph * _Nullable CTRunGetGlyphsPtr(
    CTRunRef run );
void CTRunGetGlyphs(
    CTRunRef run,
    CFRange range,
    CGGlyph buffer[_Nonnull] );

CTRun可以獲取到每個字符對應(yīng)的位置艇肴,同樣有兩個方法:
同樣推薦使用CTRunGetPositions腔呜,原因同上(CTRunGetPositionsPtr可能有值的時候也會返回NULL)

const CGPoint * _Nullable CTRunGetPositionsPtr(
    CTRunRef run );
void CTRunGetPositions(
    CTRunRef run,
    CFRange range,
    CGPoint buffer[_Nonnull] );

CTRunRef可以獲取生成時的Range,以便定位到這段文字在整體的位置再悼;

CFRange CTRunGetStringRange(
    CTRunRef run );

在排版時核畴,有兩個方法:

  • CTRunGetTypographicBounds 獲取這個CTRun的最小排版size;
  • CTRunGetImageBounds 獲取這個CTRunRef的最小顯示size冲九;
double CTRunGetTypographicBounds(
    CTRunRef run,
    CFRange range,
    CGFloat * _Nullable ascent,
    CGFloat * _Nullable descent,
    CGFloat * _Nullable leading );
CGRect CTRunGetImageBounds(
    CTRunRef run,
    CGContextRef _Nullable context,
    CFRange range )

一個CTLine里面會包括多個CTRun谤草,每個CTRun都包括各自的位置信息,在排版的時候可以通過CTRunGetTextMatrix獲取相應(yīng)的位置莺奸,再通過CGContextSetTextMatrix設(shè)置到CGContext丑孩;

CGAffineTransform CTRunGetTextMatrix(
    CTRunRef run );

最終繪制的方法是CTRunDraw,傳入CTRun和CGContextRef以及CFRange即可灭贷。

void CTRunDraw(
    CTRunRef run,
    CGContextRef context,
    CFRange range );

舉個例子温学,下面是一段繪制CTRun的樣例代碼:

- (void)drawInContext:(CGContextRef)context
{
        if (!_run || !context)
        {
                return;
        }

        CGAffineTransform textMatrix = CTRunGetTextMatrix(_run);

        if (CGAffineTransformIsIdentity(textMatrix))
        {
                CTRunDraw(_run, context, CFRangeMake(0, 0));
        }
        else
        {
                CGPoint pos = CGContextGetTextPosition(context);

                // set tx and ty to current text pos according to docs
                textMatrix.tx = pos.x;
                textMatrix.ty = pos.y;

                CGContextSetTextMatrix(context, textMatrix);

                CTRunDraw(_run, context, CFRangeMake(0, 0));

                // restore identity
                CGContextSetTextMatrix(context, CGAffineTransformIdentity);
        }
};

4、CTRunDelegate

CTRunDelegate是CTRun的delegate氧腰,我們可以手動設(shè)置CTRun的Ascent枫浙、Descent、Width屬性古拴,這是圖文混排的基礎(chǔ)箩帚;插入一個空白的字符,將其字符的大小設(shè)置為(width, height)黄痪,留出對應(yīng)的大小空白區(qū)域紧帕,然后在排版結(jié)束完在對應(yīng)的位置插入UIImageView就實(shí)現(xiàn)了圖文混排的效果;
下面是一段插入特定寬高字符的示例代碼:

static CGFloat ascentCallback(void * refCon){
    SSEmptyLayoutData *data = (__bridge SSEmptyLayoutData *)refCon;
    return data.size.height;
}

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

static CGFloat widthCallback(void * refCon){
    SSEmptyLayoutData *data = (__bridge SSEmptyLayoutData *)refCon;
    return data.size.width;
}

- (void)insert {
    CTRunDelegateCallbacks callbacks;
    memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
    callbacks.version = kCTRunDelegateVersion1;
    callbacks.getAscent = ascentCallback;
    callbacks.getDescent = descentCallback;
    callbacks.getWidth = widthCallback;
    CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)(layoutData));
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space,
                                   CFRangeMake(0, 1),
                                   kCTRunDelegateAttributeName,
                                   delegate);
    CFRelease(delegate);
}

5桅打、CTFrameRef

CTFrame是由多行文本組成的布局frame是嗜,由framesetter生成。
CTFrameGetStringRange可以獲取frame中的字符串范圍(創(chuàng)建frame時候填的參數(shù))挺尾,如果CTFramesetterCreateFrame填的是(0, 0)鹅搪,則會按照實(shí)際的數(shù)字返回,例如填的是(3, 0)遭铺,生成的frame有50個字符丽柿,那么返回的range是(3, 50);
同理魂挂,CTFrameGetVisibleStringRange可以獲取可見字符的range甫题,規(guī)則同上;

CFRange CTFrameGetStringRange(
    CTFrameRef frame );
CFRange CTFrameGetVisibleStringRange(
    CTFrameRef frame );

同時涂召,還有方法可以獲取Frame的Path坠非、FrameAttribute等屬性

CFDictionaryRef _Nullable CTFrameGetFrameAttributes(
    CTFrameRef frame );
CGPathRef CTFrameGetPath(
    CTFrameRef frame );

更為常見的是讀取CTLine的信息,可以調(diào)用CTFrameGetLines直接返回所有的CTLine果正,也可以調(diào)用CTFrameGetLineOrigins返回每一行的起始位置(注意CoreText的坐標(biāo)原點(diǎn)是左下角)炎码;

CFArrayRef CTFrameGetLines(
    CTFrameRef frame );
void CTFrameGetLineOrigins(
    CTFrameRef frame,
    CFRange range,
    CGPoint origins[_Nonnull] );

最后盟迟,CTFrame同樣支持直接渲染

void CTFrameDraw(
    CTFrameRef frame,
    CGContextRef context );

6、CTTypesetterRef

CTTypesetter是基礎(chǔ)的排版類辅肾,可以通過AttributeString創(chuàng)建队萤,并根據(jù)需要附加options(通常用不到);
typesetter通常用于創(chuàng)建多行文本的換行和其他上下文相關(guān)的字符處理矫钓;(CTLineRef也可以排版,但是只有自己當(dāng)前行的信息)

CTTypesetterRef CTTypesetterCreateWithAttributedString(
    CFAttributedStringRef string );
CTTypesetterRef _Nullable CTTypesetterCreateWithAttributedStringAndOptions(
    CFAttributedStringRef string,
    CFDictionaryRef _Nullable options );

typesetter很常用的方法是斷行舍杜,傳入需要換成的stringRange新娜,和行的偏移offset,便會對typesetter內(nèi)的字符進(jìn)行處理既绩,生成一行CTLine(如果參數(shù)不合法概龄,比如超過邊界則會返回NULL);
如果不想控制offset饲握,可以調(diào)用下面的方法私杜,offset默認(rèn)為0;

CTLineRef CTTypesetterCreateLineWithOffset(
    CTTypesetterRef typesetter,
    CFRange stringRange,
    double offset );
CTLineRef CTTypesetterCreateLine(
    CTTypesetterRef typesetter,
    CFRange stringRange );

上面的換行方法需要傳入stringRange救欧,這個range由哪里生成衰粹?
CTTypesetterSuggestLineBreakWithOffset方法傳入typesetter,開始的位置startIndex笆怠,行的寬度width铝耻,以及行位置偏移offset,會返回這行文本的長度蹬刷;(配合startIndex瓢捉,就組成了一個range)
同理,CTTypesetterSuggestLineBreak適用于offset為0的時候办成;

CFIndex CTTypesetterSuggestLineBreakWithOffset(
    CTTypesetterRef typesetter,
    CFIndex startIndex,
    double width,
    double offset );
CFIndex CTTypesetterSuggestLineBreak(
    CTTypesetterRef typesetter,
    CFIndex startIndex,
    double width );

需要注意泡态,當(dāng)我們用typesetter排版的時候,attributestring中的換行屬性(linebreaking選項(xiàng))并不生效迂卢,CTTypesetterSuggestLineBreak方法不會截斷一個單詞(類似NSLineBreakByWordWrapping)某弦,而CTTypesetterSuggestClusterBreak則類似NSLineBreakByCharWrapping;(注意冷守,這些截斷并不會生成省略號刀崖,如果有需要截斷成ABC...的樣式,需要用CTLine的方法)

CFIndex CTTypesetterSuggestClusterBreakWithOffset(
    CTTypesetterRef typesetter,
    CFIndex startIndex,
    double width,
    double offset );
CFIndex CTTypesetterSuggestClusterBreak(
    CTTypesetterRef typesetter,
    CFIndex startIndex,
    double width );

7拍摇、CTFramesetterRef

排版生成類亮钦,每個CTFramesetterRef內(nèi)都會有一個CTTypesetterRef來負(fù)責(zé)換行、字符處理等充活,可以通過CTTypesetterRef創(chuàng)建蜂莉;

CTFramesetterRef CTFramesetterCreateWithTypesetter( 
    CTTypesetterRef typesetter ); 

也可以通過NSAttributeString來創(chuàng)建蜡娶;

CTFramesetterRef CTFramesetterCreateWithAttributedString( 
    CFAttributedStringRef string );  

CTFramesetterRef可以產(chǎn)生排版結(jié)果(用于渲染),stringRange的len=0時映穗,表示填充字符到path放不下窖张,frameAttributes是frame相關(guān)屬性,比如從上到下填充蚁滋,還是從左到右宿接;

CTFrameRef CTFramesetterCreateFrame( 
    CTFramesetterRef framesetter, 
    CFRange stringRange, 
    CGPathRef path, 
    CFDictionaryRef _Nullable frameAttributes ); 

如果有需要訪問CTTypesetterRef,則可以直接從CTFramesetterRef讀仍肌睦霎;

CTTypesetterRef CTFramesetterGetTypesetter(
    CTFramesetterRef framesetter );

一個更常見的場景,是計算CTFramesetterRef所占用的大小走诞,已創(chuàng)建排版所用的CGPath副女,可以用下面的方法,constraints是目標(biāo)區(qū)域的最大size(可以將其height設(shè)置為CGFLOAT_MAX表示不限制)蚣旱,fitRange會返回最終填充的字符長度碑幅,返回值的size是計算的size;

CGSize CTFramesetterSuggestFrameSizeWithConstraints(
    CTFramesetterRef framesetter,
    CFRange stringRange,
    CFDictionaryRef _Nullable frameAttributes,
    CGSize constraints,
    CFRange * _Nullable fitRange ) ;

四塞绿、CoreText排版

經(jīng)過漫長的學(xué)習(xí)戒劫,我們終于了解排版的基礎(chǔ)知識和CoreText常用類创坞,接下來看看CoreText的實(shí)際應(yīng)用颖低。

1俭茧、正常的文字排版(CTFrame)

最常見的排版過程是先創(chuàng)建NSAttributeString,然后創(chuàng)建CTFramesetterRef涧黄,接著是生成繪制的區(qū)域UIBezierPath篮昧,用這兩個生成CTFrameRef,最后調(diào)用CTFrameDraw進(jìn)行繪制笋妥;

    NSString *str = @"一二三四五六七八九十一二三四五六七八九十\n一二三四五六七八九十一二三四五六七八九十一二三四五六七八九十";
    CFMutableAttributedStringRef attrString = (__bridge CFMutableAttributedStringRef)[self getNormalMutableAttributeStrWithStr:str];

    CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString(attrString);
    UIBezierPath * bezierPath = [UIBezierPath bezierPathWithRect:CGRectMake(30, 0, self.topDrawView.width / 2, self.topDrawView.height / 2)];
    CTFrameRef frameRef = CTFramesetterCreateFrame(frameSetter,
                                                   CFRangeMake(0, 0),
                                                   bezierPath.CGPath, NULL);
    CTFrameDraw(frameRef, context);

最終繪制的效果:

2懊昨、CTLine的排版

CTLine的排版首先是創(chuàng)建NSAttributeString,接著創(chuàng)建CTTypesetterRef(與CTFrame不同春宣,CTFrame是用CTFramesetter來處理)酵颁,在用setter進(jìn)行分行處理,得到每一行的CTLine月帝;
拿到CTLine之后躏惋,我們可以進(jìn)行對齊操作,也可以進(jìn)行左對齊和右對齊嚷辅,最后設(shè)置行的起始位置簿姨,進(jìn)行繪制;

    NSString *str = @"Guangdong has recently published Several Policies and Measures for Promoting Scientific and Technological Innovation";
    CFAttributedStringRef attrString = (__bridge CFAttributedStringRef)[self getAttributeStrWithStr:str];

    CTTypesetterRef typesetter = CTTypesetterCreateWithAttributedString(attrString);

    CFIndex start = 0;
    CGPoint textPosition = CGPointMake(0, 55);
    double width = self.topDrawView.width;

    BOOL isCharLineBreak = NO;
    BOOL isJustifiedLine = NO;
    float flush = 0; // centered,可以調(diào)整這里的數(shù)字0是左對齊扁位,1是右對齊准潭,0.5居中
    while (start < str.length) {
        CFIndex count;
        if (isCharLineBreak) {
            count = CTTypesetterSuggestClusterBreak(typesetter, start, width);
        }
        else {
            count = CTTypesetterSuggestLineBreak(typesetter, start, width);
        }
        CTLineRef line = CTTypesetterCreateLine(typesetter, CFRangeMake(start, count));
        if (isJustifiedLine) {
            line = CTLineCreateJustifiedLine(line, 1, width);
        }
        double penOffset = CTLineGetPenOffsetForFlush(line, flush, width);
        CGContextSetTextPosition(context, textPosition.x + penOffset, self.topDrawView.height - textPosition.y);
        CTLineDraw(line, context);
        textPosition.y += CTLineGetBoundsWithOptions(line, 0).size.height;
        start += count;
    }

上面的代碼有三個參數(shù),分別可以設(shè)置換行方式域仇、是否兩端對齊和調(diào)整對齊方式刑然;

默認(rèn)的換行方式以及不不進(jìn)行對齊操作:

Cluster的換行方式以及進(jìn)行對齊操作:

3、CTRun的排版

CTRun繪制的前面步驟可以使用CTFrame暇务、也可以使用CTLine泼掠,最終是通過CTLineGetGlyphRuns從CTLine拿到CTRun的數(shù)組;這里以一行文本為例垦细,重點(diǎn)關(guān)注一行文本中多個CTRun如何進(jìn)行繪制武鲁;
方式1:
遍歷CTRun數(shù)組,對于每一個CTRun直接調(diào)用CTRunDraw進(jìn)行繪制蝠检;

            CTRunDraw(run, context, CFRangeMake(0, 0));

方式2:
對于每個CTRun,我們讀取CTRun的中每個字形的位置和字形信息挚瘟,再讀取CTRun的屬性包括字體顏色叹谁、大小、背景去設(shè)置CGContext乘盖,最后通過CGContextShowGlyphsAtPositions進(jìn)行繪制焰檩。

            CFIndex glyphCount = CTRunGetGlyphCount(run);
            CGPoint *positions = calloc(glyphCount, sizeof(CGPoint));
            CTRunGetPositions(run, CFRangeMake(0, 0), positions);
            CGGlyph *glyphs = calloc(glyphCount, sizeof(CGGlyph));
            CTRunGetGlyphs(run, CFRangeMake(0, 0), glyphs);
            CFDictionaryRef attrDic = CTRunGetAttributes(run);
            CTFontRef runFont = CFDictionaryGetValue(attrDic, kCTFontAttributeName);
            CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL);
            CGColorRef fontColor = (CGColorRef)CFDictionaryGetValue(attrDic, NSForegroundColorAttributeName);
            CGFloat fontSize = CTFontGetSize(runFont);
            CGContextSetFont(context, cgFont);
            CGContextSetFontSize(context, fontSize);
            [(__bridge UIColor *)fontColor setFill];
            // CGColorGetComponents(fontColor) 獲取到的顏色是空的,包括CGColorGetAlpha
            //        CGContextSetFillColor(context, CGColorGetComponents(fontColor));
            CGContextShowGlyphsAtPositions(context, glyphs, positions, glyphCount);
            free(positions);
            free(glyphs);

排版效果:

4订框、圖文混排

圖文混排是CTFrame析苫、CTLine、CTRun的綜合運(yùn)用穿扳,原理是通過給NSAttributeString中添加一個空白字符衩侥,同時設(shè)置這個字符寬高為圖片的size,最終排版的時候會預(yù)留出來一個與圖片大小一致的空白區(qū)域矛物;再通過CoreText的方法讀取這個空白區(qū)域的位置茫死,在對應(yīng)的位置繪制對應(yīng)的圖片。
整個過程:

1履羞、創(chuàng)建NSMutableAttributedString峦萎,初始化原始的排版數(shù)據(jù),插入帶特定大小的空白字符忆首,生成空白字符的代碼:

static CGFloat ascentCallback(void * refCon){
    NSValue *data = (__bridge NSValue *)refCon;
    return [data CGSizeValue].height;
}

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

static CGFloat widthCallback(void * refCon){
    NSValue *data = (__bridge NSValue *)refCon;
    return [data CGSizeValue].width;
}

- (NSAttributedString *)getEmtpyAttributeString {
    CTRunDelegateCallbacks callbacks;
    memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
    callbacks.version = kCTRunDelegateVersion1;
    callbacks.getAscent = ascentCallback;
    callbacks.getDescent = descentCallback;
    callbacks.getWidth = widthCallback;
    CGSize size = CGSizeMake(50, 50);
    CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)([NSValue valueWithCGSize:size]));
    NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:@" " attributes:
  @{NSBackgroundColorAttributeName:[UIColor colorWithRed:0.5 green:1 blue:0.5 alpha:0.5],
    NSForegroundColorAttributeName:[UIColor greenColor],
    }];
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space,
                                   CFRangeMake(0, 1),
                                   kCTRunDelegateAttributeName,
                                   delegate);
    CFRelease(delegate);
    return space;
}

2爱榔、遍歷排版結(jié)果,從CTFrame中拿到CTLine的數(shù)組糙及,再從每個CTLine中拿到CTRun的數(shù)組详幽,再從CTRun讀取delegate的信息,判斷是否為特定的空白字符丁鹉;

    // 計算圖片所在位置
    CFArrayRef linesArr = CTFrameGetLines(frameRef);
    for (int i = 0; i < CFArrayGetCount(linesArr); ++i) {
        CTLineRef line = CFArrayGetValueAtIndex(linesArr, i);
        CFArrayRef runsArr = CTLineGetGlyphRuns(line);
        for (int j = 0; j < CFArrayGetCount(runsArr); ++j) {
            CTRunRef run = CFArrayGetValueAtIndex(runsArr, j);
            CTRunDelegateRef delegate = CFDictionaryGetValue(CTRunGetAttributes(run), kCTRunDelegateAttributeName);
            if (!delegate) {
                continue;
            }
            NSValue *data = CTRunDelegateGetRefCon(delegate);
            if (!data) {
                continue;
            }
            // 找到添加的特殊字符
            CGSize size = [data CGSizeValue];
            CGFloat offsetX;
            CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, &offsetX);

            CGPoint lineOrigin;
            CTFrameGetLineOrigins(frameRef, CFRangeMake(i, 1), &lineOrigin); // 獲取第i行的位置

            UIImage *image = [UIImage imageNamed:@"abc"];
            CGContextDrawImage(context, CGRectMake(lineOrigin.x + offsetX + marginX, lineOrigin.y, size.width, size.height), image.CGImage);
            // 如果用UIKit的方法進(jìn)行繪制妒潭,會出現(xiàn)上下顛倒的情況
//            [image drawInRect:CGRectMake(lineOrigin.x + offsetX + marginX, lineOrigin.y, size.width, size.height)];
        }
    }

3悴能、創(chuàng)建圖片并用前面找到的位置進(jìn)行繪制,注意UIKit和CG坐標(biāo)系的不同雳灾,如果直接使用UIImage的draw方法會出現(xiàn)上下顛倒的情況漠酿,所以這里使用CGContextDrawImage的繪制方式避免上下顛倒;

最終的繪制效果:

思考題:為什么圖片底部會有一個淺綠色的區(qū)域谎亩?

五炒嘲、一些討論

iOS中的unichar、char和unicode的區(qū)別

char是基礎(chǔ)的字符類型匈庭,一個字節(jié)夫凸;
unichar是iOS定義的類型,實(shí)際是unsigned short阱持,也就是UInt16夭拌;
unicode與前兩者的維度不同,指的是一種字符集衷咽,與其類似的概念是ASCII碼鸽扁;至于常見UTF8,是一種unicode的編碼方式镶骗。

我們來看一段c的代碼:
這里的len會是多少桶现?

    char s[] = "測試名字";
    int len = strlen(s);
    NSLog(@"len:%d", len);

結(jié)果:len:12。因此可以知道鼎姊,當(dāng)我們直接訪問s[0]時骡和,并不能讀取到"測"字。

換一段oc的代碼:
這里的len會輸出多少相寇?當(dāng)我們訪問str的第一個字符時會返回什么慰于?

    NSString *str = @"測試名字";
    NSLog(@"len:%d", str.length);

結(jié)果:len:4。當(dāng)我們用characterAtIndex讀取str第一個字符時裆赵,返回的是"測"字东囚。

(lldb) p [str characterAtIndex:0]
(unichar) $0 = U+6d4b u'測'

為什么會有這些不同的長度和字符讀取結(jié)果?

字符是一個虛擬的概念战授,要將字符存到字節(jié)流里面去页藻,需要對其進(jìn)行編碼;同理植兰,當(dāng)我們拿到一串字節(jié)流份帐,比如說上面的s[]數(shù)組(12Bytes的字節(jié)流),需要用特定的編碼格式去讀取楣导。
Xcode里面用的c字符串是用UTF8來編碼废境,存到s[]字符數(shù)組中的長度是12;
NSString的length是返回UTF16的長度,并不是字符的長度噩凹;可以嘗試往字符中添加emoji表情或者其他占兩個UInt16的字符巴元,會發(fā)現(xiàn)length與字符長度不同,同樣也無法用characterAtIndex讀到對應(yīng)的字符驮宴;

這樣也是為什么我們在OC中無法像c語言一樣逮刨,直接用str[0]去訪問NSString的第一個字符,而是使用characterAtIndex的接口去獲榷略蟆(并且返回的是UTF16編碼的字符)修己;
另外,在iOS中NSUnicodeStringEncoding的編碼方式就是NSUTF16StringEncoding迎罗。

關(guān)于Unicode更詳細(xì)的介紹睬愤,見百科

FillColor和StrokeColor的區(qū)別

在用CGContext繪制的時候纹安,經(jīng)常需要設(shè)置顏色尤辱,常用的有下面兩種:

CG_EXTERN void CGContextSetFillColor(CGContextRef cg_nullable c,
    const CGFloat * cg_nullable components);
CG_EXTERN void CGContextSetStrokeColor(CGContextRef cg_nullable c,
    const CGFloat * cg_nullable components);

那么Fill和Stroke兩種顏色有什么區(qū)別?先看看英文的定義:

Fill sets the color inside the object and stroke sets the color of the line drawn around the object.

用中文來表達(dá)厢岂,就是一個是填充顏色啥刻,一個是描邊顏色。
在iOS中咪笑,我們通過NSForegroundColorAttributeNameNSStrokeColorAttributeName這兩個attribute來設(shè)置填充顏色。

用中文來表達(dá)娄涩,就是一個是填充顏色窗怒,一個是描邊顏色。
在iOS中蓄拣,我們通過NSForegroundColorAttributeNameNSStrokeColorAttributeName這兩個attribute來設(shè)置填充顏色扬虚。

    [stringAttributes setObject: [UIColor grayColor] forKey: NSForegroundColorAttributeName];
    [stringAttributes setObject: [NSNumber numberWithFloat: 0] forKey: NSStrokeWidthAttributeName];
    [stringAttributes setObject: [UIColor redColor] forKey: NSStrokeColorAttributeName];

當(dāng)StrokeWidth為負(fù)數(shù)的時候,F(xiàn)illColor和StrokeColor同時存在球恤,如下:

Fill灰色辜昵,Stroke紅色,StrokeWidth=-3

當(dāng)StrokeWidth為正數(shù)的時候咽斧,F(xiàn)illColor不生效堪置,僅有StrokeColor,如下:

Fill灰色张惹,Stroke紅色舀锨,StrokeWidth=3

當(dāng)StrokeWidth為0的時候,F(xiàn)illColor生效宛逗,StrokeColor無效果坎匿,如下:

Fill灰色,Stroke紅色,StrokeWidth=0

圖文混排中底部綠色區(qū)域

圖文混排其實(shí)是排版時插入一個特殊的空白字符替蔬,并設(shè)定字符的寬高為特定size告私,預(yù)留對應(yīng)size的空白,再算出對應(yīng)位置的坐標(biāo)承桥,繪制上對應(yīng)的圖片驻粟。

image

根據(jù)測量,文字中圖片的size確實(shí)為預(yù)設(shè)的文字大小快毛,底部的淺綠色區(qū)域其實(shí)是排版時格嗅,一行的descent區(qū)域。
回顧下我們設(shè)置圖片寬高的代碼:

static CGFloat ascentCallback(void * refCon){
    NSValue *data = (__bridge NSValue *)refCon;
    return [data CGSizeValue].height;
}

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

static CGFloat widthCallback(void * refCon){
    NSValue *data = (__bridge NSValue *)refCon;
    return [data CGSizeValue].width;
}

實(shí)際上我們只設(shè)置了ascent為圖片的高唠帝,width為圖片的寬屯掖,descent設(shè)置的為0。但是一個CTLine往往包括多個文字襟衰,整行的descent實(shí)際上是所有字符的descent的最大值贴铜。同樣的,一行的ascent也是行內(nèi)所有字符的ascent最大值瀑晒。

總結(jié)

本文詳細(xì)介紹了CoreText的基礎(chǔ)概念以及實(shí)際運(yùn)用绍坝,如果理解完CoreText框架和文字排版、圖文混排等知識苔悦,那么已經(jīng)足夠支撐做起一個閱讀器啦轩褐。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市玖详,隨后出現(xiàn)的幾起案子把介,更是在濱河造成了極大的恐慌,老刑警劉巖蟋座,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拗踢,死亡現(xiàn)場離奇詭異,居然都是意外死亡向臀,警方通過查閱死者的電腦和手機(jī)巢墅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來券膀,“玉大人君纫,你說我怎么就攤上這事∏郾颍” “怎么了庵芭?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長雀监。 經(jīng)常有香客問我双吆,道長眨唬,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任好乐,我火速辦了婚禮匾竿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蔚万。我一直安慰自己岭妖,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布反璃。 她就那樣靜靜地躺著昵慌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪淮蜈。 梳的紋絲不亂的頭發(fā)上斋攀,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天,我揣著相機(jī)與錄音梧田,去河邊找鬼淳蔼。 笑死,一個胖子當(dāng)著我的面吹牛裁眯,可吹牛的內(nèi)容都是我干的鹉梨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼穿稳,長吁一口氣:“原來是場噩夢啊……” “哼存皂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起逢艘,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤艰垂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后埋虹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡娩怎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年搔课,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片截亦。...
    茶點(diǎn)故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡爬泥,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出崩瓤,到底是詐尸還是另有隱情袍啡,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布却桶,位于F島的核電站境输,受9級特大地震影響蔗牡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜嗅剖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一辩越、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧信粮,春花似錦黔攒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至旅掂,卻和暖如春赏胚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背辞友。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工栅哀, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人称龙。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓留拾,卻偏偏與公主長得像,于是被迫代替她去往敵國和親鲫尊。 傳聞我的和親對象是個殘疾皇子痴柔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評論 2 355

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