# CATextLayer
用戶界面是無法從一個單獨的圖片里面構建的。一個設計良好的圖標能夠很好地表現一個按鈕或控件的意圖,不過你遲早都要需要一個不錯的老式風格的文本標簽。
如果你想在一個圖層里面顯示文字,完全可以借助圖層代理直接將字符串使用Core Graphics寫入圖層的內容(這就是UILabel的精髓)。如果越過寄宿于圖層的視圖,直接在圖層上操作,那其實相當繁瑣。你要為每一個顯示文字的圖層創建一個能像圖層代理一樣工作的類,還要邏輯上判斷哪個圖層需要顯示哪個字符串,更別提還要記錄不同的字體,顏色等一系列亂七八糟的東西。
萬幸的是這些都是不必要的,Core Animation提供了一個`CALayer`的子類`CATextLayer`,它以圖層的形式包含了`UILabel`幾乎所有的繪制特性,并且額外提供了一些新的特性。
同樣,`CATextLayer`也要比`UILabel`渲染得快得多。很少有人知道在iOS 6及之前的版本,`UILabel`其實是通過WebKit來實現繪制的,這樣就造成了當有很多文字的時候就會有極大的性能壓力。而`CATextLayer`使用了Core text,并且渲染得非常快。
讓我們來嘗試用`CATextLayer`來顯示一些文字。清單6.2的代碼實現了這一功能,結果如圖6.2所示。
清單6.2 用`CATextLayer`來實現一個`UILabel`
~~~
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *labelView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create a text layer
CATextLayer *textLayer = [CATextLayer layer];
textLayer.frame = self.labelView.bounds;
[self.labelView.layer addSublayer:textLayer];
//set text attributes
textLayer.foregroundColor = [UIColor blackColor].CGColor;
textLayer.alignmentMode = kCAAlignmentJustified;
textLayer.wrapped = YES;
//choose a font
UIFont *font = [UIFont systemFontOfSize:15];
//set layer font
CFStringRef fontName = (__bridge CFStringRef)font.fontName;
CGFontRef fontRef = CGFontCreateWithFontName(fontName);
textLayer.font = fontRef;
textLayer.fontSize = font.pointSize;
CGFontRelease(fontRef);
//choose some text
NSString *text = @"Lorem ipsum dolor sit amet, consectetur adipiscing \ elit. Quisque massa arcu, eleifend vel varius in, facilisis pulvinar \ leo. Nunc quis nunc at mauris pharetra condimentum ut ac neque. Nunc elementum, libero ut porttitor dictum, diam odio congue lacus, vel \ fringilla sapien diam at purus. Etiam suscipit pretium nunc sit amet \ lobortis";
//set layer text
textLayer.string = text;
}
@end
~~~

圖6.2 用`CATextLayer`來顯示一個純文本標簽
如果你仔細看這個文本,你會發現一個奇怪的地方:這些文本有一些像素化了。這是因為并沒有以Retina的方式渲染,第二章提到了這個`contentScale`屬性,用來決定圖層內容應該以怎樣的分辨率來渲染。`contentsScale`并不關心屏幕的拉伸因素而總是默認為1.0。如果我們想以Retina的質量來顯示文字,我們就得手動地設置`CATextLayer`的`contentsScale`屬性,如下:
~~~
textLayer.contentsScale = [UIScreen mainScreen].scale;
~~~
這樣就解決了這個問題(如圖6.3)

圖6.3 設置`contentsScale`來匹配屏幕
`CATextLayer`的`font`屬性不是一個`UIFont`類型,而是一個`CFTypeRef`類型。這樣可以根據你的具體需要來決定字體屬性應該是用`CGFontRef`類型還是`CTFontRef`類型(Core Text字體)。同時字體大小也是用`fontSize`屬性單獨設置的,因為`CTFontRef`和`CGFontRef`并不像UIFont一樣包含點大小。這個例子會告訴你如何將`UIFont`轉換成`CGFontRef`。
另外,`CATextLayer`的`string`屬性并不是你想象的`NSString`類型,而是`id`類型。這樣你既可以用`NSString`也可以用`NSAttributedString`來指定文本了(注意,`NSAttributedString`并不是`NSString`的子類)。屬性化字符串是iOS用來渲染字體風格的機制,它以特定的方式來決定指定范圍內的字符串的原始信息,比如字體,顏色,字重,斜體等。
## 富文本
iOS 6中,Apple給`UILabel`和其他UIKit文本視圖添加了直接的屬性化字符串的支持,應該說這是一個很方便的特性。不過事實上從iOS3.2開始`CATextLayer`就已經支持屬性化字符串了。這樣的話,如果你想要支持更低版本的iOS系統,`CATextLayer`無疑是你向界面中增加富文本的好辦法,而且也不用去跟復雜的Core Text打交道,也省了用`UIWebView`的麻煩。
讓我們編輯一下示例使用到`NSAttributedString`(見清單6.3).iOS 6及以上我們可以用新的`NSTextAttributeName`實例來設置我們的字符串屬性,但是練習的目的是為了演示在iOS 5及以下,所以我們用了Core Text,也就是說你需要把Core Text framework添加到你的項目中。否則,編譯器是無法識別屬性常量的。
圖6.4是代碼運行結果(注意那個紅色的下劃線文本)
清單6.3 用NSAttributedString實現一個富文本標簽。
~~~
#import "DrawingView.h"
#import
#import
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *labelView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create a text layer
CATextLayer *textLayer = [CATextLayer layer];
textLayer.frame = self.labelView.bounds;
textLayer.contentsScale = [UIScreen mainScreen].scale;
[self.labelView.layer addSublayer:textLayer];
//set text attributes
textLayer.alignmentMode = kCAAlignmentJustified;
textLayer.wrapped = YES;
//choose a font
UIFont *font = [UIFont systemFontOfSize:15];
//choose some text
NSString *text = @"Lorem ipsum dolor sit amet, consectetur adipiscing \ elit. Quisque massa arcu, eleifend vel varius in, facilisis pulvinar \ leo. Nunc quis nunc at mauris pharetra condimentum ut ac neque. Nunc \ elementum, libero ut porttitor dictum, diam odio congue lacus, vel \ fringilla sapien diam at purus. Etiam suscipit pretium nunc sit amet \ lobortis";
?
//create attributed string
NSMutableAttributedString *string = nil;
string = [[NSMutableAttributedString alloc] initWithString:text];
//convert UIFont to a CTFont
CFStringRef fontName = (__bridge CFStringRef)font.fontName;
CGFloat fontSize = font.pointSize;
CTFontRef fontRef = CTFontCreateWithName(fontName, fontSize, NULL);
//set text attributes
NSDictionary *attribs = @{
(__bridge id)kCTForegroundColorAttributeName:(__bridge id)[UIColor blackColor].CGColor,
(__bridge id)kCTFontAttributeName: (__bridge id)fontRef
};
[string setAttributes:attribs range:NSMakeRange(0, [text length])];
attribs = @{
(__bridge id)kCTForegroundColorAttributeName: (__bridge id)[UIColor redColor].CGColor,
(__bridge id)kCTUnderlineStyleAttributeName: @(kCTUnderlineStyleSingle),
(__bridge id)kCTFontAttributeName: (__bridge id)fontRef
};
[string setAttributes:attribs range:NSMakeRange(6, 5)];
//release the CTFont we created earlier
CFRelease(fontRef);
//set layer text
textLayer.string = string;
}
@end
~~~

圖6.4 用CATextLayer實現一個富文本標簽。
## 行距和字距
有必要提一下的是,由于繪制的實現機制不同(Core Text和WebKit),用`CATextLayer`渲染和用`UILabel`渲染出的文本行距和字距也不是不盡相同的。
二者的差異程度(由使用的字體和字符決定)總的來說挺小,但是如果你想正確的顯示普通便簽和`CATextLayer`就一定要記住這一點。
##UILabel的替代品
我們已經證實了`CATextLayer`比`UILabel`有著更好的性能表現,同時還有額外的布局選項并且在iOS 5上支持富文本。但是與一般的標簽比較而言會更加繁瑣一些。如果我們真的在需求一個`UILabel`的可用替代品,最好是能夠在Interface Builder上創建我們的標簽,而且盡可能地像一般的視圖一樣正常工作。
我們應該繼承`UILabel`,然后添加一個子圖層`CATextLayer`并重寫顯示文本的方法。但是仍然會有由`UILabel`的`-drawRect:`方法創建的空寄宿圖。而且由于`CALayer`不支持自動縮放和自動布局,子視圖并不是主動跟蹤視圖邊界的大小,所以每次視圖大小被更改,我們不得不手動更新子圖層的邊界。
我們真正想要的是一個用`CATextLayer`作為宿主圖層的`UILabel`子類,這樣就可以隨著視圖自動調整大小而且也沒有冗余的寄宿圖啦。
就像我們在第一章『圖層樹』討論的一樣,每一個`UIView`都是寄宿在一個`CALayer`的示例上。這個圖層是由視圖自動創建和管理的,那我們可以用別的圖層類型替代它么?一旦被創建,我們就無法代替這個圖層了。但是如果我們繼承了`UIView`,那我們就可以重寫`+layerClass`方法使得在創建的時候能返回一個不同的圖層子類。`UIView`會在初始化的時候調用`+layerClass`方法,然后用它的返回類型來創建宿主圖層。
清單6.4 演示了一個`UILabel`子類`LayerLabel`用`CATextLayer`繪制它的問題,而不是調用一般的`UILabel`使用的較慢的`-drawRect:`方法。`LayerLabel`示例既可以用代碼實現,也可以在Interface Builder實現,只要把普通的標簽拖入視圖之中,然后設置它的類是LayerLabel就可以了。
清單6.4 使用`CATextLayer`的`UILabel`子類:`LayerLabel`
~~~
#import "LayerLabel.h"
#import
@implementation LayerLabel
+ (Class)layerClass
{
//this makes our label create a CATextLayer //instead of a regular CALayer for its backing layer
return [CATextLayer class];
}
- (CATextLayer *)textLayer
{
return (CATextLayer *)self.layer;
}
- (void)setUp
{
//set defaults from UILabel settings
self.text = self.text;
self.textColor = self.textColor;
self.font = self.font;
//we should really derive these from the UILabel settings too
//but that's complicated, so for now we'll just hard-code them
[self textLayer].alignmentMode = kCAAlignmentJustified;
?
[self textLayer].wrapped = YES;
[self.layer display];
}
- (id)initWithFrame:(CGRect)frame
{
//called when creating label programmatically
if (self = [super initWithFrame:frame]) {
[self setUp];
}
return self;
}
- (void)awakeFromNib
{
//called when creating label using Interface Builder
[self setUp];
}
- (void)setText:(NSString *)text
{
super.text = text;
//set layer text
[self textLayer].string = text;
}
- (void)setTextColor:(UIColor *)textColor
{
super.textColor = textColor;
//set layer text color
[self textLayer].foregroundColor = textColor.CGColor;
}
- (void)setFont:(UIFont *)font
{
super.font = font;
//set layer font
CFStringRef fontName = (__bridge CFStringRef)font.fontName;
CGFontRef fontRef = CGFontCreateWithFontName(fontName);
[self textLayer].font = fontRef;
[self textLayer].fontSize = font.pointSize;
?
CGFontRelease(fontRef);
}
@end
~~~
如果你運行代碼,你會發現文本并沒有像素化,而我們也沒有設置`contentsScale`屬性。把`CATextLayer`作為宿主圖層的另一好處就是視圖自動設置了`contentsScale`屬性。
在這個簡單的例子中,我們只是實現了`UILabel`的一部分風格和布局屬性,不過稍微再改進一下我們就可以創建一個支持`UILabel`所有功能甚至更多功能的`LayerLabel`類(你可以在一些線上的開源項目中找到)。
如果你打算支持iOS 6及以上,基于`CATextLayer`的標簽可能就有有些局限性。但是總得來說,如果想在app里面充分利用`CALayer`子類,用`+layerClass`來創建基于不同圖層的視圖是一個簡單可復用的方法。
- Introduction
- 1. 圖層樹
- 1.1 圖層與視圖
- 1.2 圖層的能力
- 1.3 使用圖層
- 1.4 總結
- 2. 寄宿圖
- 2.1 contents屬性
- 2.2 Custom Drawing
- 2.3 總結
- 3. 圖層幾何學
- 3.1 布局
- 3.2 錨點
- 3.3 坐標系
- 3.4 Hit Testing
- 3.5 自動布局
- 3.6 總結
- 4. 視覺效果
- 4.1 圓角
- 4.2 圖層邊框
- 4.3 陰影
- 4.4 圖層蒙板
- 4.5 拉伸過濾
- 4.6 組透明
- 4.7 總結
- 5. 變換
- 5.1 仿射變換
- 5.2 3D變換
- 5.3 固體對象
- 5.4 總結
- 6. 專用圖層
- 6.1 CAShapeLayer
- 6.2 CATextLayer
- 6.3 CATransformLayer
- 6.4 CAGradientLayer
- 6.5 CAReplicatorLayer
- 6.6 CAScrollLayer
- 6.7 CATiledLayer
- 6.8 CAEmitterLayer
- 6.9 CAEAGLLayer
- 6.10 AVPlayerLayer
- 6.11 總結
- 7. 隱式動畫
- 7.1 事務
- 7.2 完成塊
- 7.3 圖層行為
- 7.4 呈現與模型
- 7.5 總結
- 8. 顯式動畫
- 8.1 屬性動畫
- 8.2 動畫組
- 8.3 過渡
- 8.4 在動畫過程中取消動畫
- 8.5 總結
- 9. 圖層時間
- 9.1 CAMediaTiming協議
- 9.2 層級關系時間
- 9.3 手動動畫
- 9.4 總結
- 10. 緩沖
- 10.1 動畫速度
- 10.2 自定義緩沖函數
- 10.3 總結
- 11. 基于定時器的動畫
- 11.1 定時幀
- 11.2 物理模擬
- 12. 性能調優
- 12.1. CPU VS GPU
- 12.2 測量,而不是猜測
- 12.3 Instruments
- 12.4 總結
- 13. 高效繪圖
- 13.1 軟件繪圖
- 13.2 矢量圖形
- 13.3 臟矩形
- 13.4 異步繪制
- 13.5 總結
- 14. 圖像IO
- 14.1 加載和潛伏
- 14.2 緩存
- 14.3 文件格式
- 14.4 總結
- 15. 圖層性能
- 15.1 隱式繪制
- 15.2 離屏渲染
- 15.3 混合和過度繪制
- 15.4 減少圖層數量
- 15.5 總結