iOS 文本相關-CoreText

Core Text is an advanced, low-level technology for laying out text and handling fonts. Core Text works directly with Core Graphics (CG), also known as Quartz, which is the high-speed graphics rendering engine that handles two-dimensional imaging at the lowest level in OS X and iOS.
CoreText是一種高級的底層技術, 用于布局文本和處理字體。CoreText直接與Core Graphics (CG) 一起工作, 也稱為Quartz, 它是在 OS X 和 iOS 的最底層的處理二維成像的高速圖形渲染引擎。

相關知識

字體(Font)

字體表示的是同一大小,同一樣式(Style)字形的集合。從這個意義上來說,當我們為文字設置粗體,斜體時其實是使用了另外一種字體(下劃線不算)。

字符(Character)和字形(Glyphs)

排版過程中一個重要的步驟就是從字符到字形的轉換,字符表示信息本身,一般就是指某種編碼,如Unicode編碼,而字形是它的圖形表現形式,指字符編碼對應的圖片。但是他們之間不是一一對應關系,同個字符的不同字體族,不同字體大小,不同字體樣式都對應了不同的字形。而由于連寫(Ligatures)的存在,多個字符也會存在對應一個字形的情況。


image.png

字形的各個參數(字形度量Glyph Metrics)

image.png
  • 邊界框 bbox(bounding box)
    這是一個假想的框子,它盡可能緊密的裝入字形。
  • 基線(baseline)
    一條假想的線,一行上的字形都以此線作為上下位置的參考,在這條線的左側存在一個點叫做基線的原點,
  • 上行高度(ascent)
    從原點到字體中最高(這里的高深都是以基線為參照線的)的字形的頂部的距離,ascent是一個正值
  • 下行高度(descent)
    從原點到字體中最深的字形底部的距離,descent是一個負值(比如一個字體原點到最深的字形的底部的距離為2,那么descent就為-2)
  • 行距(line gap)
    line gap也可以稱作leading(其實準確點講應該叫做External leading),行高line Height則可以通過 ascent + |descent| + linegap 來計算。
  • 字間距(Kerning)
    字與字之間的距離,為了排版的美觀,并不是所有的字形之間的距離都是一致的,但是這個基本步影響到我們的文字排版。
  • 基礎原點(Origin)
    基線上最左側的點。
image.png

紅框高度既為當前行的行高,綠線為baseline,綠色到紅框上部分為當前行的最大Ascent,綠線到黃線為當前行的最大Desent,而黃框的高即為行間距。
由此可以得出:lineHeight = Ascent + |Decent| + Leading

坐標系

CoreText一開始是定位于桌面的排版系統,使用了傳統的原點在左下角的坐標系,所以它在繪制文本的時候都是參照左下角的原點進行繪制的。
Core Graphic 中的context也是以左下角為原點的,但通過UIGraphicsGetCurrentContext()獲得的當前context是已經被處理過的了,如果啥也不處理,直接在當前context上進行CoreText繪制,文字是鏡像且上下顛倒,因此在CoreText中的布局需要對其坐標系進行轉換,。一般做法是直接獲取當前上下文。并將當前上下文的坐標系轉換為CoreText坐標系,再將布局好的CoreText繪制到當前上下文中即可。

使用以下的代碼進行坐標系的轉換

// 坐標系調整
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(context, CGAffineTransformIdentity);//設置字形變換矩陣為CGAffineTransformIdentity,也就是說每一個字形都不做圖形變換

//    CGContextTranslateCTM(context, 0, rect.size.height);
//    CGContextScaleCTM(context, 1, -1);
CGContextConcatCTM(context, CGAffineTransformMake(1, 0, 0, -1, 0, rect.size.height));

CoreText框架中重要的類

image.png
  • CFAttributedStringRef
    屬性字符串,用于存儲需要繪制的文字字符和字符屬性

  • CTFramesetterRef
    framesetter對應的類型是 CTFramesetter,通過CFAttributedStringRef進行初始化,它作為CTFrame對象的生產工廠,負責根據path生產對應的CTFrame;

  • CTFrame
    CTFrame是可以通過CTFrameDraw函數直接繪制到context上的,也可以在繪制之前,操作CTFrame中的CTLine,進行參數的微調。

  • CTLine
    在CTFrame內部是由多個CTLine來組成的,每個CTLine代表一行;可以看做Core Text繪制中的一行的對象 通過它可以獲得當前行的line ascent,line descent ,line leading,還可以獲得Line下的所有Glyph Runs;

  • CTRun
    或者叫做 Glyph Run,每個CTLine又是由多個CTRun組成的,每個CTRun代表一組顯示風格一致的文本,是一組共享相同attributes(屬性)的字形的集合體;

CTFrame是指整個該UIView子控件的繪制區域,CTLine則是指每一行,CTRun則是每一段具有一樣屬性的字符串。比如某段字體大小、顏色都一致的字符串為一個CTRun,CTRun不可以跨行,不管屬性一致或不一致。通常的結構是每一個CTFrame有多個CTLine,每一個CTLine有多個CTRun。
你不需要自己創建CTRun,Core Text將根據NSAttributedString的屬性來自動創建CTRun。每個CTRun對象對應不同的屬性,正因此,你可以自由的控制字體、顏色、字間距等等信息。

image.png
CTFrameRef

如上圖中最外層(藍色框)的內容區域對應的就是CTFrame,繪制的是一整段的內容,方法的匯總

/*
返回CTFrameRef對象的CFType
類型ID是一個整數,用于標識Core Foundation對象“所屬”的不透明類型。
*/
CFTypeID CTFrameGetTypeID( void );
/*
返回最初要求填充框架的字符范圍。
*/
CFRange CTFrameGetStringRange(CTFrameRef frame )

/*
返回實際適合frame的字符范圍。
此函數可用于級聯frame,因為它返回可以在Frame中看到的字符范圍。 下一個Frame將從該Frame結束處開始。
*/
CFRange CTFrameGetVisibleStringRange( CTFrameRef frame ) ;
// 返回用于創建frame的path
CGPathRef CTFrameGetPath(CTFrameRef frame )
// 返回用于創建frame的Attributes。
CFDictionaryRef _Nullable CTFrameGetFrameAttributes(CTFrameRef frame );

// 返回frame中的CTLine的數組。
CFArrayRef CTFrameGetLines(CTFrameRef frame )

/*
獲取CTFrame中每一行的起始坐標,結果保存在返回參數中

將一些CGPoint結構體復制到原點緩沖區中。復制到原點緩沖區的最大行原點數是range參數的長度。
frame
您要從中獲取線原點數組的frame。
range
復制的線起點范圍。如果range的長度為0,則將從range的location起到最后的行原點。
origins
將原點復制到的緩沖區。具有至少與range.length一樣多的元素。此數組中的每個CGPoint都是CTFrameGetLines返回的相對于路徑邊界框原點的線數組中對應線的原點,可以從CGPathGetPathBoundingBox獲得該點。
*/
void CTFrameGetLineOrigins(CTFrameRef frame,CFRange range,CGPoint origins[_Nonnull] ) 

//將整個frame繪制到上下文中。
void CTFrameDraw( CTFrameRef frame, CGContextRef context )
CTFramesetterRef
// 返回框架設置器對象的CFType
CFTypeID CTFramesetterGetTypeID( void );
/*
通過從CTTypesetterRef創建CTFramesetterRef。
*/
CTFramesetterRef CTFramesetterCreateWithTypesetter(CTTypesetterRef typesetter )

/*
從屬性字符串創建不可變的CTFramesetterRef對象。

生成的CTFramesetterRef可用于通過CTFramesetterCreateFrame調用創建和填充CTFrame。
*/
CTFramesetterRef CTFramesetterCreateWithAttributedString(CFAttributedStringRef attrString )

/*
使用CTFramesetterRef創建不可變CTFrameRef。

創建一個由path參數提供的路徑形狀的,充滿了字形的CTFrame。framesetter將繼續填充frame,直到用完文本或發現不再適合的文本為止。

framesetter
用來創建CTFrame的CTFramesetterRef。
stringRange
創建framesetter的屬性字符串的范圍,該范圍將在適合框架的行中排版。如果ranfe的length部分設置為0,則framesetter將繼續添加行,直到用完文本或空格為止。
path
指定frame形狀的CGPath對象。該路徑可能不是矩形的。
frameAttributes
可以在此處指定控制幀填充過程的其他屬性,如果沒有這樣的屬性,則為NULL。
*/
CTFrameRef CTFramesetterCreateFrame(CTFramesetterRef framesetter, CFRange stringRange, CGPathRef path,CFDictionaryRef _Nullable frameAttributes )

/*
返回CTFramesetterRef正在使用的CTTypesetterRef對象。

每個CTFramesetterRef在內部使用CTTypesetterRef根據字符串中的字符進行換行和其他上下文分析; 如果調用者想對該CTTypesetterRef執行其他操作,此函數將返回特定CTFramesetterRef正在使用的CTTypesetterRef。

*/
CTTypesetterRef CTFramesetterGetTypesetter( CTFramesetterRef framesetter )
/*
確定字符串范圍所需的 CTFrame大小。

framesetter
用于測量框架尺寸。
stringRange
CTFrame大小適用的字符串范圍。字符串范圍是指用于創建framesetter的字符串范圍。如果range的length部分設置為0,則framesetter將繼續添加line,直到用完文本或空格為止。
frameAttributes
控制CTFrame填充過程的其他屬性,如果沒有這樣的屬性,則為NULL。
constraints
CTFrame尺寸受其限制的寬度和高度。任一維度的CGFLOAT_MAX值都表示應將其視為不受約束。
fitRange
返回時,包含實際適合約束大小的字符串范圍。
*/
CGSize CTFramesetterSuggestFrameSizeWithConstraints( CTFramesetterRef framesetter,CFRange stringRange,CFDictionaryRef _Nullable frameAttributes,CGSize constraints,  CFRange * _Nullable fitRange )
CTLine

上圖紅色框中的內容就是CTLine,上圖一共有三個CTLine對象

// 返回CTLine對象的Core Foundation類型標識符。
CFTypeID CTLineGetTypeID( void );
/*
直接從屬性字符串創建單個不可變的行對象。

允許客戶端創建行而不創建CTTypesetter對象。  不需要換行符的簡單元素(例如文本標簽)可以使用此API。
*/
CTLineRef CTLineCreateWithAttributedString(CFAttributedStringRef attrString );

/*
從現有行創建截斷的行。
line
創建截斷行的行。
width
截斷開始處的寬度。如果line的寬度大于width,則該行將被截斷。
truncationType 截斷類型
truncationToken
該CTLineRef被添加到發生截斷的位置,以指示該行已被截斷。 通常,截斷令牌是省略號(U + 2026)。 如果此參數設置為NULL,則不使用截斷令牌,僅將行切斷。
*/ 
CTLineRef _Nullable CTLineCreateTruncatedLine( CTLineRef line, double width, CTLineTruncationType truncationType, CTLineRef _Nullable truncationToken )
typedef CF_ENUM(uint32_t, CTLineTruncationType) {
    kCTLineTruncationStart  = 0,// 在行的開頭截斷,使結束部分可見。
    kCTLineTruncationEnd    = 1, // 在行尾截斷,使開始部分可見。
    kCTLineTruncationMiddle = 2 // 在行的中間截斷,使開始和結束部分都可見。
};
/*
從現有線創建對齊線。
line
從中創建對齊線的線。
justificationFactor
設置為1.0或更大時,將執行完全對齊。 如果將此參數設置為小于1.0,則執行不同程度的部分對齊。 如果將其設置為0或小于0,則不進行對齊。
justificationWidth
所得行對齊的寬度。 如果justificationWidth小于線條的實際寬度,則執行負對齊(即,字形被擠壓在一起)。
*/
CTLineRef _Nullable CTLineCreateJustifiedLine( CTLineRef line, CGFloat justificationFactor, double justificationWidth )
// 返回line的總字形計數。
// 總字形計數等于line的字形的總和。
CFIndex CTLineGetGlyphCount( CTLineRef line );

// 返回運行時line對象的字形數組。獲取CTLine包含的所有的CTRun
CFArrayRef CTLineGetGlyphRuns( CTLineRef line )
// 獲取該行中最初產生字形的字符范圍。
CFRange CTLineGetStringRange( CTLineRef line )
/*
獲取繪制平齊文本所需的筆偏移量。
line
從中獲得平齊位置的線。
flushFactor
flushFactor等于或小于0表示左沖洗。 等于或大于1.0的flushFactor表示正確沖洗。 沖洗系數在0到1.0之間表示中心沖洗程度不同,值為0.5表示完全中心沖洗。
flushWidth
指定沖洗操作的寬度。
*/
double CTLineGetPenOffsetForFlush( CTLineRef line, CGFloat flushFactor, double flushWidth )
//畫一條完整的線。
/*
這是一個方便的函數,因為可以通過運行字形,從中刪除字形并調用諸如CGContextShowGlyphsAtPositions之類的功能來逐行繪制線。
*/
void CTLineDraw(CTLineRef line, CGContextRef context )
/*
計算線條的印刷范圍。
line
計算其印刷范圍的線。
ascent
線的上升。 如果不需要,可以將此參數設置為NULL。
descent
線的下降。 如果不需要,可以將此參數設置為NULL。
leading
該行的前導。 如果不需要,可以將此參數設置為NULL。
*/
double CTLineGetTypographicBounds( CTLineRef line, CGFloat * _Nullable ascent, CGFloat * _Nullable descent, CGFloat * _Nullable leading )

/*
計算一條線的rect。
CTLineBoundsOptions
*/
CGRect CTLineGetBoundsWithOptions( CTLineRef line,CTLineBoundsOptions options )

//傳遞0(無選項)將返回印刷范圍,
typedef CF_OPTIONS(CFOptionFlags, CTLineBoundsOptions) {
    kCTLineBoundsExcludeTypographicLeading  = 1 << 0,//傳遞此選項可排除印刷前導。
    kCTLineBoundsExcludeTypographicShifts   = 1 << 1,// 傳遞此選項可忽略由于定位(例如字距調整或基線對齊)而引起的跨流移位。
    kCTLineBoundsUseHangingPunctuation      = 1 << 2, // 通常,線邊界包括所有字形;將此選項傳遞為將標準標點懸掛在行的任何一端,視為完全懸掛。
    kCTLineBoundsUseGlyphPathBounds         = 1 << 3, // 傳遞此選項以使用字形路徑范圍,而不是默認的印刷范圍。
    kCTLineBoundsUseOpticalBounds           = 1 << 4, // 傳遞此選項以使用光學范圍。此選項優先 kCTLineBoundsUseGlyphPathBounds。
    kCTLineBoundsIncludeLanguageExtents     = 1 << 5,// 傳遞此選項可根據各種語言的通用字形序列添加額外的空間。該結果打算在繪制時使用,以避免可能由于印刷范圍引起的剪裁。與kCTLineBoundsUseGlyphPathBounds一起使用時,此選項無效。
};

/*
返回行的尾隨空白寬度。根據尾隨空白寬度創建線條可能導致線條實際上比所需寬度更長。 
由于不可見空格而通常這不是問題,但此函數可用于確定由于尾隨空格而導致的行寬量。
*/
double CTLineGetTrailingWhitespaceWidth( CTLineRef line )

/*
計算線條的圖像邊界。
line
計算圖像邊界的線。
context
計算圖像邊界的上下文。 這是必需的,因為上下文中可能包含一些設置,這些設置會導致圖像邊界發生變化。
*/
CGRect CTLineGetImageBounds(CTLineRef line,CGContextRef _Nullable context )

/*
執行命中測試。用于確定鼠標單擊或其他事件的字符串索引。 該字符串索引對應于下一個字符應插入的字符。 
line
正在檢查的線。
position
鼠標單擊相對于線條原點的位置。
*/ 
CFIndex CTLineGetStringIndexForPosition( CTLineRef line, CGPoint position );

/*
確定字符串索引的圖形偏移量。獲取CTRun的起始位置

此函數返回一個或多個與字符串索引對應的圖形偏移,適用于相鄰線之間的移動或繪制自定義插入記號。
為了在相鄰的線之間移動,可以針對兩條線的任何相對壓痕來調整主偏移量。
構造x值為偏移,其y值為0.0的CGPoint適合傳遞給CTLineGetStringIndexForPosition。
對于繪制自定義插入符號,返回的主偏移量對應于插入符號的一部分,該部分代表字符的視覺插入位置,該字符的方向與行的書寫方向匹配。
line
請求偏移量的行。
charIndex
對應于所需位置的字符串索引。
secondaryOffset
在輸出時,沿charIndex的基線的輔助偏移量。當單個插入符足以容納字符串索引時,該值將與主偏移量相同,主偏移量是該函數的返回值。可能為NULL。
*/ 
CGFloat CTLineGetOffsetForStringIndex( CTLineRef line, CFIndex charIndex, CGFloat * _Nullable secondaryOffset )

// 枚舉一行中字符的插入符偏移量。
void CTLineEnumerateCaretOffsets(  CTLineRef line,  void (^block)(double offset, CFIndex charIndex, bool leadingEdge, bool* stop) )
CTRun

如上圖綠色框中的內容就是CTRun,每一行中相同格式的一塊內容是一個CTRun,一行中可以存在多個CTRun

// 返回CTRunRef對象的Core Foundation類型標識符。
CFTypeID CTRunGetTypeID( void )
// 獲取CTRunRef的字形計數。
CFIndex CTRunGetGlyphCount( CTRunRef run ) 
/*返回用于創建字形運行的屬性字典。
獲取到的內容是通過`CFAttributedStringSetAttribute`方法設置給屬性字符串的NSDictionary,
key為`kCTRunDelegateAttributeName`,值為`CTRunDelegateRef`*/
CFDictionaryRef CTRunGetAttributes( CTRunRef run )
/*
CTRunRef具有可用于加快某些操作的狀態。 
知道CTRunRef字形的方向和順序可以幫助進行字符串索引分析,
而知道位置是否引用身份文本矩陣可以避免進行昂貴的比較。 
提供此狀態是為了方便,因為此信息不是嚴格必需的,但在某些情況下可能會有所幫助。
*/
CTRunStatus CTRunGetStatus(CTRunRef run )
//用于指示運行的處置。
typedef CF_OPTIONS(uint32_t, CTRunStatus)
{
    kCTRunStatusNoStatus = 0,// 沒有特殊屬性。
    kCTRunStatusRightToLeft = (1 << 0),//設置后,運行從右到左。
/*
設置后,游程已以某種方式重新排序,使得與字形關聯的字符串索引不再嚴格增加(對于從左至右游程)或減少(對于從右至左游程)。
*/
    kCTRunStatusNonMonotonic = (1 << 1),
    kCTRunStatusHasNonIdentityMatrix = (1 << 2) //設置后,運行需要在當前CG上下文中設置特定的文本矩陣才能正確繪制。
};
/*
返回CTRunRef中存儲的字形數組的直接指針。
字形數組的長度等于CTRunGetGlyphCount返回的值。 即使流中包含字形,調用者也應為此函數做好準備以返回NULL。 如果此函數返回NULL,則調用者必須分配自己的緩沖區并調用CTRunGetGlyphs以獲取字形。
*/
const CGGlyph * _Nullable CTRunGetGlyphsPtr(
    CTRunRef run )

/*
將一系列字形復制到用戶提供的緩沖區中。
run
從其復制字形的CTRunRef。
range
要復制的字形范圍。 如果范圍的長度設置為0,則復制操作將從范圍的開始索引繼續到運行結束。
buffer
字形復制到的緩沖區。 必須至少將緩沖區分配給范圍長度指定的值。
*/
void CTRunGetGlyphs( CTRunRef run, CFRange range, CGGlyph buffer[_Nonnull] )

/*
返回CTRunRef中存儲的字形位置數組的直接指針。

相對于包含CTRunRef的CTLineRef的原點。CTRunRef中字形的位置
 位置數組的長度等于CTRunGetGlyphCount返回的值。 
即使流中包含字形,調用者也應為此函數做好準備以返回NULL。 
如果此函數返回NULL,則調用者必須分配自己的緩沖區并調用CTRunGetPositions以獲取字形位置。
*/ 
const CGPoint * _Nullable CTRunGetPositionsPtr( CTRunRef run )

/*
返回CTRunRef中存儲的 字形提前 數組的直接指針。

數組的長度等于CTRunGetGlyphCount返回的值。 
即使流中包含字形,調用者也應為此函數做好準備以返回NULL。 
如果此函數返回NULL,則調用者需要分配自己的緩沖區并調用CTRunGetAdvances以獲取。 請注意,僅前進就不足以在行中正確定位字形,因為CTRunRef可能具有非同一性矩陣,或者CTline中的初始字形可能具有非零原點。 調用者應考慮改用positions代替。
*/ 
const CGSize * _Nullable CTRunGetAdvancesPtr( CTRunRef run )
/*
將一定范圍的字形前進復制到用戶提供的緩沖區中。

run
復制的CTRunRef。
range
希望復制的字形擴展范圍。 如果范圍的長度設置為0,則復制操作將從范圍的開始索引繼續到運行結束。
buffer
字形前進到的緩沖區被復制。 必須至少將緩沖區分配給范圍長度指定
*/
void CTRunGetAdvances( CTRunRef run,  CFRange range, CGSize buffer[_Nonnull] )
/*
返回運行中存儲的字符串索引的直接指針。

索引是最初生成CTRunRef的字形的字符索引。 它們可用于將CTRunRef中的字形映射到后備存儲中的字符。 
字符串索引數組的長度將等于CTRunGetGlyphCount返回的值。 
即使流中包含字形,調用者也應為此函數做好準備以返回NULL。 
如果此函數返回NULL,則調用者必須分配自己的緩沖區并調用CTRunGetStringIndices來獲取索引。
*/
const CFIndex * _Nullable CTRunGetStringIndicesPtr(CTRunRef run )
/*
將一系列字符串索引復制到用戶提供的緩沖區中。
*/
void CTRunGetStringIndices( CTRunRef run,CFRange range,CFIndex buffer[_Nonnull] )

// 獲取CTRunRef最初產生字形的字符范圍。獲取CTRun字符串的Range
CFRange CTRunGetStringRange( CTRunRef run )

/*
獲取CTRun的繪制屬性`ascent`、`desent`,返回值是CTRun的寬度:(印刷范圍)。

run
計算印刷范圍的CTRunRef。
range
要測量的CTRunRef范圍。 如果范圍的長度設置為0,則測量操作將從范圍的起始索引繼續到運行結束。
ascent
運行上升。 如果不需要,可以將其設置為NULL。
descent
運行下降。 如果不需要,可以將其設置為NULL。
leading
運行的領先。 如果不需要,可以將其設置為NULL。
*/
double CTRunGetTypographicBounds(CTRunRef run, CFRange range, CGFloat * _Nullable ascent,CGFloat * _Nullable descent,  CGFloat * _Nullable leading )

/*
計算字形范圍的圖像邊界。
run
計算圖像邊界的CTRunRef。
context
正在計算圖像邊界的上下文。 這是必需的,因為上下文中可能包含一些設置,這些設置會導致圖像邊界發生變化。
range
要測量的CTRunRef范圍。 如果范圍的長度設置為0,則測量操作將從范圍的起始索引繼續到CTRunRef結束。
*/
CGRect CTRunGetImageBounds(CTRunRef run, CGContextRef _Nullable context,CFRange range )
// 返回繪制此CTRunRef所需的文本矩陣。
// 為了在CTRunRef中正確繪制字形,應將此函數返回的CGAffineTransform的字段tx和ty設置為當前文本位置。
CGAffineTransform CTRunGetTextMatrix(
    CTRunRef run )
/*
將一定范圍的基礎前進和起點復制到用戶提供的緩沖區中。
CTRunRef的基本前進和起點確定其字形的位置,但在用于繪制之前需要進行其他處理。
與CTRunGetAdvances返回的前進類似,基本前進是從字形的原點到下一個字形的原點的位移,除了基本前進不包括字體布局表相對于另一個字形所做的任何定位(例如相對于其基準的標記)。
當前字形的原點相對于起始位置的偏移量決定了字形的實際位置,當前字形的基本前進距離相對于起始位置的偏移量確定了下一個字形的位置。

runRef
包含要復制的基本進度和起點的CTRunRef。
range
要復制的值的范圍。如果范圍的長度設置為0,則復制操作將從范圍的起始索引繼續到運行結束。
advancesBuffer
基本行進將被復制到的緩沖區,或者為NULL。如果不為NULL,則緩沖區必須允許至少與范圍長度指定的元素一樣多的元素。
originsBuffer
將原點復制到的緩沖區,或者為NULL。如果不為NULL,則緩沖區必須允許至少與范圍長度指定的元素一樣多的元素。
*/
void CTRunGetBaseAdvancesAndOrigins( CTRunRef runRef, CFRange range, CGSize advancesBuffer[_Nullable], CGPoint originsBuffer[_Nullable] )

/*
繪制完整的CTRunRef或一部分CTRunRef。

range
要繪制的部分。 如果范圍的長度設置為0,則繪制操作將從范圍的起始索引繼續到運行結束。
*/
void CTRunDraw(CTRunRef run, CGContextRef context,CFRange range )
CTRunDelegate

CTRunDelegate和CTRun是緊密聯系的,CTFrame初始化的時候需要用到的圖片信息是通過CTRunDelegate的callback獲得到的,

// 返回CTRunDelegate對象的CFType。
CFTypeID CTRunDelegateGetTypeID( void )
/* 定義釋放CTRunDelegate對象時調用的函數的指針。

refCon
創建CTRunDelegate時,提供給CTRunDelegateCreate函數的引用常數
*/
typedef void (*CTRunDelegateDeallocateCallback) (void * refCon );
/*
指向函數的指針,該函數確定CTRun中字形的印刷升序。
*/
typedef CGFloat (*CTRunDelegateGetAscentCallback) (void * refCon );
/*
指向函數的指針,該函數確定CTRun中字形的印刷降序。
*/
typedef CGFloat (*CTRunDelegateGetDescentCallback) ( void * refCon );
// 指向函數的指針,該函數確定運行中字形的印刷寬度。
typedef CGFloat (*CTRunDelegateGetWidthCallback) (void * refCon );
/*
創建CTRunDelegateRef的不可變實例。需要傳遞CTRunDelegateCallbacks對象,使用CFAttributedStringSetAttribute方法把CTRunDelegate對象和NSAttributedString對象綁定,在CTFrame初始化的時候回調用CTRunDelegate對象里面CTRunDelegateCallbacks對象的回調方法返回`Ascent`、`Descent`、`Width`信息

callbacks
一個結構,它包含指向此運行委托的回調的指針。
*/
CTRunDelegateRef _Nullable CTRunDelegateCreate(const CTRunDelegateCallbacks* callbacks, void * _Nullable refCon )

typedef struct
{
    CFIndex                         version;
    CTRunDelegateDeallocateCallback dealloc;
    CTRunDelegateGetAscentCallback  getAscent;
    CTRunDelegateGetDescentCallback getDescent;
    CTRunDelegateGetWidthCallback   getWidth;
} CTRunDelegateCallbacks;

// 返回 CTRunDelegate 的“ refCon”值。
void * CTRunDelegateGetRefCon(CTRunDelegateRef runDelegate )

繪制操作

簡單繪制文字
image.png
- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];
    // 坐標系調整
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextConcatCTM(context, CGAffineTransformMake(1, 0, 0, -1, 0, rect.size.height));
    
    // 路徑
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, rect);
    
    // 繪制的內容屬性字符串
     NSDictionary *attributes = @{NSFontAttributeName: [UIFont systemFontOfSize:18],
                                  NSForegroundColorAttributeName: [UIColor blueColor]
                                  };
    NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:@"你的回話凌亂著 在這個時刻\
我想起噴泉旁的白鴿 甜蜜散落了\
情緒莫名的拉扯 我還愛你呢\
而你斷斷續續唱著歌 假裝沒事了\
時間過了 走了 愛情面臨選擇\
你冷了 倦了 我哭了\
離開時的不快樂 你用卡片手寫著\
有些愛只給到這 真的痛了" attributes:attributes];

    // 使用NSMutableAttributedString創建CTFrame,并繪制
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
    
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
    CTFrameDraw(frame, context);
}

效果


image.png
豎版文本繪制

待續

繪制圖片

Core Text本身并不支持圖片繪制,圖片的繪制需要通過Core Graphics進行。
通過設置CTRun為圖片的繪制中留出適當的空間,需要使用到CTRunDelegate了,CTRunDelegate作為CTRun相關屬性或操作擴展的一個入口,使得我們可以對CTRun做一些自定義的行為。
為圖片留位置的方法就是加入一個空白的CTRun,自定義其ascent,descent,width等參數,使得繪制文本的時候留下空白位置給相應的圖片。然后圖片在相應的空白位置上使用Core Graphics接口進行繪制。
因此繪制圖片最重要的一個步驟就是計算圖片所在的位置,最后是在drawRect繪制方法中使用CGContextDrawImage方法進行繪制圖片即可

計算圖片位置流程圖
image.png

效果圖


image.png
主要代碼

創建需要渲染的屬性字符串,和渲染

- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1, -1);

    NSMutableAttributedString *attri = [[NSMutableAttributedString alloc] initWithString:@"每個人都認為他自己至少具有一種主要的美德,我的美德是:我是我所結識過的少有的幾個誠實人中間的一個。"];

    [attri appendAttributedString:[self imageAttributeString]];
    
    [attri appendAttributedString:[[NSAttributedString alloc] initWithString:@"人們的品行有的好像建筑在堅硬的巖石上,有的好像建筑在泥沼里,不過超過一定的限度,我就不在乎它建在什么之上了。"]];
    
    [attri addAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:17.0f]} range:NSMakeRange(0, attri.length)];
    // 103 × 150
    
    
    CTFrameRef frame = [self ctFrameWithAttributeString:attri.copy frame:rect];
    
    [self drawWithFrame:frame context:context];
}

創建圖片的屬性字符串

/*
1. 創建CTRunDelegate對象,傳遞callback和參數的代碼,
創建CTFrame對象的時候會通過CTRunDelegate中callbak的幾個回調方法
getDescent、getDescent、getWidth返回繪制的圖片的信息,
方法getDescent、getDescent、getWidth中的參數是
CTRunDelegateCreate(&callback, (__bridge_retained void *)(metaData))方法中的metaData參數,
特別地,這里的參數需要把所有權交給CF對象,而不能使用簡單的橋接,防止ARC模式下的OC對象自動釋放,
在方法getDescent、getDescent、getWidth訪問會出現BAD_ACCESS的錯誤
*/
- (NSAttributedString *)imageAttributeString {
    // 1 創建CTRunDelegateCallbacks
    CTRunDelegateCallbacks callback;
    memset(&callback, 0, sizeof(CTRunDelegateCallbacks));
    callback.getAscent = getAscent;
    callback.getDescent = getDescent;
    callback.getWidth = getWidth;
    
    // 2 創建CTRunDelegateRef
    CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callback, (__bridge void * _Nullable)(self.imageItem));
    
    // 3 設置占位使用的圖片屬性字符串
    // 參考:https://en.wikipedia.org/wiki/Specials_(Unicode_block)  U+FFFC  OBJECT REPLACEMENT CHARACTER, placeholder in the text for another unspecified object, for example in a compound document.

    unichar objectReplacementChar = 0xFFFC;

    NSMutableAttributedString *imagePlaceHolderAttributeString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithCharacters:&objectReplacementChar length:1]];
    
    // 4 設置RunDelegate代理
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imagePlaceHolderAttributeString, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate);
    
    CFRelease(runDelegate);
    
    return imagePlaceHolderAttributeString;
}


// MARK: - CTRunDelegateCallbacks 回調方法
static CGFloat getAscent(void *ref) {
    
    float ascent = [(__bridge ImageDrawItem *)ref ascent];
    return ascent;
}
static CGFloat getDescent(void *ref) {
    float descent = [(__bridge ImageDrawItem *)ref descent];
    return descent;
}

static CGFloat getWidth(void *ref) {
    float width = [(__bridge ImageDrawItem *)ref width];
    return width;
}

根據屬性字符串和顯示范圍創建CTFrameRef

- (CTFrameRef)ctFrameWithAttributeString:(NSAttributedString *)attributeString frame:(CGRect)frame {
    // 繪制區域
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, (CGRect){{0, 0}, frame.size});
    
    // 使用NSMutableAttributedString創建CTFrame
    CTFramesetterRef ctFramesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeString);
    CTFrameRef ctFrame = CTFramesetterCreateFrame(ctFramesetter, CFRangeMake(0, attributeString.length), path, NULL);
    
    CFRelease(ctFramesetter);
    CFRelease(path);
    return ctFrame;
}

計算圖片所在的位置的代碼:

- (void)drawWithFrame:(CTFrameRef) frame context:(CGContextRef)context {
    
    // CTFrameGetLines獲取CTFrame內容的行
    NSArray *lines = (NSArray *)CTFrameGetLines(frame);
    // CTFrameGetLineOrigins獲取每一行的起始點,保存在lineOrigins數組中
    CGPoint lineOrigins[lines.count];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
    // 遍歷行
    for (int i = 0; i < lines.count; i++) {
        CTLineRef line = (__bridge CTLineRef)lines[i];
        
        NSArray *runs = (NSArray *)CTLineGetGlyphRuns(line);
        // 遍歷runs
        for (int j = 0; j < runs.count; j++) {
            CTRunRef run = (__bridge CTRunRef)(runs[j]);
            // 獲得屬性
            NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes(run);
            if (!attributes) {
                continue;
            }
            // 從屬性中獲取到創建屬性字符串使用CFAttributedStringSetAttribute設置的delegate值
            CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];
            if (!delegate) {
                continue;
            }
            
            
            
            // CTRunDelegateGetRefCon方法從delegate中獲取使用CTRunDelegateCreate初始時候設置的元數據
            DrawItem *metaData = (DrawItem *)CTRunDelegateGetRefCon(delegate);
            
            
            if (!metaData) {
                continue;
            }
            
            // 找到代理則開始計算圖片位置信息
            CGFloat ascent;
            CGFloat desent;
            // 可以直接從metaData獲取到圖片的寬度和高度信息
            //也可以通過CTRunGetTypographicBounds函數獲得
            CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &desent, NULL);
            
            // CTLineGetOffsetForStringIndex獲取CTRun的起始位置
            CGFloat xOffset = lineOrigins[i].x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
            CGFloat yOffset = lineOrigins[i].y;
         
            // 更新ImageItem對象的位置
            if ([metaData isKindOfClass:[ImageDrawItem class]]) {
                ImageDrawItem *imageItem = metaData;
//                [[UIImage imageNamed:imageItem.imageNamed] drawInRect:CGRectMake(xOffset, yOffset, width, ascent + desent)];
                
                CGContextDrawImage(context, CGRectMake(xOffset, yOffset, width, ascent + desent), [UIImage imageNamed:imageItem.imageNamed].CGImage);
            }
        }
    }
    
    // 使用CTFrame在CGContextRef上下文上繪制
    CTFrameDraw(frame, context);

}

CTFrame會根據CTRunDelegateRef的信息空出一片區域去繪制圖片,我們需要通過一些函數,計算出圖片的繪制區域,然后調用CGContextDrawImage函數去繪制圖片

文字環繞圖片

只需要修改path,空出一個區域填圖片充就可以了。

內容高亮和事件處理

事件的處理
點擊事件的處理基本思路是使用CTFrame對象獲取到所有的CTRun對象,遍歷CTRun對象,判斷CTRun元素是否可以點擊

  1. 給NSMutableAttributedString設置自定義屬性,表示這個NSMutableAttributedString對應的CTRun是可以點擊的
  2. CTFrame遍歷CTRun,取出在上一步設置的特殊屬性,計算CTRun最終渲染顯示的位置,記錄可點擊元素的位置

首先創建模型,存儲相關信息

UIKIT_EXTERN NSString * ActionAndHilightedItemKey;

@interface ActionAndHilightedItem : NSObject
@property (nonatomic , strong) void(^action)(void);

@property (nonatomic , strong) UIColor *highlightedBackgroundColor;

@property (nonatomic , strong) UIColor *highlightedColor;

@property (nonatomic , strong) NSMutableArray *frames;

@end

NSMutableAttributedString設置自定義屬性

- (void) appendAttributedString:(NSAttributedString *)str action:(void(^)(void))action highlightedTextColor:(UIColor *)highlightedTextColor highlightedBackgroundColor:(UIColor *)highlightedBackgroundColor{
    
    NSMutableAttributedString *attriStr = [[NSMutableAttributedString alloc] initWithAttributedString:str];
    
    ActionAndHilightedItem *item = [[ActionAndHilightedItem alloc] init];
    
    item.action = action;
    item.highlightedColor = highlightedTextColor;
    item.highlightedBackgroundColor = highlightedBackgroundColor;
    
    [attriStr addAttributes:@{ActionAndHilightedItemKey:item} range:NSMakeRange(0, str.length)];
    
    [self.string appendAttributedString:attriStr];
}

定義兩個屬性來存儲相關的配置

@property (nonatomic , strong) NSMutableSet <ActionAndHilightedItem *>*actionAndHighlightedItems;

@property (nonatomic , strong) ActionAndHilightedItem *currentItem;

繪制代碼

- (void)drawRect:(CGRect)rect{
    [super drawRect:rect];
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1, -1);
    
    if (self.currentItem) {
        [self.string enumerateAttribute:ActionAndHilightedItemKey inRange:NSMakeRange(0, self.string.length) options:0 usingBlock:^(id  _Nullable value, NSRange range, BOOL * _Nonnull stop) {
            
            if (value == self.currentItem) {
             
                if (self.currentItem.highlightedColor != nil) {
                    [self.string addAttributes:@{NSForegroundColorAttributeName:self.currentItem.highlightedColor} range:range];
                }
                
                if (self.currentItem.highlightedBackgroundColor) {
                    [self.string addAttributes:@{NSBackgroundColorAttributeName:self.currentItem.highlightedBackgroundColor} range:range];
                }
                *stop = YES;
            }
        }];
    }
    else{
        
        [self.string addAttributes:@{NSForegroundColorAttributeName:[UIColor blackColor]} range:NSMakeRange(0, self.string.length)];
        [self.string addAttributes:@{NSBackgroundColorAttributeName:[UIColor clearColor]} range:NSMakeRange(0, self.string.length)];
    
    }
    CTFrameRef frame = [DrawTool ctFrameWithAttributeString:self.string.copy frame:rect];
    [self drawWithFrame:frame rect:rect context:context];
}

- (void)drawWithFrame:(CTFrameRef)frame rect:(CGRect)rect context:(CGContextRef)context {
    
    [self.actionAndHighlightedItems removeAllObjects];
    
    // CTFrameGetLines獲取但CTFrame內容的行數
    NSArray *lines = (NSArray *)CTFrameGetLines(frame);
    // CTFrameGetLineOrigins獲取每一行的起始點,保存在lineOrigins數組中
    CGPoint lineOrigins[lines.count];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
    for (int i = 0; i < lines.count; i++) {
        CTLineRef line = (__bridge CTLineRef)lines[i];
    
        NSArray *runs = (NSArray *)CTLineGetGlyphRuns(line);
        for (int j = 0; j < runs.count; j++) {
            CTRunRef run = (__bridge CTRunRef)(runs[j]);
            
            NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes(run);
            if (!attributes) {
                continue;
            }
            
            // 獲取附加的數據->設置鏈接、圖片等元素的點擊效果的位置
            ActionAndHilightedItem *extraData = (ActionAndHilightedItem *)[attributes valueForKey:ActionAndHilightedItemKey];
            if (extraData) {
                // 獲取CTRun的信息
                CGFloat ascent;
                CGFloat desent;
                // 可以直接從metaData獲取到圖片的寬度和高度信息
                CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &desent, NULL);
                CGFloat height = ascent + desent;
                
                // CTLineGetOffsetForStringIndex獲取CTRun的起始位置
                CGFloat xOffset = lineOrigins[i].x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
                
                // lineOrigins[i].y;基于baseline的?
//                CGFloat yOffset = lineOrigins[i].y;
                CGFloat yOffset = rect.size.height - lineOrigins[i].y - ascent;
                
                
                                    // 由于CoreText和UIKit坐標系不同所以要做個對應轉換
                    // CGRect ctClickableFrame = CGRectMake(xOffset, yOffset, width, height);
                    // 將CoreText坐標轉換為UIKit坐標
                CGRect uiKitClickableFrame = CGRectMake(xOffset, yOffset, width, height);
                [extraData.frames addObject:@(uiKitClickableFrame)];
                [self.actionAndHighlightedItems addObject:extraData];
            }
        }
    }
    
    CTFrameDraw(frame, context);
    
}

點擊效果處理
數據處理好了,點時效果是判斷點擊位置是否存在相應配置,如果有則重新繪制
點擊事件的監聽

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
   
    UITouch *touch = touches.anyObject;
    CGPoint point = [touch locationInView:self];
    
    NSArray <ActionAndHilightedItem *> *enumerator = [self.actionAndHighlightedItems objectEnumerator].allObjects;
        
        ActionAndHilightedItem *item = nil;
        
    for (int i = 0; i<enumerator.count; i++) {
        
        item = enumerator[i];
            for (NSInteger i = 0; i<item.frames.count; i++) {
                CGRect frame = [item.frames[i] CGRectValue];
                if (CGRectContainsPoint(frame, point)) {
                    self.currentItem = item;
                    
                    [self setNeedsDisplay];
                    return;
                }
            }
        }
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    UITouch *touch = touches.anyObject;
    CGPoint point = [touch locationInView:self];
    
    if (self.currentItem == nil) {
        return;;
    }
    
    for (NSInteger i = 0; i<self.currentItem.frames.count; i++) {
        CGRect frame = [self.currentItem.frames[i] CGRectValue];
        if (CGRectContainsPoint(frame, point)) {
            
            if(self.currentItem.action){
                self.currentItem.action();
            }
            
            break;
        }
    }
    
    self.currentItem = nil;
    [self setNeedsDisplay];
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.currentItem = nil;
    [self setNeedsDisplay];

}
文字行數限制和顯示更多

文字行數限制

  1. 判斷是否有行數限制,沒有使用默認繪制(CTFrameDraw)即可,
  2. 有行數限制且不是最后一行直接繪制(使用CTLineDraw,并且需要使用CGContextSetTextPosition方法設置繪制文本的位置)
  3. 判斷最后一行的顯示是否會超出,超出則把最后一行的內容截取,拼接“...”在被截取的原始內容之后
    使用CTLineCreateTruncatedLine創建最后一行顯示的內容,返回CTLine對象
    如果設置了截斷標識字符串點擊事件,需要把位置信息進行保存,用于后面的事件處理

- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];
    // 坐標系調整
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextConcatCTM(context, CGAffineTransformMake(1, 0, 0, -1, 0, rect.size.height));

    // 繪制的內容屬性字符串
    NSDictionary *attributes = @{NSFontAttributeName: [UIFont systemFontOfSize:18],
                                  NSForegroundColorAttributeName: [UIColor blueColor]
                                  };
    
    
    NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:@"你的回話凌亂著 在這個時刻\n\
我想起噴泉旁的白鴿 甜蜜散落了\n\
情緒莫名的拉扯 我還愛你呢\n\
而你斷斷續續唱著歌 假裝沒事了\n\
時間過了 走了 愛情面臨選擇\n\
你冷了 倦了 我哭了\n\
離開時的不快樂 你用卡片手寫著\n\
有些愛只給到這 真的痛了" attributes:attributes];

    CTFrameRef frame = [DrawTool ctFrameWithAttributeString:attrStr frame:rect];
    
    
    if (self.showAll) {
        CTFrameDraw(frame, context);
    }
    else{
        NSArray *lines = (NSArray *)CTFrameGetLines(frame);

        // 創建結構體數組,保存行的起始位置
        CGPoint lineOrigins[3];
        // 獲取每一行的起點位置
        CTFrameGetLineOrigins(frame, CFRangeMake(0, 3), lineOrigins);
        for (int lineIndex = 0; lineIndex < 3; lineIndex ++) {
            // 獲得行
            CTLineRef line = (__bridge CTLineRef)(lines[lineIndex]);
            // 獲取行顯示的文本的range
            CFRange range = CTLineGetStringRange(line);
            // 判斷最后一行是否能夠顯示完文字
            if (lineIndex == 2
                       && range.location + range.length < attrStr.length) {
                       // 創建截斷字符串
                
                self.actionModel = [[ActionAndHilightedItem alloc] init];
                __weak NumberOfLinesAndShowMoreView *weaSelf = self;
                self.actionModel.action = ^(){
                    
                    __strong NumberOfLinesAndShowMoreView *self = weaSelf;
                    self.showAll = YES;
                    [self setNeedsDisplay];
                };
                
                
                NSAttributedString *tokenString = [[NSMutableAttributedString alloc] initWithString:@"查看全部" attributes:@{ActionAndHilightedItemKey:self.actionModel ,NSForegroundColorAttributeName:[UIColor blackColor],NSFontAttributeName:[UIFont systemFontOfSize:20]}];
                                
                // 創建 截斷的 CTLine
                CTLineRef truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)tokenString);
                   
                // 截斷的類型
                CTLineTruncationType truncationType = kCTLineTruncationEnd;
                
                double lineW = CTLineGetTypographicBounds(line,NULL,NULL,NULL);
                
                CTLineRef lastLine = CTLineCreateTruncatedLine(line, lineW-0.1 , truncationType, truncationTokenLine);
                                
                                // 添加truncation的位置信息
                NSArray *runs = (NSArray *)CTLineGetGlyphRuns(lastLine);
                
                for (NSInteger runIndex = 0; runIndex<runs.count; runIndex ++) {
                    CTRunRef run = (__bridge CTRunRef)(runs[runIndex]);
                    
                    NSDictionary *dict = CTRunGetAttributes(run);
                    
                    if ([dict objectForKey:ActionAndHilightedItemKey] != nil) {
                        
                        ActionAndHilightedItem *item = dict[ActionAndHilightedItemKey];
                        
                        
                        // 獲取CTRun的信息
                        CGFloat ascent;
                        CGFloat desent;
                        // 可以直接從metaData獲取到圖片的寬度和高度信息
                        CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &desent, NULL);
                        CGFloat height = ascent + desent;
                        
                        // CTLineGetOffsetForStringIndex獲取CTRun的起始位置
                        CGFloat xOffset = lineOrigins[lineIndex].x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
                        
                        // lineOrigins[i].y;基于baseline的?
        //                CGFloat yOffset = lineOrigins[i].y;
                        CGFloat yOffset = rect.size.height - lineOrigins[lineIndex].y - ascent;
                        
                        
                                            // 由于CoreText和UIKit坐標系不同所以要做個對應轉換
                            // CGRect ctClickableFrame = CGRectMake(xOffset, yOffset, width, height);
                            // 將CoreText坐標轉換為UIKit坐標
                        CGRect uiKitClickableFrame = CGRectMake(xOffset, yOffset, width, height);
                        [item.frames addObject:@(uiKitClickableFrame)];
                    }
                    
                }
                CFRelease(truncationTokenLine);
                CGContextSetTextPosition(context, lineOrigins[lineIndex].x, lineOrigins[lineIndex].y);
                
                CTLineDraw(lastLine, context);
                
                CGContextSetTextPosition(context, lineOrigins[lineIndex].x, lineOrigins[lineIndex].y - 30);

                CTLineDraw(line, context);
                                
                            
            } else {
                       
                CGContextSetTextPosition(context, lineOrigins[lineIndex].x, lineOrigins[lineIndex].y);

                CTLineDraw(line, context);
                           
            }
        }
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.isTouchActionModel = NO;
    UITouch *touch = touches.anyObject;
    CGPoint point = [touch locationInView:self];
    
    ActionAndHilightedItem *item = self.actionModel;
        
        
    for (NSInteger i = 0; i<item.frames.count; i++) {
        CGRect frame = [item.frames[i] CGRectValue];
        if (CGRectContainsPoint(frame, point)) {
            self.isTouchActionModel = YES;
            return;
        }
    }
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    UITouch *touch = touches.anyObject;
    CGPoint point = [touch locationInView:self];
    
    if (self.isTouchActionModel == NO) {
        return;;
    }
    
    NSLog(@"開始匹配");
    
    for (NSInteger i = 0; i<self.actionModel.frames.count; i++) {
        CGRect frame = [self.actionModel.frames[i] CGRectValue];
        
        
        
        if (CGRectContainsPoint(frame, point)) {
            NSLog(@"匹配到了");
            
            if(self.actionModel.action){
                self.actionModel.action();
            }
            
            self.isTouchActionModel = NO;
            return;
        }
    }
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.isTouchActionModel = NO;
}
文字排版樣式和效果

排版效果有以下幾種

  • 字體
  • 顏色
  • 陰影
  • 行間距
  • 對齊方式
  • 段間距
    本質上都是設置NSMutableAttributedString的屬性
  • 行間距對其段間距行高是段落屬性,使用kCTParagraphStyleAttributeNamekey設置對應的屬性
  • 陰影使用需要使用CoreGraphics的API CGContextSetShadowWithColor 進行設置的
  • 字體使用kCTFontAttributeName進行設置;顏色使用kCTForegroundColorAttributeName進行設置

kCTParagraphStyleAttributeName

/**
 設置排版樣式
 */
- (void)setStyleToAttributeString:(NSMutableAttributedString *)attributeString {
    CTParagraphStyleSetting settings[] =
    {
        {kCTParagraphStyleSpecifierAlignment,sizeof(self.textAlignment),&_textAlignment},
        {kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(self.lineSpacing),&_lineSpacing},
        {kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(self.lineSpacing),&_lineSpacing},
        {kCTParagraphStyleSpecifierParagraphSpacing,sizeof(self.paragraphSpacing),&_paragraphSpacing},
    };
    CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, sizeof(settings) / sizeof(settings[0]));
    [attributeString addAttribute:(id)kCTParagraphStyleAttributeName
                       value:(__bridge id)paragraphStyle
                       range:NSMakeRange(0, [attributeString length])];
    CFRelease(paragraphStyle);
}

CGContextSetShadowWithColor

if (shadowColor == nil
        || CGSizeEqualToSize(shadowOffset, CGSizeZero)) {
        return;
    }
    CGContextSetShadowWithColor(context,shadowOffset, shadowAlpha, shadowColor.CGColor);
內容大小計算和自動布局

常用有三種方式布局的效果

  • 手動布局手動計算高度
  • 自動布局自動計算高度
  • 自動布局中限制了內容高度

手動布局手動計算高度
計算內容大小需要重寫UIView的方法sizeThatFits,返回一個CGSize。

  • CTLineGetStringRange方法獲取最后一行CTLineRef(如果設置的行數限制需要使用,否則不用這個步驟)的內容顯示的范圍,返回一個CFRange對象
  • CTFramesetterSuggestFrameSizeWithConstraints方法計算指定范圍內容的大小,第二個參數是CFRange類型,在設置行數限制的情況傳遞上面獲取到的CFRange對象,沒有行數限制的情況直接設置一個空的CFRange對象(CFRangeMake(0, 0))

- (CGSize)sizeThatFits:(CGSize)size {
    NSAttributedString *drawString = self.data.attributeStringToDraw;
    if (drawString == nil) {
        return CGSizeZero;
    }
    CFAttributedStringRef attributedStringRef = (__bridge CFAttributedStringRef)drawString;
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attributedStringRef);

    CFRange range = CFRangeMake(0, 0);
    if (_numberOfLines > 0 && framesetter) {
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
        CFArrayRef lines = CTFrameGetLines(frame);
        
        if (nil != lines && CFArrayGetCount(lines) > 0) {
            NSInteger lastVisibleLineIndex = MIN(_numberOfLines, CFArrayGetCount(lines)) - 1;
            CTLineRef lastVisibleLine = CFArrayGetValueAtIndex(lines, lastVisibleLineIndex);
            
            CFRange rangeToLayout = CTLineGetStringRange(lastVisibleLine);
            range = CFRangeMake(0, rangeToLayout.location + rangeToLayout.length);
        }
        CFRelease(frame);
        CFRelease(path);
    }
    
    CFRange fitCFRange = CFRangeMake(0, 0);
    CGSize newSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, range, NULL, size, &fitCFRange);
    if (framesetter) {
        CFRelease(framesetter);
    }
    
    return newSize;
}

自動布局
自動布局會調用intrinsicContentSize方法獲取內容的大小,所以重寫這個方法,這個方法調用sizeThatFits方法獲取到內容的大小,然后返回即可,

// 計算顯示自己需要多大的size
- (CGSize)intrinsicContentSize {
    return [self sizeThatFits:CGSizeMake(self.bounds.size.width, MAXFLOAT)];
}
添加自定義View和設置對齊方式

添加View其實和添加圖片的處理方式很類似,只不過添加圖片我們是使用CG繪圖的方式把圖片繪制在View上,而添加View是使用UIkit的方法addSubview把View添加到View的層級上
添加view
首先定義一個添加View的方法,在該方法中主要是進行數據模型的保存以及生產特殊的占位屬性字符串,然后添加屬性字符串的RunDelegate

- (void)addView:(UIView *)view size:(CGSize)size align:(YTAttachmentAlignType)align clickActionHandler:(ClickActionHandler)clickActionHandler {
    YTAttachmentItem *imageItem = [YTAttachmentItem new];
    [self updateAttachment:imageItem withFont:self.font];
    imageItem.align = align;
    imageItem.attachment = view;
    imageItem.type = YTAttachmentTypeView;
    imageItem.size = size;
    imageItem.clickActionHandler = clickActionHandler;
    [self.attachments addObject:imageItem];
    NSAttributedString *imageAttributeString = [self attachmentAttributeStringWithAttachmentItem:imageItem size:size];
    [self.attributeString appendAttributedString:imageAttributeString];
}

- (NSAttributedString *)attachmentAttributeStringWithAttachmentItem:(YTAttachmentItem *)attachmentItem size:(CGSize)size {
    // 創建CTRunDelegateCallbacks
    CTRunDelegateCallbacks callback;
    memset(&callback, 0, sizeof(CTRunDelegateCallbacks));
    callback.getAscent = getAscent;
    callback.getDescent = getDescent;
    callback.getWidth = getWidth;
    
    // 創建CTRunDelegateRef
//    NSDictionary *metaData = @{YTRunMetaData: attachmentItem};
    CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callback, (__bridge void * _Nullable)(attachmentItem));
    
    // 設置占位使用的圖片屬性字符串
    // 參考:https://en.wikipedia.org/wiki/Specials_(Unicode_block)  U+FFFC  OBJECT REPLACEMENT CHARACTER, placeholder in the text for another unspecified object, for example in a compound document.
    unichar objectReplacementChar = 0xFFFC;
    NSMutableAttributedString *imagePlaceHolderAttributeString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithCharacters:&objectReplacementChar length:1] attributes:[self defaultTextAttributes]];
    
    // 設置RunDelegate代理
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imagePlaceHolderAttributeString, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate);
    
    // 設置附加數據,設置點擊效果
    NSDictionary *extraData = @{YTExtraDataAttributeTypeKey: attachmentItem.type == YTAttachmentTypeImage ? @(YTDataTypeImage) : @(YTDataTypeView),
                                YTExtraDataAttributeDataKey: attachmentItem,
                                };
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imagePlaceHolderAttributeString, CFRangeMake(0, 1), (CFStringRef)YTExtraDataAttributeName, (__bridge CFTypeRef)(extraData));
    
    CFRelease(runDelegate);
    return imagePlaceHolderAttributeString;
}

// MARK: - CTRunDelegateCallbacks 回調方法
static CGFloat getAscent(void *ref) {
    YTAttachmentItem *attachmentItem = (__bridge YTAttachmentItem *)ref;
    if (attachmentItem.align == YTAttachmentAlignTypeTop) {
        return attachmentItem.ascent;
    } else if (attachmentItem.align == YTAttachmentAlignTypeBottom) {
        return attachmentItem.size.height - attachmentItem.descent;
    } else if (attachmentItem.align == YTAttachmentAlignTypeCenter) {
        return attachmentItem.ascent - ((attachmentItem.descent + attachmentItem.ascent) - attachmentItem.size.height) / 2;
    }
    return attachmentItem.size.height;
}

static CGFloat getDescent(void *ref) {
    YTAttachmentItem *attachmentItem = (__bridge YTAttachmentItem *)ref;
    if (attachmentItem.align == YTAttachmentAlignTypeTop) {
        return attachmentItem.size.height - attachmentItem.ascent;
    } else if (attachmentItem.align == YTAttachmentAlignTypeBottom) {
        return attachmentItem.descent;
    } else if (attachmentItem.align == YTAttachmentAlignTypeCenter) {
        return attachmentItem.size.height - attachmentItem.ascent + ((attachmentItem.descent + attachmentItem.ascent) - attachmentItem.size.height) / 2;
    }
    return 0;
}

static CGFloat getWidth(void *ref) {
    YTAttachmentItem *attachmentItem = (__bridge YTAttachmentItem *)ref;
    return attachmentItem.size.width;
}

計算添加的View在父View中的位置,可以直接復用添加image的方法

對齊方式

image.png

bounding box(邊界框),這是一個假想的框子,它盡可能緊密的裝入字形。
baseline(基線),一條假想的線,一行上的字形都以此線作為上下位置的參考,一般在文本的3/4處,在這條線的左側存在一個點叫做基線的原點。
ascent(上行高度),從原點到字體中最高(這里的高深都是以基線為參照線的)的字形的頂部的距離,ascent是一個正值。
descent(下行高度),從原點到字體中最深的字形底部的距離,descent是一個負值(比如一個字體原點到最深的字形的底部的距離為2,那么descent就為-2)。

基于以下數據分析對齊方式

Font.fontAscent = 33.75.   
Font.fontDescent = 27.04. 
LineHeight = Font.fontAscent + Font.fontDescent = 60.8. 

頂部對齊
需要設置ascent值為文字內容的ascent,descent值為attachmen的高度減去ascent,當內容的高度為40

  • ascent= Font.fontAscent = 33.75.
  • descent = 40 - ascent = 6.25.
ascent = 33.75. 
descent = 6.25. 
height = ascent + descent = 40. 
baseline = 33.75. 
image.png

底部對齊
需要設置descent值為文字內容的descent,ascent值為attachmen的高度減去ascent,

  • descent= Font.fontDescent = 27.04.
  • ascent = 40 - descent = 12.95.
ascent = 12.95. 
descent = 27.04. 
height = ascent + descent = 40.

image.png

居中對齊
image.png

先計算ascent值,ascent值為文字內容的ascent減去頂部的那一段差值,(如下圖標準中的值為21處的高度),然后descent值為attachmen的高度減去ascent,如下圖所示(圖片上的標注是2x,并且數值因為是手動使用工具標注,會有一些細微的偏差),內容的高度為40,所以有:

  • ascent = Font.fontAscent - (LineHeight - 40)/2 = 23.35.
  • descent = 40 - ascent = 16.64.
    ascent = 23.35.
    descent = 16.64.
    height = ascent + descent = 40.

參考鏈接
https://my.oschina.net/FEEDFACF/blog/1845922

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容