文字排版入門—— 排版基礎论颅、CoreText和圖文混排

一毕泌、排版概念

1喝检、Characters and Glyphs(字符和字形)

字符是文字的最小單元,以這段文字為例撼泛,每個字都是一個字符挠说;需要注意,字符是一個抽象的概念愿题;
當文字真正繪制出來時需要選擇字體损俭,以“A”這個字母為例,當字母'A'印刷出來或者顯示到屏幕潘酗,可能有多種字體杆兵,每種字體都有一種字形'A':


但是,字符和字形不是一一對應仔夺,也不是一對多的關系琐脏!
在某些字體中,相同的字符可能會包括多個的字形:
“é” = “e” + “′” (一個字符由兩個字形組合而成)
一個字形缸兔,也可以容納多個字符日裙,如下:(右邊的字形是連寫ff,包括兩個字符f)


上圖的連字符是一種上下文相關的字形惰蜜,一個字符的字形由受到下一個字符的影響昂拂。

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

Typeface指一系列風格接近的字體抛猖,而Font是一系列具有一致大小格侯、樣式的字形組成的字體;通常多個字體會組成一個字型财著,如圖:


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

3联四、字體屬性

字體屬性指的是字符的字形大小和布局。同一字體中的字符屬性大致相同撑教,常用屬性包括:baseline(字符基線)朝墩、ascent(字形最高點和baseline的距離)、descent(字形最低點和baseline的距離)驮履、leading(行間距)等鱼辙。


字符屬性的詳細介紹:
text direction:文字的排版順序廉嚼,像English是從左上角開始玫镐,從左到右;也有文字的排版是從右到左或者是從上到下的排版等怠噪;
line breaking:在字符串中找到一個點恐似,截取出一段文本用于顯示一行;
baseline:所有字形的虛擬基準線傍念,如下圖藍色部分:(也會有部分字形跨過基準線矫夷,比如說g)

left-side bearing:如圖葛闷,是字符之間默認的間隙;(同理還有right-side bearing
descent/ascent:字形的上下部分双藕;
bounding rectangle:字形的可見部分淑趾;
kerning:文字默認排版時,寬度由advance width指定忧陪,默認會留有一小部分間隔扣泊;也可以通過設置字間距(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ā)者通常不需要關心)床估;
  • 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屬性可以獲取到富文本對應的string;
那么缎脾,為什么NSAttributeString不做成NSString的子類祝闻?
首先從字面意思來看,NSAttributeString很容易就被當成NSString的子類遗菠,導致會寫出NSAttributeString==NSString的這類判斷治筒,將NSAttributeString不寫成NSString子類可以避免這一類問題;
更為重要的是舷蒲,NSAttributeString是描述string的各種屬性耸袜,而NSString是單純的字符串。

NSAttributeString的屬性在生成之后便無法修改牲平,如果需要修改某些屬性堤框,則需要使用NSMutableAttributeString。
NSAttributeString有兩種方法可以讀取對應某個字符的屬性:

attributesAtIndex:effectiveRange:
attributesAtIndex:longestEffectiveRange:inRange:

為什么需要有兩種方法纵柿?
attribute是針對一段字符的屬性蜈抓,而一個字符串往往會有多個attribute;
如果把字符串中的每個字符看成一維數(shù)軸上的點昂儒,那么attribute就是一個點或者兩個點之間的線段沟使,NSAttributeString由許許多多的線段組成;
當我們獲取某個點的屬性渊跋,實際上就是詢問所有的線段中腊嗡,經(jīng)過這個點的線段(屬性)有哪些。
所以為了優(yōu)化速度拾酝,可以通過指定effectiveRange燕少,縮小遍歷的范圍。如果想知道該屬性的最大覆蓋范圍蒿囤,則使用帶longestEffectiveRange的方法客们,但是需要手動設置遍歷range,否則會遍歷整個字符串的屬性材诽。

如下底挫,4個點代表4個字符,一個紅色的線段表示一個(0, 3)的屬性脸侥,藍色的線段表示(1, 3)的屬性建邓;
當我們獲取第2個點的屬性時,因為紅色和藍色線段都經(jīng)過第2個點湿痢,所以會返回兩個屬性涝缝;
當我們獲取第1個點的屬性時,只有紅色的線段經(jīng)過第1個點譬重,則只會返回一個屬性拒逮;

最后注意,當Attribute在賦值給NSAttributeString之后不應該修改臀规,比如說下面的段屬性設置滩援,當我們把這個dict賦值給NSAttributeString之后,就不應該修改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)換和進行文字排版的框架恩袱,API基于C語言。
當我們需要排版時胶哲,可以對字符串設置各種格式畔塔,生成NSAttributeString;
然后用NSAttributeString去創(chuàng)建CTFramesetter類鸯屿,CTFramesetter會處理排版信息澈吨,然后生成排版后的結(jié)果CTFrame;
CTFrame是一段或者多段文本寄摆,每段文本又由多行文字組成谅辣,每行的表示為CTLine;
CTLine是一行文本婶恼,每行文本由多個CTRun組成桑阶,CTRun是一小段連續(xù)的字形;
CTTypeSetter負責上下文相關排版處理勾邦,比如說換行联逻,每個CTFrame中都會有一個CTTypeSetter;
他們之間的關系圖如下:


總的來說检痰,CTFramesetter是生成CTFrame的工廠類包归,初始化參數(shù)是attributed string,會在內(nèi)部創(chuàng)建CTTypesetter并進行實際的排版铅歼;CTLine類似每一行的文字公壤,CTRun是一行中具有相同屬性的連續(xù)字形,比如說“我正在分享閱讀器”椎椰,就會由三個CTRun組成厦幅,分別是“我正在”、“分享”慨飘、“閱讀器”(因為“分享”兩個字加粗了确憨,否則就會是一個CTRun)译荞。

1、CTFont

CTFontRef是CoreText的字體休弃,可以讀取字體的版權(quán)信息(copyright)吞歼、fontFamily、style等信息塔猾;
CTFontCreateWithName()用于創(chuàng)建字體篙骡,但是只有CTFontManager中已注冊的字體能夠返回(默認字體大小12);
CTFont提供的方法還有很多丈甸,列舉一些比較常用的:
對字符和字形進行轉(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 );

可以直接對某些字形進行渲染忽刽,參數(shù)font提供字體相關屬性,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市埋;但是同樣因為沒有顯式創(chuàng)建typesetter黎泣,無法配置換行參數(shù);這個API多用于一行文本的排版)

CTLineRef CTLineCreateWithAttributedString(
    CFAttributedStringRef attrString );

有時候我們需要對CTLine進行截斷缤谎,下面的方法是對line進行截斷抒倚,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

在對一行排版的時候斟赚,有時候我們希望兩端對齊着降,此時可以用下面的方法實現(xiàn):
line是需要對齊的行,justificationFactor是調(diào)整的系數(shù)(范圍0到1拗军,假如文字長度是100任洞,限定寬度是300蓄喇,則填充的空白區(qū)域為200*justificationFactor),justificationWidth是目標寬度交掏,如果line的長度超過了justificationWidth妆偏,則會返回NULL;

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

有時候我們只是希望對齊文本耀销,并不想填充空白字符,此時上面的方法并不合適铲汪,需要改用CTLineGetPenOffsetForFlush熊尉;line是要排版的行,flushFactor是0~1的浮點數(shù)(0表示靠左掌腰,1表示靠右狰住,0.5表示居中),flushWidth表示對齊的寬度齿梁;

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

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

void CTLineDraw(
    CTLineRef line,
    CGContextRef context ) ;

CTLine測量相關的方法,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袱瓮,點擊的事件需要自己手動計算和處理,CTLineGetStringIndexForPosition傳入行信息和位置信息爱咬,可以傳出該位置對應的字符索引尺借;(注意位置是基于左下角原點坐標系,如果是UIKit的坐標則需要做坐標系變換)

CFIndex CTLineGetStringIndexForPosition(
    CTLineRef line,
    CGPoint position );

計算當行中精拟,某個字符相對的x坐標燎斩;(secondaryOffset通常用不到虱歪,傳NULL即可)

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

3、CTRunRef

排版時個格式相同的基礎單位栅表,包括一個或多個字形笋鄙,通過CTRunGetGlyphCount可以獲取怪瓶;

CFIndex CTRunGetGlyphCount(
    CTRunRef run );

CTRunRef包括多個文字相關屬性萧落,可以通過CTRunGetAttributes獲取洗贰;(屬性可能是來自NSAttributeString找岖,也可能來自于內(nèi)部排版引擎的生成)

CFDictionaryRef CTRunGetAttributes(
    CTRunRef run );

CTRunRef有一個很方便的地方,便是可以直接拿到字符對應的字形:
CTRunGetGlyphsPtr可以拿到對應字形列表(但是返回值可能為NULL敛滋,即使存在字形)许布;
CTRunGetGlyphs是更推薦的做法,創(chuàng)建buffer然后傳入CoreText绎晃,直接獲取對應的字形蜜唾;(字形其實就是一個unsigned short的類型)

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

CTRun可以獲取到每個字符對應的位置,同樣有兩個方法:
同樣推薦使用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獲取相應的位置,再通過CGContextSetTextMatrix設置到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,我們可以手動設置CTRun的Ascent遍愿、Descent存淫、Width屬性,這是圖文混排的基礎沼填;插入一個空白的字符桅咆,將其字符的大小設置為(width, height),留出對應的大小空白區(qū)域坞笙,然后在排版結(jié)束完在對應的位置插入UIImageView就實現(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ù)字返回寞冯,例如填的是(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的坐標原點是左下角)留搔;

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

最后晕换,CTFrame同樣支持直接渲染

void CTFrameDraw(
    CTFrameRef frame,
    CGContextRef context );

6霜瘪、CTTypesetterRef

CTTypesetter是基礎的排版類,可以通過AttributeString創(chuàng)建奴迅,并根據(jù)需要附加options(通常用不到);
typesetter通常用于創(chuàng)建多行文本的換行和其他上下文相關的字符處理挺据;(CTLineRef也可以排版取具,但是只有自己當前行的信息)

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

typesetter很常用的方法是斷行,傳入需要換成的stringRange扁耐,和行的偏移offset暇检,便會對typesetter內(nèi)的字符進行處理,生成一行CTLine(如果參數(shù)不合法婉称,比如超過邊界則會返回NULL)块仆;
如果不想控制offset,可以調(diào)用下面的方法王暗,offset默認為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 );

需要注意,當我們用typesetter排版的時候缀蹄,attributestring中的換行屬性(linebreaking選項)并不生效峭跳,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來負責換行逝段、字符處理等垛玻,可以通過CTTypesetterRef創(chuàng)建;

CTFramesetterRef CTFramesetterCreateWithTypesetter( 
    CTTypesetterRef typesetter ); 

也可以通過NSAttributeString來創(chuàng)建奶躯;

CTFramesetterRef CTFramesetterCreateWithAttributedString( 
    CFAttributedStringRef string );  

CTFramesetterRef可以產(chǎn)生排版結(jié)果(用于渲染)帚桩,stringRange的len=0時,表示填充字符到path放不下嘹黔,frameAttributes是frame相關屬性账嚎,比如從上到下填充,還是從左到右儡蔓;

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

如果有需要訪問CTTypesetterRef郭蕉,則可以直接從CTFramesetterRef讀取喂江;

CTTypesetterRef CTFramesetterGetTypesetter(
    CTFramesetterRef framesetter );

一個更常見的場景召锈,是計算CTFramesetterRef所占用的大小,已創(chuàng)建排版所用的CGPath开呐,可以用下面的方法烟勋,constraints是目標區(qū)域的最大size(可以將其height設置為CGFLOAT_MAX表示不限制),fitRange會返回最終填充的字符長度筐付,返回值的size是計算的size卵惦;

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

四、CoreText排版

經(jīng)過漫長的學習瓦戚,我們終于了解排版的基礎知識和CoreText常用類沮尿,接下來看看CoreText的實際應用。

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

最常見的排版過程是先創(chuàng)建NSAttributeString畜疾,然后創(chuàng)建CTFramesetterRef赴邻,接著是生成繪制的區(qū)域UIBezierPath,用這兩個生成CTFrameRef啡捶,最后調(diào)用CTFrameDraw進行繪制姥敛;

    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進行分行處理了赌,得到每一行的CTLine墨榄;
拿到CTLine之后,我們可以進行對齊操作勿她,也可以進行左對齊和右對齊袄秩,最后設置行的起始位置,進行繪制逢并;

    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ù)猪狈,分別可以設置換行方式箱沦、是否兩端對齊和調(diào)整對齊方式辩恼;

默認的換行方式以及不不進行對齊操作:

Cluster的換行方式以及進行對齊操作:

3、CTRun的排版

CTRun繪制的前面步驟可以使用CTFrame谓形、也可以使用CTLine灶伊,最終是通過CTLineGetGlyphRuns從CTLine拿到CTRun的數(shù)組;這里以一行文本為例寒跳,重點關注一行文本中多個CTRun如何進行繪制聘萨;
方式1:
遍歷CTRun數(shù)組,對于每一個CTRun直接調(diào)用CTRunDraw進行繪制童太;

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

方式2:
對于每個CTRun米辐,我們讀取CTRun的中每個字形的位置和字形信息,再讀取CTRun的屬性包括字體顏色书释、大小翘贮、背景去設置CGContext,最后通過CGContextShowGlyphsAtPositions進行繪制爆惧。

            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的綜合運用址遇,原理是通過給NSAttributeString中添加一個空白字符,同時設置這個字符寬高為圖片的size斋竞,最終排版的時候會預留出來一個與圖片大小一致的空白區(qū)域倔约;再通過CoreText的方法讀取這個空白區(qū)域的位置,在對應的位置繪制對應的圖片坝初。
整個過程:

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的方法進行繪制,會出現(xiàn)上下顛倒的情況
//            [image drawInRect:CGRectMake(lineOrigin.x + offsetX + marginX, lineOrigin.y, size.width, size.height)];
        }
    }

3勾栗、創(chuàng)建圖片并用前面找到的位置進行繪制惨篱,注意UIKit和CG坐標系的不同,如果直接使用UIImage的draw方法會出現(xiàn)上下顛倒的情況围俘,所以這里使用CGContextDrawImage的繪制方式避免上下顛倒砸讳;

最終的繪制效果:


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

五界牡、一些討論

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

char是基礎的字符類型,一個字節(jié)宿亡;
unichar是iOS定義的類型常遂,實際是unsigned short,也就是UInt16挽荠;
unicode與前兩者的維度不同克胳,指的是一種字符集,與其類似的概念是ASCII碼圈匆;至于常見UTF8漠另,是一種unicode的編碼方式。

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

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

結(jié)果:len:12酗钞。因此可以知道,當我們直接訪問s[0]時,并不能讀取到"測"字砚作。

換一段oc的代碼:
這里的len會輸出多少窘奏?當我們訪問str的第一個字符時會返回什么?

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

結(jié)果:len:4葫录。當我們用characterAtIndex讀取str第一個字符時着裹,返回的是"測"字。

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

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

字符是一個虛擬的概念骇扇,要將字符存到字節(jié)流里面去,需要對其進行編碼面粮;同理少孝,當我們拿到一串字節(jié)流,比如說上面的s[]數(shù)組(12Bytes的字節(jié)流)熬苍,需要用特定的編碼格式去讀取稍走。
Xcode里面用的c字符串是用UTF8來編碼,存到s[]字符數(shù)組中的長度是12柴底;
NSString的length是返回UTF16的長度婿脸,并不是字符的長度;可以嘗試往字符中添加emoji表情或者其他占兩個UInt16的字符柄驻,會發(fā)現(xiàn)length與字符長度不同狐树,同樣也無法用characterAtIndex讀到對應的字符;

這樣也是為什么我們在OC中無法像c語言一樣鸿脓,直接用str[0]去訪問NSString的第一個字符抑钟,而是使用characterAtIndex的接口去獲取(并且返回的是UTF16編碼的字符)答憔;
另外味赃,在iOS中NSUnicodeStringEncoding的編碼方式就是NSUTF16StringEncoding掀抹。

關于Unicode更詳細的介紹虐拓,見百科

FillColor和StrokeColor的區(qū)別

在用CGContext繪制的時候傲武,經(jīng)常需要設置顏色蓉驹,常用的有下面兩種:

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.

用中文來表達揪利,就是一個是填充顏色态兴,一個是描邊顏色。
在iOS中疟位,我們通過NSForegroundColorAttributeNameNSStrokeColorAttributeName這兩個attribute來設置填充顏色瞻润。

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

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


Fill灰色绍撞,Stroke紅色正勒,StrokeWidth=-3

當StrokeWidth為正數(shù)的時候,F(xiàn)illColor不生效傻铣,僅有StrokeColor章贞,如下:


Fill灰色,Stroke紅色非洲,StrokeWidth=3

當StrokeWidth為0的時候鸭限,F(xiàn)illColor生效,StrokeColor無效果两踏,如下:
Fill灰色败京,Stroke紅色,StrokeWidth=0

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

圖文混排其實是排版時插入一個特殊的空白字符梦染,并設定字符的寬高為特定size喧枷,預留對應size的空白,再算出對應位置的坐標弓坞,繪制上對應的圖片隧甚。


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

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;
}

實際上我們只設置了ascent為圖片的高族吻,width為圖片的寬帽借,descent設置的為0。但是一個CTLine往往包括多個文字超歌,整行的descent實際上是所有字符的descent的最大值砍艾。同樣的,一行的ascent也是行內(nèi)所有字符的ascent最大值巍举。

總結(jié)

本文詳細介紹了CoreText的基礎概念以及實際運用脆荷,如果理解完CoreText框架和文字排版、圖文混排等知識懊悯,那么已經(jīng)足夠支撐做起一個閱讀器啦蜓谋,恭喜你。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末炭分,一起剝皮案震驚了整個濱河市桃焕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌捧毛,老刑警劉巖观堂,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件让网,死亡現(xiàn)場離奇詭異,居然都是意外死亡师痕,警方通過查閱死者的電腦和手機寂祥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來七兜,“玉大人丸凭,你說我怎么就攤上這事⊥笾” “怎么了惜犀?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長狠裹。 經(jīng)常有香客問我虽界,道長,這世上最難降的妖魔是什么涛菠? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任莉御,我火速辦了婚禮,結(jié)果婚禮上俗冻,老公的妹妹穿的比我還像新娘礁叔。我一直安慰自己,他們只是感情好迄薄,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布琅关。 她就那樣靜靜地躺著,像睡著了一般讥蔽。 火紅的嫁衣襯著肌膚如雪涣易。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天冶伞,我揣著相機與錄音新症,去河邊找鬼。 笑死响禽,一個胖子當著我的面吹牛徒爹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播金抡,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼瀑焦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了梗肝?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤铺董,失蹤者是張志新(化名)和其女友劉穎巫击,沒想到半個月后禀晓,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡坝锰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年粹懒,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片顷级。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡凫乖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出弓颈,到底是詐尸還是另有隱情帽芽,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布翔冀,位于F島的核電站导街,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏纤子。R本人自食惡果不足惜搬瑰,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望控硼。 院中可真熱鬧泽论,春花似錦、人聲如沸卡乾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽说订。三九已至抄瓦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間陶冷,已是汗流浹背钙姊。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留埂伦,地道東北人煞额。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像沾谜,于是被迫代替她去往敵國和親膊毁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

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

  • 系列文章: CoreText實現(xiàn)圖文混排 CoreText實現(xiàn)圖文混排之點擊事件 CoreText實現(xiàn)圖文混排之文...
    老司機Wicky閱讀 40,167評論 221 432
  • 0. 基本知識準備 0.1 字形( Glyph)基本了解 基礎原點(Origin)首先是位于基線上處于基線最左側(cè)的...
    破弓閱讀 3,149評論 4 24
  • CoreText是一個進階的比較底層的布局文本和處理字體的技術(shù)基跑,CoreText API在OS X v10.5 和...
    smalldu閱讀 13,446評論 18 129
  • 最近在網(wǎng)上看了一些大牛的文章婚温,自己也試著寫了一下,感覺圖文混排真的很強大媳否。 廢話不多說栅螟,開始整 先上效果圖跟代碼荆秦,...
    AllureJM閱讀 991評論 0 1
  • 今天來畫個Q版的小姐姐吧!這個小姐姐是我的親姐姐哦力图!姐姐懷孕了步绸,身體里還住著一個可愛的小寶寶!所以我要把姐姐最幸福...
    邢小杰閱讀 1,225評論 6 13