Study CoreTextJAN 31ST, 2013 | COMMENTS前言好久沒寫日志,之前說過至少一天一篇,至了第二天就沒開始做了。太忙了,有些疲于奔命,因為涼茶項目和阿拉丁項目的原因,自己有點力不從心了。引言學習CoreText,最初的想法是寫一個雜志類的應用,因為對網易和zarca應用一些技術的疑問,所以,自己有了很強的興趣欲和鉆研欲,開始這段有點不順的學習過程。難題1、對CGContextRef的CTM不理解,觀念導致很多東西沒有正確的理解。2、對NS的了解不多,一些文字繪制方面的座標系問題讓自己很迷惑。3、對CoreText麻煩的API嚴重不適應。關于CTMCTM,Context Translate Matrix。 它是把要繪制的上下文以一個叫做Matrix的東西來表示,可以簡單地想作,繪制的上下文的每一個點都映射在Matrix上,你在Matrix上的操作都會使得上下文上的點產生相應的變動。如放大、旋轉、移動。在一般的教程里面,為了達到旋轉或放大縮小的目的,一般都會先改變這個上下文,如:12345CGContextTranslateCTM(context, 0, self.bounds.size.height);CGContextScaleCTM(context, 1.0f, -1.0f);// some draw code// ....然后進行繪圖操作。那么這個繪圖操作是怎么做的呢?這個對Matrix的操作,為什么是放在前面而不是放在后面,為什么放在后面又沒有效果呢?不是說改變Matrix就會改變上面的映射的所有點呢?這些常規的邏輯思維使得問題越發無法理解和解決。那么我們先從context來了解。一般情況也,我們總是認為context就是畫布,所有的matrix旋轉都是針對畫布的旋轉,雖然這樣的理解是錯誤的,但是得到的結果卻是正確的,但是如果在一些稍復雜的坐標系轉換時,或者更改matrix時在之前或之后的理解時,這樣理解就會得到難得理解的結果。其實context說的是繪畫人所處的角度上下文。如下圖,默認的情況下,繪畫人的角度是正對著畫布的:畫布是白色的,而我則是在左上角用一個黃色的三角形來標識它的左上角,使用left top來標識context的左上角,而繪畫人是黃色的圓形。要記著??!畫布無論怎么樣都是正對著屏幕的,它不會旋轉,或者放大縮小,或者移動。那么為什么又看起來我放大了或者移動了呢?其實移動的是你的context,也就是你所處的context視角,我舉個例子,比方說我要旋轉180度在左上角寫一個“abcdefg”。首先,我要先旋轉180度:然后,我在左上角寫上“abcdefg”:然后重置context:可以看到,我們改變context只是改變了自己面對畫布的角度,而畫布仍然是正對著屏幕的,自己始終以context的左上角為自己角度的左上角,而不是以畫布的左上角為左上角,也就是說,這時繪畫時的座標(0,0)是你旋轉后context的left top,而不是畫布 的左上角,記著這一點很重要。所以,在繪畫的時候,其實是倒著畫在了畫布的右下角上。而重置context,則是把自己正對著畫布而已。這也就說清了為什么是在使用matrix更改context之后進行繪圖有效(把自己面對畫布的角度先調整了),而不是在畫了之后再調整(因為你都畫完了,再調整自己的角度還有什么用?)。正確理解使用matrix更改context的方式很重要,因為這涉及到坐標系的問題,之后的CoreText相當討論會講到一個例子。關于NS座標系NS坐標系是以左下角為(0,0),與iOS的坐標系在Y上是相反的,所以,在iOS進行CoreText進行繪圖或文字的時候,X方向是一致的,但是Y則是倒過來的。如下圖:那么怎么辦呢?想想,仔細看上面這張圖,貌似像是正常方向的倒影,但是水平線卻在最上面。嗯,挪下來,然后再反過來,看一下效果。如下圖:效果:效果果然如圖所示,好?。】墒鞘遣皇蔷瓦@樣完了呢?不是,還有一個更為重要的問題,這個時候,進行了兩次的轉換matrix,context的left top在哪里呢?根據之前的理論,那得讓自己先把自己向下移,然后把頭倒過來,OK,這下明白了,這下畫布的左下角變成了context的左上角,別的都沒變。這時,當你在(20, 20)畫一個長方形,其實就是畫布的(20, canvas.height - 20 + rect.size.height)的位置上畫了個長方形,而且是倒過來的。仔細想想這個,有趣的事情還有很多,因為按照自己看過本文之前的理論,可能會非常驚訝為什么得到的結果和自己想的不一樣,一直以為是在(20, 20)處畫一個長方形,結果卻剛好相反,這就是沒有理解context及matrix的正確含義所致。關于CoreText的API這種CoreFoundation式的API讓我覺得很不適應,和一開始接觸Objective-C一樣,我在罵娘。不過一定要注意一點,就是使用Create函數建立的對象引用,必須要使用CFRelease掉。CoreText是什么?這是一個低級的API,它的數據源是NSAttributedString。它可以根據NSAttributedString的定義的每個range的subNSAttributedString的樣式進行對字符串的渲染。可以這樣說,這是一個富文本渲染器。為什么要用CoreText不是有了UIWebView了嗎?為什么還要用CoreText呢?首先,UIWebView是一個很重量級的東西,占用大量的內存,加載緩慢。其次,UIWebView做不了一些東西,比如說分列顯示,這點肯定webpage做不到(我做了這么久web,直到現在也想不到辦法)。最后,UIWebView做出來的東西總是讓人覺得不夠cool,不知怎的,你總會發現用CoreText做的東西相當的有雜志感。那么為什么要用CoreText呢?其實很重要的一點是那個無用的UILabel控件,一方面是不能控制行高,另一個功能有限,特別難以控制多行文本,讓人蛋疼的地方是,多行文本時,它竟然只能垂直居中。另,使用CoreText可以很好地做一些個性化的東西,比如可以使用動畫,這一點UIWebView做不到。它能做一些很cool的東西,比方說,雜志,新聞類的應用。這是我為什么使用CoreText的原因。CoreText的概念。CTFramesetterRef按名字就知道,這是一個setter,一個屬性設置器,它引用了一些有用的對像,諸如字體之類的,它的工作就是,生成一個CTFrameRef對象。CTFrameRef這是一個Frame對象,用于表示一個繪制區域,它是由CTFramesetterRef生成。CTLineRef表示要繪制的一行,這個概念可以不用理解,一般情況下我們不會使用到它,但是如果想做更為徹底的定制工作的話,那么就要去看apple的文檔了。CTFrameRef對象包含了多個line對象。CTNodeRef表示要繪制的某個節點(subNSAttributedString),每line對象包含多個node對象,這個節點表示著不同格式的NSAttributedString對象的如何繪制。這個也可以不用理解,一般情況下我們也不會使用到它,不過要是用于在中間插入圖片的話,這個就要考慮了。CoreText是不支持中間插入圖片的,不過我們可以在讀到特殊標記的node的時候,返回不同的行高和行寬,預留空間,在繪制完coretext之后,在這些個空間處繪制相應的圖片。CoreText怎么使用呢?上面寫了一堆概念性的東西,一下子還真是看不懂。那么怎么用呢?其實流程是這樣的: 1、生成要繪制的NSAttributedString對象。 2、生成一個CTFramesetterRef對象,然后創建一個CGPath對象,這個Path對象用于表示可繪制區域坐標值、長寬。 3、使用上面生成的setter和path生成一個CTFrameRef對象,這個對象包含了這兩個對象的信息(字體信息、坐標信息),它就可以使用CTFrameDraw方法繪制了。這里有一個demo代碼:1234567891011121314151617181920212223- (void)_drawRectWithContext:(CGContextRef)context{? // generate NSAttributedString object? ? NSAttributedString *contentAttrString = [self _generateAttributeString];? // path? ? CGRect bounds = CGRectInset(self.bounds, 10.0f, 10.0f);? ? CGMutablePathRef path = CGPathCreateMutable();? ? CGPathAddRect(path, NULL, bounds);? ? // ------------------------ begin draw? ? // draw coretext? ? CTFramesetterRef framesetter? ? ? ? = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)(contentAttrString));? ? CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);? ? CTFrameDraw(frame, context);? ? CFRelease(frame);? ? CFRelease(path);? ? CFRelease(framesetter);? ? // ------------------------ end draw}NSAttributedString呢?一直都在說CoreText的事情,那么怎么使用NSAttributedString呢?怎么設置NSAttributedString的屬性,怎么設計行高,怎么設置字體大小,樣式?首先得會設置屬性,調用NSAttributedString的setAttributes:range:方法就可以設置屬性:123// set attributes to attributed string[attrString setAttributes:attributes? ? ? ? ? ? ? ? ? ? range:NSMakeRange(0, attrString.length)];那么都有哪些屬性呢?有下面這些屬性:123456789101112131415const CFStringRef kCTCharacterShapeAttributeName;const CFStringRef kCTFontAttributeName;const CFStringRef kCTKernAttributeName;const CFStringRef kCTLigatureAttributeName;const CFStringRef kCTForegroundColorAttributeName;const CFStringRef kCTForegroundColorFromContextAttributeName;const CFStringRef kCTParagraphStyleAttributeName;const CFStringRef kCTStrokeWidthAttributeName;const CFStringRef kCTStrokeColorAttributeName;const CFStringRef kCTSuperscriptAttributeName;const CFStringRef kCTUnderlineColorAttributeName;const CFStringRef kCTUnderlineStyleAttributeName;const CFStringRef kCTVerticalFormsAttributeName;const CFStringRef kCTGlyphInfoAttributeName;const CFStringRef kCTRunDelegateAttributeName下面是說明:kCTCharacterShapeAttributeNameControls glyph selection. Value must be a CFNumberRef object. Default is value is 0 (disabled). A non-zero value is interpreted as Apple Type Services kCharacterShapeType selector + 1 (seefor selectors). For example, an attribute value of 1 corresponds to kTraditionalCharactersSelector.
kCTFontAttributeName
The font of the text to which this attribute applies. The value associated with this attribute must be a CTFont object. Default is Helvetica 12.
kCTKernAttributeName
The amount to kern the next character. The value associated with this attribute must be a CFNumber float. Default is standard kerning. The kerning attribute indicates how many points the following character should be shifted from its default offset as defined by the current character’s font in points: a positive kern indicates a shift farther away from and a negative kern indicates a shift closer to the current character. If this attribute is not present, standard kerning is used. If this attribute is set to 0.0, no kerning is done at all.
kCTLigatureAttributeName
The type of ligatures to use. The value associated with this attribute must be a CFNumber object. Default is an integer value of 1. The ligature attribute determines what kinds of ligatures should be used when displaying the string. A value of 0 indicates that only ligatures essential for proper rendering of text should be used. A value of 1 indicates that standard ligatures should be used, and 2 indicates that all available ligatures should be used. Which ligatures are standard depends on the script and possibly the font. Arabic text, for example, requires ligatures for many character sequences but has a rich set of additional ligatures that combine characters. English text has no essential ligatures, and typically has only two standard ligatures, those for “fi” and “fl”—all others are considered more advanced or fancy.
kCTForegroundColorAttributeName
The foreground color of the text to which this attribute applies. The value associated with this attribute must be a CGColor object. Default value is black.
kCTForegroundColorFromContextAttributeName
Sets a foreground color using the context’s fill color. Value must be a CFBooleanRef object. Default is false. The reason this exists is because an NSAttributedString object defaults to a black color if no color attribute is set. This forces Core Text to set the color in the context. This attribute allows developers to sidestep this, making Core Text set nothing but font information in the CGContext. If set, this attribute also determines the color used by kCTUnderlineStyleAttributeName, in which case it overrides the foreground color.
kCTParagraphStyleAttributeName
The paragraph style of the text to which this attribute applies. A paragraph style object is used to specify things like line alignment, tab rulers, writing direction, and so on. Value must be a CTParagraphStyle object. Default is an empty CTParagraphStyle object. See CTParagraphStyle Reference for more information.
kCTStrokeWidthAttributeName
The stroke width. Value must be a CFNumberRef object. Default value is 0.0, or no stroke. This attribute, interpreted as a percentage of font point size, controls the text drawing mode: positive values effect drawing with stroke only; negative values are for stroke and fill. A typical value for outlined text is 3.0.
kCTStrokeColorAttributeName
The stroke color. Value must be a CGColorRef object. Default is the foreground color.
kCTSuperscriptAttributeName
Controls vertical text positioning. Value must be a CFNumberRef object. Default is integer value 0. If supported by the specified font, a value of 1 enables superscripting and a value of -1 enables subscripting.
kCTUnderlineColorAttributeName
The underline color. Value must be a CGColorRef object. Default is the foreground color.
kCTUnderlineStyleAttributeName
The style of underlining, to be applied at render time, for the text to which this attribute applies. Value must be a CFNumber object. Default is kCTUnderlineStyleNone. Set a value of something other than kCTUnderlineStyleNone to draw an underline. In addition, the constants listed in “CTUnderlineStyleModifiers” can be used to modify the look of the underline. The underline color is determined by the text’s foreground color.
kCTVerticalFormsAttributeName
The orientation of the glyphs in the text to which this attribute applies. Value must be a CFBoolean object. Default is False. A value of False indicates that horizontal glyph forms are to be used; True indicates that vertical glyph forms are to be used.
kCTGlyphInfoAttributeName
The glyph info object to apply to the text associated with this attribute. Value must be a CTGlyphInfo object. The glyph specified by this CTGlyphInfo object is assigned to the entire attribute range, provided that its contents match the specified base string and that the specified glyph is available in the font specified by kCTFontAttributeName. See CTGlyphInfo Reference for more information.
kCTRunDelegateAttributeName
The run-delegate object to apply to an attribute range of the string. The value must be a CTRunDelegate object. The run delegate controls such typographic traits as glyph ascent, descent, and width. The values returned by the embedded run delegate apply to each glyph resulting from the text in that range. Because an embedded object is only a display-time modification, you should avoid applying this attribute to a range of text with complex behavior, such as text having a change of writing direction or having combining marks. It is thus recommended you apply this attribute to a range containing the single character U+FFFC. See CTRunDelegate Reference for more information.
上面所述的東西貌似只是說明了設置字體與樣式,卻沒有行高、縮進之類的東西哦??!
嗯,不是沒有,而是CoreText把它當成是段落樣式來設置了,也就是說,要設置kCTParagraphStyleAttributeName的屬性就行。kCTParagraphStyleAttributeName屬性的值是一個CTParagraphStyle對象,你需要把你想要設置的段落屬性放進這個對象就可以設置行高之類的東西:
kCTParagraphStyleSpecifierAlignment = 0,
kCTParagraphStyleSpecifierFirstLineHeadIndent = 1,
kCTParagraphStyleSpecifierHeadIndent = 2,
kCTParagraphStyleSpecifierTailIndent = 3,
kCTParagraphStyleSpecifierTabStops = 4,
kCTParagraphStyleSpecifierDefaultTabInterval = 5,
kCTParagraphStyleSpecifierLineBreakMode = 6,
kCTParagraphStyleSpecifierLineHeightMultiple = 7,
kCTParagraphStyleSpecifierMaximumLineHeight = 8,
kCTParagraphStyleSpecifierMinimumLineHeight = 9,
kCTParagraphStyleSpecifierLineSpacing = 10,? ? ? ? ? /* deprecated */
kCTParagraphStyleSpecifierParagraphSpacing = 11,
kCTParagraphStyleSpecifierParagraphSpacingBefore = 12,
kCTParagraphStyleSpecifierBaseWritingDirection = 13,
kCTParagraphStyleSpecifierMaximumLineSpacing = 14,
kCTParagraphStyleSpecifierMinimumLineSpacing = 15,
kCTParagraphStyleSpecifierLineSpacingAdjustment = 16,
kCTParagraphStyleSpecifierLineBoundsOptions = 17,
kCTParagraphStyleSpecifierCount
下面是說明:
kCTParagraphStyleSpecifierAlignment
The text alignment. Natural text alignment is realized as left or right alignment, depending on the line sweep direction of the first script contained in the paragraph. Type: CTTextAlignment. Default: kCTNaturalTextAlignment. Application: CTFramesetter.
kCTParagraphStyleSpecifierFirstLineHeadIndent
The distance, in points, from the leading margin of a frame to the beginning of the paragraph’s first line. This value is always nonnegative. Type: CGFloat. Default: 0.0. Application: CTFramesetter.
kCTParagraphStyleSpecifierHeadIndent
The distance, in points, from the leading margin of a text container to the beginning of lines other than the first. This value is always nonnegative. Type: CGFloat Default: 0.0 Application: CTFramesetter
kCTParagraphStyleSpecifierTailIndent
The distance, in points, from the margin of a frame to the end of lines. If positive, this value is the distance from the leading margin (for example, the left margin in left-to-right text). If 0 or negative, it’s the distance from the trailing margin. Type: CGFloat. Default: 0.0. Application: CTFramesetter.
kCTParagraphStyleSpecifierTabStops
The CTTextTab objects, sorted by location, that define the tab stops for the paragraph style. Type: CFArray of CTTextTabRef. Default: 12 left-aligned tabs, spaced by 28.0 points. Application: CTFramesetter, CTTypesetter.
kCTParagraphStyleSpecifierDefaultTabInterval
The documentwide default tab interval. Tabs after the last specified by kCTParagraphStyleSpecifierTabStops are placed at integer multiples of this distance (if positive). Type: CGFloat. Default: 0.0. Application: CTFramesetter, CTTypesetter.
kCTParagraphStyleSpecifierLineBreakMode
The mode that should be used to break lines when laying out the paragraph’s text. Type: CTLineBreakMode. Default: kCTLineBreakByWordWrapping. Application: CTFramesetter
kCTParagraphStyleSpecifierLineHeightMultiple
The line height multiple. The natural line height of the receiver is multiplied by this factor (if positive) before being constrained by minimum and maximum line height. Type: CGFloat. Default: 0.0. Application: CTFramesetter.
kCTParagraphStyleSpecifierMaximumLineHeight
The maximum height that any line in the frame will occupy, regardless of the font size or size of any attached graphic. Glyphs and graphics exceeding this height will overlap neighboring lines. A maximum height of 0 implies no line height limit. This value is always nonnegative. Type: CGFloat. Default: 0.0. Application: CTFramesetter.
kCTParagraphStyleSpecifierMinimumLineHeight
The minimum height that any line in the frame will occupy, regardless of the font size or size of any attached graphic. This value is always nonnegative. Type: CGFloat. Default: 0.0. Application: CTFramesetter.
kCTParagraphStyleSpecifierLineSpacing
The space in points added between lines within the paragraph (commonly known as leading). This value is always nonnegative. Type: CGFloat. Default: 0.0. Application: CTFramesetter.
kCTParagraphStyleSpecifierParagraphSpacing
The space added at the end of the paragraph to separate it from the following paragraph. This value is always nonnegative and is determined by adding the previous paragraph’s kCTParagraphStyleSpecifierParagraphSpacing setting and the current paragraph’s kCTParagraphStyleSpecifierParagraphSpacingBefore setting. Type: CGFloat. Default: 0.0. Application: CTFramesetter.
kCTParagraphStyleSpecifierParagraphSpacingBefore
The distance between the paragraph’s top and the beginning of its text content. Type: CGFloat. Default: 0.0. Application: CTFramesetter.
kCTParagraphStyleSpecifierBaseWritingDirection
The base writing direction of the lines. Type: CTWritingDirection. Default: kCTWritingDirectionNatural. Application: CTFramesetter, CTTypesetter.
kCTParagraphStyleSpecifierCount
The number of style specifiers. The purpose is to simplify validation of style specifiers
那么怎么編碼呢?也就是這些屬性怎么用呢?下面是一個demo:
NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:_content];
// line space
CTParagraphStyleSetting lineSpaceSetting;
lineSpaceSetting.spec = kCTParagraphStyleSpecifierLineSpacing;
lineSpaceSetting.value = &_lineSpace;
lineSpaceSetting.valueSize = sizeof(float);
// indent
CTParagraphStyleSetting indentSetting;
indentSetting.spec = kCTParagraphStyleSpecifierFirstLineHeadIndent;
indentSetting.value = &_indent;
indentSetting.valueSize = sizeof(float);
// composite settings
CTParagraphStyleSetting settings[] = {
lineSpaceSetting,
indentSetting
};
CTParagraphStyleRef style = CTParagraphStyleCreate(settings, 2);
// build attributes
NSDictionary *attributes = @{(__bridge id)kCTParagraphStyleAttributeName: (__bridge id)style};
// set attributes to attributed string
[attrString setAttributes:attributes
range:NSMakeRange(0, attrString.length)];
分欄顯示
這個東西是相當有趣的東東,可以使用CTFrameGetVisibleStringRange函數來計算指定frame繪制了多少字符,那么就可以另建一個frame把剩余的字符繪制進去:
// get current context and store it's state
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
// translate CTM for iOS
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1, -1);
// generate attributed string
NSAttributedString *attrString = [self _generateAttributedString];
// Draw code start -------------------------------------------------------------------------------------------------
CGRect bounds = CGRectInset(self.bounds, 25.0f, 25.0f);
float columnWidth = (bounds.size.width - 30.0f) / 2.0f;
float columnHeight = bounds.size.height;
CTFramesetterRef framesetter
= CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)(attrString));
int location = 0;
// ------------- first column
CGRect firstColumnRect = CGRectMake(bounds.origin.x, bounds.origin.y, columnWidth, columnHeight);
CGMutablePathRef firstColumnPath = CGPathCreateMutable();
CGPathAddRect(firstColumnPath, NULL, firstColumnRect);
CTFrameRef firstColumnFrame =
CTFramesetterCreateFrame(framesetter, CFRangeMake(location, 0), firstColumnPath, NULL);
CFRange firstColumnStringRange = CTFrameGetVisibleStringRange(firstColumnFrame);
CTFrameDraw(firstColumnFrame, context);
// recalculate the location for next frame.
location = firstColumnStringRange.length;
// ------------- second column
CGRect secondColumnRect =
CGRectMake(bounds.origin.x + 30 + columnWidth, bounds.origin.y, columnWidth, columnHeight);
CGMutablePathRef secondColumnPath = CGPathCreateMutable();
CGPathAddRect(secondColumnPath, NULL, secondColumnRect);
CTFrameRef secondColumnFrame =
CTFramesetterCreateFrame(framesetter,
CFRangeMake(location, 0),
secondColumnPath, NULL);
CTFrameDraw(secondColumnFrame, context);
// release
CFRelease(firstColumnPath);
CFRelease(firstColumnFrame);
CFRelease(secondColumnPath);
CFRelease(secondColumnFrame);
CFRelease(framesetter);
// Draw code end? -------------------------------------------------------------------------------------------------
// restore current context
CGContextRestoreGState(context);
總結
總的來說,CoreText是相當的麻煩的,所以,必須要對相關的代碼進行一定的抽象。
這篇日志1月份就寫了,直到現在才補完。一直害怕把這些內容忘記,于是就翻了下之前寫好的測試代碼,復習了一下,然后寫完它。
好了,今天收工。
2013-3-1 智品公司
Posted by Kut.Zhang Jan 31st, 2013? iOS