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

  1. @interface ViewController ()
  2. @property (nonatomic, weak) IBOutlet UIView *labelView;
  3. @end
  4. @implementation ViewController
  5. - (void)viewDidLoad
  6. {
  7. [super viewDidLoad];
  8. //create a text layer
  9. CATextLayer *textLayer = [CATextLayer layer];
  10. textLayer.frame = self.labelView.bounds;
  11. [self.labelView.layer addSublayer:textLayer];
  12. //set text attributes
  13. textLayer.foregroundColor = [UIColor blackColor].CGColor;
  14. textLayer.alignmentMode = kCAAlignmentJustified;
  15. textLayer.wrapped = YES;
  16. //choose a font
  17. UIFont *font = [UIFont systemFontOfSize:15];
  18. //set layer font
  19. CFStringRef fontName = (__bridge CFStringRef)font.fontName;
  20. CGFontRef fontRef = CGFontCreateWithFontName(fontName);
  21. textLayer.font = fontRef;
  22. textLayer.fontSize = font.pointSize;
  23. CGFontRelease(fontRef);
  24. //choose some text
  25. 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";
  26. //set layer text
  27. textLayer.string = text;
  28. }
  29. @end

图6.2

图6.2 用CATextLayer来显示一个纯文本标签

如果你仔细看这个文本,你会发现一个奇怪的地方:这些文本有一些像素化了。这是因为并没有以Retina的方式渲染,第二章提到了这个contentScale属性,用来决定图层内容应该以怎样的分辨率来渲染。contentsScale并不关心屏幕的拉伸因素而总是默认为1.0。如果我们想以Retina的质量来显示文字,我们就得手动地设置CATextLayercontentsScale属性,如下:

  1. textLayer.contentsScale = [UIScreen mainScreen].scale;

这样就解决了这个问题(如图6.3)

图6.3

图6.3 设置contentsScale来匹配屏幕

CATextLayerfont属性不是一个UIFont类型,而是一个CFTypeRef类型。这样可以根据你的具体需要来决定字体属性应该是用CGFontRef类型还是CTFontRef类型(Core Text字体)。同时字体大小也是用fontSize属性单独设置的,因为CTFontRefCGFontRef并不像UIFont一样包含点大小。这个例子会告诉你如何将UIFont转换成CGFontRef

另外,CATextLayerstring属性并不是你想象的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实现一个富文本标签。

  1. #import "DrawingView.h"
  2. #import <QuartzCore/QuartzCore.h>
  3. #import <CoreText/CoreText.h>
  4. @interface ViewController ()
  5. @property (nonatomic, weak) IBOutlet UIView *labelView;
  6. @end
  7. @implementation ViewController
  8. - (void)viewDidLoad
  9. {
  10. [super viewDidLoad];
  11. //create a text layer
  12. CATextLayer *textLayer = [CATextLayer layer];
  13. textLayer.frame = self.labelView.bounds;
  14. textLayer.contentsScale = [UIScreen mainScreen].scale;
  15. [self.labelView.layer addSublayer:textLayer];
  16. //set text attributes
  17. textLayer.alignmentMode = kCAAlignmentJustified;
  18. textLayer.wrapped = YES;
  19. //choose a font
  20. UIFont *font = [UIFont systemFontOfSize:15];
  21. //choose some text
  22. 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";
  23. //create attributed string
  24. NSMutableAttributedString *string = nil;
  25. string = [[NSMutableAttributedString alloc] initWithString:text];
  26. //convert UIFont to a CTFont
  27. CFStringRef fontName = (__bridge CFStringRef)font.fontName;
  28. CGFloat fontSize = font.pointSize;
  29. CTFontRef fontRef = CTFontCreateWithName(fontName, fontSize, NULL);
  30. //set text attributes
  31. NSDictionary *attribs = @{
  32. (__bridge id)kCTForegroundColorAttributeName:(__bridge id)[UIColor blackColor].CGColor,
  33. (__bridge id)kCTFontAttributeName: (__bridge id)fontRef
  34. };
  35. [string setAttributes:attribs range:NSMakeRange(0, [text length])];
  36. attribs = @{
  37. (__bridge id)kCTForegroundColorAttributeName: (__bridge id)[UIColor redColor].CGColor,
  38. (__bridge id)kCTUnderlineStyleAttributeName: @(kCTUnderlineStyleSingle),
  39. (__bridge id)kCTFontAttributeName: (__bridge id)fontRef
  40. };
  41. [string setAttributes:attribs range:NSMakeRange(6, 5)];
  42. //release the CTFont we created earlier
  43. CFRelease(fontRef);
  44. //set layer text
  45. textLayer.string = string;
  46. }
  47. @end

图6.4

图6.4 用CATextLayer实现一个富文本标签。

行距和字距

有必要提一下的是,由于绘制的实现机制不同(Core Text和WebKit),用CATextLayer渲染和用UILabel渲染出的文本行距和字距也不是不尽相同的。

二者的差异程度(由使用的字体和字符决定)总的来说挺小,但是如果你想正确的显示普通便签和CATextLayer就一定要记住这一点。

UILabel的替代品

我们已经证实了CATextLayerUILabel有着更好的性能表现,同时还有额外的布局选项并且在iOS 5上支持富文本。但是与一般的标签比较而言会更加繁琐一些。如果我们真的在需求一个UILabel的可用替代品,最好是能够在Interface Builder上创建我们的标签,而且尽可能地像一般的视图一样正常工作。

我们应该继承UILabel,然后添加一个子图层CATextLayer并重写显示文本的方法。但是仍然会有由UILabel-drawRect:方法创建的空寄宿图。而且由于CALayer不支持自动缩放和自动布局,子视图并不是主动跟踪视图边界的大小,所以每次视图大小被更改,我们不得不手动更新子图层的边界。

我们真正想要的是一个用CATextLayer作为宿主图层的UILabel子类,这样就可以随着视图自动调整大小而且也没有冗余的寄宿图啦。

就像我们在第一章『图层树』讨论的一样,每一个UIView都是寄宿在一个CALayer的示例上。这个图层是由视图自动创建和管理的,那我们可以用别的图层类型替代它么?一旦被创建,我们就无法代替这个图层了。但是如果我们继承了UIView,那我们就可以重写+layerClass方法使得在创建的时候能返回一个不同的图层子类。UIView会在初始化的时候调用+layerClass方法,然后用它的返回类型来创建宿主图层。

清单6.4 演示了一个UILabel子类LayerLabelCATextLayer绘制它的问题,而不是调用一般的UILabel使用的较慢的-drawRect:方法。LayerLabel示例既可以用代码实现,也可以在Interface Builder实现,只要把普通的标签拖入视图之中,然后设置它的类是LayerLabel就可以了。

清单6.4 使用CATextLayerUILabel子类:LayerLabel

  1. #import "LayerLabel.h"
  2. #import <QuartzCore/QuartzCore.h>
  3. @implementation LayerLabel
  4. + (Class)layerClass
  5. {
  6. //this makes our label create a CATextLayer //instead of a regular CALayer for its backing layer
  7. return [CATextLayer class];
  8. }
  9. - (CATextLayer *)textLayer
  10. {
  11. return (CATextLayer *)self.layer;
  12. }
  13. - (void)setUp
  14. {
  15. //set defaults from UILabel settings
  16. self.text = self.text;
  17. self.textColor = self.textColor;
  18. self.font = self.font;
  19. //we should really derive these from the UILabel settings too
  20. //but that's complicated, so for now we'll just hard-code them
  21. [self textLayer].alignmentMode = kCAAlignmentJustified;
  22. [self textLayer].wrapped = YES;
  23. [self.layer display];
  24. }
  25. - (id)initWithFrame:(CGRect)frame
  26. {
  27. //called when creating label programmatically
  28. if (self = [super initWithFrame:frame]) {
  29. [self setUp];
  30. }
  31. return self;
  32. }
  33. - (void)awakeFromNib
  34. {
  35. //called when creating label using Interface Builder
  36. [self setUp];
  37. }
  38. - (void)setText:(NSString *)text
  39. {
  40. super.text = text;
  41. //set layer text
  42. [self textLayer].string = text;
  43. }
  44. - (void)setTextColor:(UIColor *)textColor
  45. {
  46. super.textColor = textColor;
  47. //set layer text color
  48. [self textLayer].foregroundColor = textColor.CGColor;
  49. }
  50. - (void)setFont:(UIFont *)font
  51. {
  52. super.font = font;
  53. //set layer font
  54. CFStringRef fontName = (__bridge CFStringRef)font.fontName;
  55. CGFontRef fontRef = CGFontCreateWithFontName(fontName);
  56. [self textLayer].font = fontRef;
  57. [self textLayer].fontSize = font.pointSize;
  58. CGFontRelease(fontRef);
  59. }
  60. @end

如果你运行代码,你会发现文本并没有像素化,而我们也没有设置contentsScale属性。把CATextLayer作为宿主图层的另一好处就是视图自动设置了contentsScale属性。

在这个简单的例子中,我们只是实现了UILabel的一部分风格和布局属性,不过稍微再改进一下我们就可以创建一个支持UILabel所有功能甚至更多功能的LayerLabel类(你可以在一些线上的开源项目中找到)。

如果你打算支持iOS 6及以上,基于CATextLayer的标签可能就有有些局限性。但是总得来说,如果想在app里面充分利用CALayer子类,用+layerClass来创建基于不同图层的视图是一个简单可复用的方法。