天天看點

iOS 和 Mac OS X 的字元串渲染

如何将字元串繪制到螢幕上

為了簡單起見,我們先看看UIKit在字元串渲染方面為我們提供了哪些控件。之後我們将讨論一下對于字元串的渲染, iOS 和 OS X 系統中有哪些相似和不同。

UIKit 提供了很多可以在螢幕上顯示和編輯文本的類。每一個類都是為特定使用情況準備的,是以為了避免不必要的問題,為你手上的任務挑選正确的工具是非常重要的。

UILabel

UILabel是将文本繪制到螢幕上最簡單的方式。它是UIView的一個子類,用來顯示少量的隻讀文本。文本可以被展示在一行或多行,如果文本不能适應指定的空間我們還可以使用不同的方式裁剪。盡管labels使用的方式很簡單,但是這裡有幾個技巧還是值得我們提一提的。

labels預設隻顯示一行,但是你可以将numberOfLines屬性設為其他值來改變這一行為。将它設定為一個大于1的值,文本的行數将會被限制為這個指定的值,如果設定為0則是告訴label不管文本占多少行都顯示出來。

通過設定text屬性,Labels可以顯示簡單的純文字,而設定attributedText屬性則可以讓label顯示富文本。當使用純文字的時候,你可以使用label的font,textColor, textAlignment,shadowColor和shadowOffset屬性改變它的外觀,如果你希望改變整個程式所有Label的風格,你也可以使用[UILabel appearance] 這個方法來進行全局的更改。

Attributed strings提供了更加靈活的風格可供選擇,字元串的不同部分可以使用不同的風格。讓我們看看常見布局部分,下面給出attributed strings一些示例。(下文“常見布局”那一節給出了具體的關于 Attributed String 的一些例子。)

除了通過上文提到的那些屬性來調整UILabel 的顯示風格外,你還可以通過設定UILabel的這3個BOOL值的屬性adjustsFontSizeToWidth,minimumScaleFactor,adjustsLetterSpacingToFitWidth 來讓 UILabel 根據所顯示的文本的内容自動地進行調整。如果你非常在意使用者界面的美觀,那麼你就不要開啟這些屬性,因為這會使文字的顯示效果變得不那麼美觀,但是有的時候,比如在進行App的不同語言的本土化的時候,你會遇到一些很棘手的問題,除了使用這些選項外很難找到别的解決辦法。不信的話,你可以打開 iPhone,在設定中把系統語言改為德語,然後你就會發現蘋果官方出品的程式裡到處都是被壓扁變了形的醜陋不堪的文本。這種處理方法并不完美,但有時卻很有用。

如果你使用這些選項讓UIKit壓縮你的文本以适配,如果壓縮的時候想讓文本保持在同一條基線上或需要對齊到左上角,那麼你可以定義baselineAdjustment屬性。然而,這個選項隻對單行labels起作用。

當你使用上述的方法讓文本自動縮放大小以适配你的 UILabel 時,你可以使用 baselineAdjustment 這個屬性來調整縮放時文本的基準線,是保持統一基準線還是對齊到你的 Label 的左上角。注意,這個屬性僅在單行的 Lable (即 numberOfLines 屬性值為1時)中生效。

UITextField

像labels一樣,text fields可以處理純文字或帶屬性的文本。但labels隻是能顯示文本而已,text fields還可以處理使用者輸入。然而,text fields隻限于單行文本。是以,UITextField是UIControl的一個子類,它會挂鈎到(hook into)響應鍊,并且當使用者開始或結束編輯時分發(deliver)這些行為消息。如果想要得到更多的控制權,你可以實作text field的代理。

Text fields有一系列控制文本輸入行為的選項。UITextField 實作了UITextInputTraits協定,這個協定需要你指定鍵盤外觀和操作的各種細節,比如,需要顯示哪種鍵盤,傳回按鈕的響應事件是什麼。

當沒有文本輸入的時候Text fields還可以顯示一個占位符,在右手邊顯示一個标準的清除按鈕,控制任意左右兩個輔助視圖。你還可以為其設定一個背景圖檔,這樣我們就可以用一個可變大小的圖檔為text field自定義邊框風格了。

但每當你需要輸入多行文本的時候,你就需要使用到UITextField的大哥了……

UITextView

Text views是顯示或編輯大量文本的理想選擇。UITextView是UIScrollView的一個子類,是以它能允許使用者前後滾動達到處理溢出文本的目的。和text fields一樣,text views也能處理純文字和帶屬性的文本。Text views也實作了UITextInputTraits協定來控制鍵盤的行為和外觀。

但除了text view處理多行文本的能力外,它最大的賣點就是你可以使用、定制整個Text Kit堆。你可以自定義行為或為layout manager、text container或text storage替換你自定義的子類。objc.io issue #5中有提到

Text Kit方面的文章

不幸的是,UITextView在iOS7中還有些問題。目前還是1.0版本。它是基于OS X Text Kit從頭開始重新實作的。iOS7之前,它是基于Webkit并且功能很少。

Mac中又是什麼情況呢?

現在我們的讨論已經覆寫了UIKit中基本的text類,我們繼續解釋一下這些類在AppKit中結構的不同之處。

首先,AppKit中并沒有類似UILabel的控件。而顯示文本最基本的類是NSTextField。我們将text field設為不可編輯、不可選擇,這樣便等同于iOS中的UILabel了。雖然NSTextField聽起來類似于UITextField,但NSTextField并不限制于單行文本。

NSTextView,換句話說,就是等同于UITextView,它也為我們揭露了整個Cocoa Text System。但它還囊括了很多額外的功能。很大的原因是因為Mac是一個具有指針裝置(滑鼠)的電腦。最值得注意的就是包含了設定、編輯制表符的标尺。

Core Text

上面我們讨論的所有類最終都使用Core Text布局、繪制真實的符号。Core Text是一個非常強大的framework,它已經超出我們這篇文章讨論的範圍。但是如果你曾經需要通過完全自定義的方式繪制文本(e.g.貝塞爾曲線),那你需要詳細的了解一下。

Core Text在任何繪圖方面為你提供了充分的靈活性。然而,Core Text非常難于操作。它是一個複雜的Core Foundation / C API。Core Text 在排版方面給了你充分的使用權。

在Table View中顯示動态Text

可能和所有人都打過交道的字元串繪制方法就是最常見的可變高度的table view cells。你能在社交媒體應用中見到這種。table view的delegate有一個方法。tableView:heightForRowAtIndexPath:,這便是用來計算高度的。iOS7之前,很難通過一種可靠的方式使用它。

在我們的示例中,我們将會在table view中顯示一列語錄:

iOS 和 Mac OS X 的字元串渲染

首先,為了實作完全的自定義,我們建立一個UITableViewCell的子類。在這個子類中,我們需要親自為我們的label布局:

- (void)layoutSubviews

{

    [super layoutSubviews];

    self.textLabel.frame = CGRectInset(self.bounds, MyTableViewCellInset,MyTableViewCellInset);

}

MyTableViewCellInset被定義為一個常量,是以我們可以将它用在table view的delegate的高度計算中。最簡單、準确計算高度的方法是将字元串轉換成帶屬性的字元串,然後計算出帶屬性字元串的高度。我們使用table view的寬度減去兩倍的MyTableViewCellInset常量(前面和後面的空間)。為了計算真實的高度,我們使用boundingRectWithSize:options:context:.

第一個參數是限制text大小的。我們隻需要關心寬度的限制,是以我們為高度傳一個最大值常量

CGFLOAT_MAX

.第二個參數是非常重要的:如果你傳一個其他值,bounding rect無疑會出錯。如果你想要調整字型縮放and/or追蹤,你可以使用第三個參數。最終,一旦我們得到boundingRect,我們需要再次加上inset:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath

    CGFloat labelWidth = self.tableView.bounds.size.width - MyTableViewCellInset*2;

    NSAttributedString *text = [self attributedBodyTextAtIndexPath:indexPath];

    NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin |NSStringDrawingUsesFontLeading;

    CGRect boundingRect = 1;

    return (CGFloat) (ceil(boundingRect.size.height) + MyTableViewCellInset*2);

對于bounding rect的結果還有兩件敏感的事情,除非你讀了文檔,不然這兩件事你不一定會知道:傳回的size傳回一個小數,文檔中讓我們使用ceil将結果四舍五入。最終,結果可能是會比實際的大一點。

請注意,因為我們的text是純文字時,我們建立的attributedBodyTextAtIndexPath:方法也會在tableView:cellForRowAtIndexPath:中用到。這樣,我們需要確定他們保持同步。

還有,看看文檔(如下截圖),我們可以看到iOS7釋出後,很多方法都被棄用了。如果你通過查找網頁或StackOverflow,你會發現很多答案、以及測量字元大小的變通方法。因為text system受到了重大檢修(在内部實作中,所有的東西都使用TextKit進行繪制了,而不是WebKit),是以請使用新方法。

iOS 和 Mac OS X 的字元串渲染

另一個動态調整table view cell大小的選擇就是使用Auto Layout,你可以在

這篇博文中

找到更詳細的說明。然後你可以利用contained lables的intrinsicContentSize。然而,現在自動布局比手動計算要慢很多。可是對于原型開發,這很完美:它允許你快速調整constraints并且移動事物(特别當你cell中不止一個element時這顯得特别重要)。一旦你完成産品的設計疊代,然後你就可以用手動布局的方式重新編寫代碼。

使用Text Kit和NSAttributedString布局

使用Text Kit,你将會擁有令人驚訝的靈活性來建立專業級别的文本布局。随着這些靈活性帶來的是如何組合為數衆多的選項來完成複雜的布局。

我們準備給出幾個示例并強調一些常見的布局問題,同時給出解決方案。

經典的文本

首先,讓我們看一些經典的文本。我們将會使用Jacomy-Régnier的Histoire des nombres et de la numération mécanique,并設為Bodoni字型。最終截屏效果如下所示:

iOS 和 Mac OS X 的字元串渲染

這些都是由Text Kit完成的。兩段文字之間的裝飾也是text,使用的是Bodoni Ornaments字型。

我們為文體風格使用調整好的text。第一段從最左邊開始,接下來的段落都會插入空格.

這有三種不同的風格:文體風格,首行縮進的變化風格,裝飾物風格。

讓我們先設定body1stAttributes:

CGFloat const fontSize = 15;

NSMutableDictionary *body1stAttributes = [NSMutableDictionary dictionary];

body1stAttributes[NSFontAttributeName] = [UIFont fontWithName:@"BodoniSvtyTwoITCTT-Book"  size:fontSize];

NSMutableParagraphStyle *body1stParagraph = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];

body1stParagraph.alignment = NSTextAlignmentJustified;

body1stParagraph.minimumLineHeight = fontSize + 3;

body1stParagraph.maximumLineHeight = body1stParagraph.minimumLineHeight;

body1stParagraph.hyphenationFactor = 0.97;

body1stAttributes[NSParagraphStyleAttributeName] = body1stParagraph;

我們将字型設定為BodoniSvtyTwoITCTT。這是字型的PostScript名。如果想尋找字型名,我們可以使用+[UIFont familyNames]首先得到可用的字型系列集合。一個字型系列就是我們所熟知的字型。每個字型或字型系列有一個或多個字型。為了得到這些字型的名字,我們可以使用+[UIFont fontNamesForFamilyName:]。注意一下,當你處理多樣字型時,UIFontDescriptor類非常有用,e.g.當你想要知道一個給定的字型是什麼版本的斜體。

許多設定位于NSParagraphStyle。我們建立一個預設風格的可變拷貝并做些調整。在我們的例子中,我們将會為字型大小加上3pt。

接着,我們會為這些段落的屬性建立一個拷貝并修改他們來建立boddyAttributes,(注意,這是我們段落的屬性,跟上文的body1stParagraph已經不是同一個了)

NSMutableDictionary *bodyAttributes = [body1stAttributes mutableCopy];

NSMutableParagraphStyle *bodyParagraph =

[bodyAttributes[NSParagraphStyleAttributeName] mutableCopy];

bodyParagraph.firstLineHeadIndent = fontSize;

bodyAttributes[NSParagraphStyleAttributeName] = bodyParagraph;

我們簡單的建立了一個屬性字典的可變拷貝,同時為了改變段落風格我們也需要建立一個可變拷貝。将firstLineHeadIndent設為和字型大小一樣,我們便會得到想要的空格縮進。

接着,裝飾段落風格:

NSMutableDictionary *ornamentAttributes = [NSMutableDictionary dictionary];

ornamentAttributes[NSFontAttributeName] = [UIFont fontWithName:@"BodoniOrnamentsITCTT" size:36];

NSMutableParagraphStyle *ornamentParagraph = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];

ornamentParagraph.alignment = NSTextAlignmentCenter;

ornamentParagraph.paragraphSpacingBefore = fontSize;

ornamentParagraph.paragraphSpacing = fontSize;

ornamentAttributes[NSParagraphStyleAttributeName] = ornamentParagraph;

這個很容易了解。我們使用裝飾字型并将文本居中對齊。此外,在裝飾字元的前後我們都要加空白段落。

資料表格

接下來是顯示數字的table。我們想要将分數的小數點對齊顯示,i.e.英語中的”.”:

iOS 和 Mac OS X 的字元串渲染

為了達到這個目的,我們需要指定table将中心停在分隔符上。

對于上面這個示例,我們簡單地做一下:

NSCharacterSet *decimalTerminator = [NSCharacterSet

characterSetWithCharactersInString:decimalFormatter.decimalSeparator];

NSTextTab *decimalTab = [[NSTextTab alloc]

initWithTextAlignment:NSTextAlignmentCenter  location:100 options:@{NSTabColumnTerminatorsAttributeName:decimalTerminator}];

NSTextTab *percentTab = [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentRight location:200 options:nil];

NSMutableParagraphStyle *tableParagraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];

tableParagraphStyle.tabStops = @[decimalTab, percentTab];

清單

另一個常見的使用情況就像list這樣:

iOS 和 Mac OS X 的字元串渲染

縮進相對容易設定。我們需要確定序列号(1)和text或者着重号和text之間有一個制表符。然後我們像這樣調整段落的風格:

NSMutableDictionary *listAttributes = [bodyAttributes mutableCopy];

NSMutableParagraphStyle *listParagraph =

[listAttributes[NSParagraphStyleAttributeName] mutableCopy];

listParagraph.headIndent = fontSize * 3;

listParagraph.firstLineHeadIndent = fontSize;

NSTextTab *listTab = [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentNatural location:fontSize * 3  options:nil];

listParagraph.tabStops = @[listTab];

listAttributes[NSParagraphStyleAttributeName] = listParagraph;

我們将headIndent設定為真實文本的縮進,将firstLineHeadIndent設定為我們希望着重号具有的縮進。最終,和headIndent一樣,我們需要在相同的位置增加一個制表符。着重号後的制表符會確定這行文本從正确的位置開始繪制。

繼續閱讀