NSAttributedString 詳解


轉自:http://blog.csdn.net/zhangao0086/article/details/7616385

http://www.cnblogs.com/whyandinside/archive/2013/12/27/3493475.html

NSAttributedString可以讓我們使一個字符串顯示的多樣化,但是目前到iOS 5爲止,好像對它支持的不是很好,因爲顯示起來不太方便(至少沒有在OS X上方便)。

首先導入CoreText.framework,並在需要使用的文件中導入:

#import<CoreText/CoreText.h>

創建一個NSMutableAttributedString:

  1. NSMutableAttributedString *attriString = [[[NSMutableAttributedString alloc] initWithString:@"this is test!"]   
  2.                                               autorelease];  
非常常規的創建方式,接下來我們給它配置屬性:

  1. //把this的字體顏色變爲紅色  
  2. [attriString addAttribute:(NSString *)kCTForegroundColorAttributeName  
  3.                     value:(id)[UIColor redColor].CGColor   
  4.                     range:NSMakeRange(0, 4)];  
  5. //把is變爲黃色  
  6. [attriString addAttribute:(NSString *)kCTForegroundColorAttributeName  
  7.                     value:(id)[UIColor yellowColor].CGColor   
  8.                     range:NSMakeRange(5, 2)];  
  9. //改變this的字體,value必須是一個CTFontRef  
  10. [attriString addAttribute:(NSString *)kCTFontAttributeName  
  11.                     value:(id)CTFontCreateWithName((CFStringRef)[UIFont boldSystemFontOfSize:14].fontName,  
  12.                                                    14,   
  13.                                                    NULL)  
  14.                     range:NSMakeRange(0, 4)];  
  15. //給this加上下劃線,value可以在指定的枚舉中選擇  
  16. [attriString addAttribute:(NSString *)kCTUnderlineStyleAttributeName  
  17.                     value:(id)[NSNumber numberWithInt:kCTUnderlineStyleDouble]  
  18.                     range:NSMakeRange(0, 4)];  
  19. return attriString;  

這樣就算是配置好了,但是我們可以發現NSAttributedString繼承於NSObject,並且不支持任何draw的方法,那我們就只能自己draw了。寫一個UIView的子類(假設命名爲TView),在initWithFrame中把背景色設爲透明(self.backgroundColor = [UIColor clearColor]),然後在重寫drawRect方法:

  1. -(void)drawRect:(CGRect)rect{  
  2.     [super drawRect:rect];  
  3.       
  4.     NSAttributedString *attriString = getAttributedString();  
  5.       
  6.     CGContextRef ctx = UIGraphicsGetCurrentContext();  
  7.     CGContextConcatCTM(ctx, CGAffineTransformScale(CGAffineTransformMakeTranslation(0, rect.size.height), 1.f, -1.f));  
  8.       
  9.     CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attriString);  
  10.     CGMutablePathRef path = CGPathCreateMutable();  
  11.     CGPathAddRect(path, NULL, rect);  
  12.       
  13.     CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);  
  14.     CFRelease(path);  
  15.     CFRelease(framesetter);  
  16.       
  17.     CTFrameDraw(frame, ctx);  
  18.     CFRelease(frame);  
  19. }  

在代碼中我們調整了CTM(current transformation matrix),這是因爲Quartz 2D的座標系統不同,比如(10, 10)到(20, 20)的直線座標:

 

座標類似於數學中的座標,可以先不調整CTM,看它是什麼樣子的,下面兩種調整方法是完全一樣的:

  1. CGContextConcatCTM(ctx, CGAffineTransformScale(CGAffineTransformMakeTranslation(0, rect.size.height), 1.f, -1.f));  
==
  1. CGContextTranslateCTM(ctx, 0, rect.size.height);  
  2. CGContextScaleCTM(ctx, 1, -1);  

CTFramesetter是CTFrame的創建工廠,NSAttributedString需要通過CTFrame繪製到界面上,得到CTFramesetter後,創建path(繪製路徑),然後得到CTFrame,最後通過CTFrameDraw方法繪製到界面上

如果想要計算NSAttributedString所要的size,就需要用到這個API:

CTFramesetterSuggestFrameSizeWithConstraints,用NSString的sizeWithFont算多行時會算不準的,因爲在CoreText裏,行間距也是你來控制的。

設置行間距和換行模式都是設置一個屬性:kCTParagraphStyleAttributeName,這個屬性裏面又分爲很多子

屬性,其中就包括

  • kCTLineBreakByCharWrapping
  • kCTParagraphStyleSpecifierLineSpacingAdjustment
設置如下:

  1. //段落  
  2.     //line break  
  3. CTParagraphStyleSetting lineBreakMode;  
  4. CTLineBreakMode lineBreak = kCTLineBreakByCharWrapping; //換行模式  
  5. lineBreakMode.spec = kCTParagraphStyleSpecifierLineBreakMode;  
  6. lineBreakMode.value = &lineBreak;  
  7. lineBreakMode.valueSize = sizeof(CTLineBreakMode);  
  8.     //行間距  
  9. CTParagraphStyleSetting LineSpacing;  
  10. CGFloat spacing = 4.0;  //指定間距  
  11. LineSpacing.spec = kCTParagraphStyleSpecifierLineSpacingAdjustment;  
  12. LineSpacing.value = &spacing;  
  13. LineSpacing.valueSize = sizeof(CGFloat);  
  14.   
  15. CTParagraphStyleSetting settings[] = {lineBreakMode,LineSpacing};  
  16. CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, 2);   //第二個參數爲settings的長度  
  17. [attributedString addAttribute:(NSString *)kCTParagraphStyleAttributeName  
  18.                          value:(id)paragraphStyle  
  19.                          range:NSMakeRange(0, attributedString.length)];  

-----------------------------------------猥瑣的分界線-----------------------------------------

這並不是唯一的方法,還有另一種替代方案:

  1. CATextLayer *textLayer = [CATextLayer layer];  
  2. textLayer.string = getAttributedString();  
  3. textLayer.frame = CGRectMake(0, CGRectGetMaxY(view.frame), 200, 200);  
  4. [self.view.layer addSublayer:textLayer];  
CATextLayer可以直接支持NSAttributedString!

-----------------------------------------猥瑣的分界線-----------------------------------------

效果圖:


源碼地址

-----------------------------------------猥瑣的分界線-----------------------------------------
目前發現有一個問題,好像中文全都會被加粗,設置不加粗的字體也沒用,應該是CoreText的bug,已經上報給了apple了。

-----------------------------------------對於cythb兄提出的問題-----------------------------------------
首先表示感謝關注

原文中確有描述不適當的地方,比如:The upper-left corner of the context is (0, 0) 。實際上Quartz2D的座標系統確實在左下角,只是有一些技術在設置它們的graphics context時使用了不同於Quartz的默認座標系統。相對於Quartz來說,這些座標系統是修改的座標系統(modified coordinate system)。


UPDATED

在iOS6之後,創建一個AttributedString變成了一件輕鬆的事情,<CoreText/CoreText.h>已經不需要導入了。如果我要設置字體的顏色,可以直接這樣:

  1. [textAttr addAttribute:NSForegroundColorAttributeName  
  2.                        value:[UIColor redColor]  
  3.                        range:NSMakeRange(0, text.length)];  


An NSAttributedString object manages character strings and associated sets of attributes (for example, font and kerning) that apply to individual characters or ranges of characters in the string.

這句話就是對這個類的一個最簡明扼要的概括。NSAttributedString管理一個字符串,以及與該字符串中的單個字符或某些範圍的字符串相關的屬性。比如這個字符串“北京天安門”,“我”跟其他字符的顏色不一樣,而“北京”與其他的字體和大小不一樣,等等。NSAttributedString就是用來存儲這些信息的,具體實現時,NSAttributedString維護了一個NSString,用來保存最原始的字符串,另有一個NSDictionary用來保存各個子串/字符的屬性。

Create Attributed String

有3種方法創建Attributed String。

1. 使用initWithString:, initWithString:attributes:, 或者 initWithAttributedString: ,下面是一個實例代碼:

1
2
3
4
5
NSFont *font = [NSFont fontWithName:@"Palatino-Roman" size:14.0];
NSDictionary *attrsDictionary = [NSDictionary dictionaryWithObject:font
                                    forKey:NSFontAttributeName];
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:@"strigil"
            attributes:attrsDictionary];

 可以看到上面創建的整個字符串關聯了Font屬性。如果希望只是對某一範圍的字符串施加某個屬性應該使用NSMutableAttributedString的 setAttributes:range:方法。這裏例子是使用了Font屬性,在Appkit中特殊定義了若干屬性,這些屬性被用於Core Text中。其他的屬性包括前景色、背景色、是否有shadow等,具體可見本文

2. 使用initWithRTF:documentAttributes:, initWithRTFD:documentAttributes:, and initWithRTFDFileWrapper:documentAttributes:從rich text (RTF) 或者 rich text with attachments (RTFD) 數據中創建。

複製代碼
NSData *rtfData = ...;  // assume rtfData is an NSData object containing valid RTF data
NSDictionary *docAttributes;
NSSize paperSize;
 
NSAttributedString *attrString;
 
if ((attrString = [[NSAttributedString alloc]
        initWithRTF: rtfData documentAttributes: &docAttributes])) {
 
    NSValue *value = [docAttrs objectForKey:@"PaperSize"];
    paperSize = [value sizeValue];
    // implementation continues...
複製代碼

3. 使用initWithHTML:documentAttributes: 和 initWithHTML:baseURL:documentAttributes:從HTML數據中創建。有線程安全問題,使用時需要注意。

對RTF和HTML的支持都是AppKit對NSAttributedString的擴展。

Accessing Attributes

從上面對這個類的介紹可以知道,如果我們要訪問某個子串/字符的屬性,需要提供子串的位置和屬性的名字,而如果不提供屬性名字,那就把所有屬性都返回。下面就是其對應的APIs:

1
2
3
4
5
6
attributesAtIndex:effectiveRange:
attributesAtIndex:longestEffectiveRange:inRange:
attribute:atIndex:effectiveRange:
attribute:atIndex:longestEffectiveRange:inRange:
fontAttributesInRange:
rulerAttributesInRange:

 fontAttributesInRange: 和 rulerAttributesInRange: 是由AppKit擴展的屬性。

The first four methods also return by reference the effective range and the longest effective range of the attributes. These ranges allow you to determine the extent of attributes. Conceptually, each character in an attributed string has its own collection of attributes; however, it’s often useful to know when the attributes and values are the same over a series of characters. This allows a routine to progress through an attributed string in chunks larger than a single character. In retrieving the effective range, an attributed string simply looks up information in its attribute mapping, essentially the dictionary of attributes that apply at the index requested. In retrieving the longest effective range, the attributed string continues checking characters past this basic range as long as the attribute values are the same. This extra comparison increases the execution time for these methods but guarantees a precise maximal range for the attributes requested.

Methods that return an effective range by reference are not guaranteed to return the maximal range to which the attribute(s) apply; they are merely guaranteed to return some range over which they apply. In practice they will return whatever range is readily available from the attributed string's internal storage mechanisms, which may depend on the implementation and on the precise history of modifications to the attributed string.

那些用reference返回有效範圍的方法並不保證一定返回attribute應用的最大範圍。它們只保證返回那些attribute有效的一些範圍。實際上,它們只是返回attributed string中容易返回的那些信息,是否容易與內部實現,已經對attributed string的精確修改歷史有關。

Methods that return a longest effective range by reference, on the other hand, are guaranteed to return the longest range containing the specified index to which the attribute(s) in question apply (constrained by the value of the argument passed in forinRange:). For efficiency, it is important that the inRange: argument should be as small as appropriate for the range of interest to the client.

那些返回最長有效範圍的方法時能夠保證返回制定attribute有效的最長的range的。爲了效率,inRange: 參數應該儘量小,能滿足客戶需要就好。

When you iterate over an attributed string by attribute ranges, either sort of method may be appropriate depending on the situation. If there is some processing to be done for each range, and you know that the full range for a given attribute is going to have to be handled eventually, it may be more efficient to use the longest-effective-range variant, so as not to have to handle the range in pieces. However, you should use the longest-effective-range methods with caution, because the longest effective range could be quite long—potentially the entire length of the document, if the inRange: argument is not constrained.

 

Changing an Attributed String

NSMutableAttributedString提供若干方法,即可以修改字符串,又可以修改字符串的屬性。經過多次修改後,有些信息可能變的不一致了,爲了讓信息保持一致,可以使用下面的方法:

1
2
3
4
5
6
fixAttributesInRange:
fixAttachmentAttributeInRange:
fixFontAttributeInRange:
fixParagraphStyleAttributeInRange:
beginEditing
endEditing

 

這些方法都是AppKit的擴展功能。

 

Reference:

1. https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/AttributedStrings/AttributedStrings.html#//apple_ref/doc/uid/10000036-BBCCGDBG 

2. https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSAttributedString_AppKitAdditions/Reference/Reference.html#//apple_ref/doc/uid/20000167-BAJJCCFC



發佈了20 篇原創文章 · 獲贊 0 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章