6 Specialzed layers 特殊层 第一部分 读书笔记

时间:2022-12-28 18:49:44

6 Specialzed layers 特殊层  第一部分  读书笔记

 

Specialization is a feature of every complex organization.

专注是每个复杂系统的特性

Catharine R. Stimpson

 

Up to this point, we have been working with the CALayer class, and we have seen that it has some useful image drawing and transformation capabilities. But Core Animation layers can be used for more than just images and colors. This chapter explores some of the other layer classes that you can use to extend Core Animation's drawing capabilities.

这章我们将研究能用来扩展core animation 绘制能力的的其他
的类。

CAShapeLayer

In Chapter 4, "Visual Effects," you learned how to use CGPath to create arbitrarily shaped shadows without using images. It would be neat if we could create arbitrarily shaped layers in the same way.

 

CAShapeLayer is a layer subclass that draws itself using vector graphics instead of a bitmap image. You specify attributes such as color and line thickness, define the desired shape using a CGPath, and CAShapeLayer renders it automatically.

你指明属性例如 color and line thickness ,用CGPath定义需要的图片,CAShapeLayer自动的把他们显示出来。 

Of course, you could use Core Graphics to draw a path directly into the contents of an ordinary CALayer (as in Chapter 2, "The Backing Image"), but there are several advantages to using CAShapeLayer instead:

你可以使用普通的CALayer,但是使用CAShaperLayer有下列好处:

 

▪ It's fast—CAShapeLayer uses hardware-accelerated drawing and is much faster than using Core Graphics to draw an image.

更快

 

▪ It's memory efficient—A CAShapeLayer does not have to create a backing image like an ordinary CALayer does, so no matter how large it gets, it won't consume much memory.

内存更有效

 

▪ It doesn't  get clipped to the layer bounds—A CAShapeLayer can happily draw outside of its bounds. Your path will not get clipped like it does when you draw into a regular CALayer using Core Graphics (as you saw in Chapter 2).

不会被layer bounds 剪切

 

▪ There's no pixelation  — When you transform a CAShapeLayer by scaling it up or moving it closer to the camera with a 3D perspective transform, it does not become pixelated like an ordinary layer's backing image would.

不会像素失真

 

Creating a CGPath

 

CAShapeLayer can be used to draw any shape that can be represented by a CGPath.

CAShapeLayer 能够绘制任何能用CGPath绘制的形状 

The shape doesn't have to be closed, and the path doesn't have to be unbroken, so you can actually draw several distinct shapes in a single layer. 形状不一定是闭合的,也不一定是不可分割的。

You can control the strokeColor and fillColor of the path, along with other properties such as lineWidth (line thickness, in points), lineCap (how the ends of lines look), and lineJoin (how the joints between lines look); but you can only set these properties once, at the layer level. If you want to draw multiple shapes with different colors or styles, you have to use a separate layer for each shape.

Listing 6.1 shows the code for a simple stick figure drawing, rendered using a single CAShapeLayer. The CAShapeLayer path property is defined as a CGPathRef, but we've created the path using the UIBezierPath helper class, which saves us from having to worry about manually releasing the CGPath. Figure 6.1 shows the result. It's not exactly a Rembrandt, but you get the idea!

Listing 6.1 Drawing a Stick Figure Using CAShapeLayer #import "DrawingView.h"

 

#import <QuartzCore/QuartzCore.h>
@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;

@end

@implementation ViewController

- (void)viewDidLoad {

[super viewDidLoad];

//create path

 

UIBezierPath *path = [[UIBezierPath alloc] init];

[path moveToPoint:CGPointMake(175, 100)];

 endAngle:2*M_PI

clockwise:YES];
[path moveToPoint:CGPointMake(150, 125)];

 

[path addLineToPoint:CGPointMake(150, 175)];

[path addLineToPoint:CGPointMake(125, 225)];

[path moveToPoint:CGPointMake(150, 175)];

[path addLineToPoint:CGPointMake(175, 225)];

[path moveToPoint:CGPointMake(100, 150)];

[path addLineToPoint:CGPointMake(200, 150)];

//create shape layer

 

CAShapeLayer *shapeLayer = [CAShapeLayer layer];

shapeLayer.strokeColor = [UIColor redColor].CGColor;

shapeLayer.fillColor = [UIColor clearColor].CGColor;

shapeLayer.lineWidth = 5;

 

shapeLayer.lineJoin = kCALineJoinRound;

shapeLayer.lineCap = kCALineCapRound;

shapeLayer.path = path.CGPath;

//add it to our view

[self.containerView.layer addSublayer:shapeLayer]; }

 

@end

6 Specialzed layers 特殊层  第一部分  读书笔记

Figure 6.1 A simple stick figure displayed using CAShapeLayer

Rounded Corners, Redux

 

Chapter 2 mentioned that CAShapeLayer provides an alternative way to create a view with rounded corners, as opposed to using the CALayer cornerRadius property. Although using a CAShapeLayer is a bit more work, it has the advantage that it allows us to specify the radius of each corner independently.

尽管使用CAShapeLayer有点麻烦,但是可以为每个角独立的指定弧度。

We could create a rounded rectangle path manually using individual straight lines and arcs, but UIBezierPath actually has some convenience constructors for creating rounded rectangles automatically. The following code snippet produces a path with three rounded corners and one sharp:

//define path parameters

 

CGRect rect = CGRectMake(50, 50, 100, 100);

CGSize radii = CGSizeMake(20, 20);

UIRectCorner corners = UIRectCornerTopRight | UIRectCornerBottomRight | UIRectCornerBottomLeft;

//create path

 

UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:radii];

 

 

We can use a CAShapeLayer with this path to create a view with mixed sharp and rounded corners.

我们可以利用CAShapeLayer 有这个path来创建一个固定的角度和圆角的view 

If we want to clip the view's contents to this shape, we can use our CAShapeLayer as the mask property of the view's backing layer instead of adding it as a sublayer. (See Chapter 4, "Visual Effects," for a full explanation of layer masks.)

CATextLayer  

 

A user interface cannot be constructed from images alone. A well-designed icon can do a great job of conveying the purpose of a button or control, but sooner or later you're going to need a good old-fashioned text label.

 

 

If you want to display text in a layer, there is nothing stopping you from using the layer delegate to draw a string directly into the layer contents with Core Graphics (which is essentially how UILabel works).

使用layer 代理去绘制一个String 直接到layer 的content是
用Core Graphics . 

This is a cumbersome approach if you are working directly with layers, though, instead of layer-backed views.

如果你想直接与layers工作,而不是在layer-backed views时,会是笨重的方法。

You would need to create a class that can act as the layer delegate for each layer that displays text, and then write logic to determine which layer should display which string, not to mention keeping track of the different fonts, colors, and so on.

 

Fortunately, this is unnecessary. Core Animation provides a subclass of CALayer called CATextLayer that encapsulates most of the string drawing features of UILabel in layer form and adds a few extra features for good measure.

CATextLayer囊括了UILabel string 绘制的特性并添加了一些更好的特色。

CATextLayer also renders much faster than UILabel. It's a little-known fact that on iOS6 and earlier, UILabel actually uses WebKit to do its text drawing, which carries a significant performance overhead when you are drawing a lot of text. CATextLayer uses Core Text and is significantly faster.

Let's try displaying some text using a CATextLayer. Listing 6.2 shows the code to set up and display a CATextLayer, and Figure 6.2 shows the result.

 

Listing 6.2 Implementing a Text Label Using CATextLayer

@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 Specialzed layers 特殊层  第一部分  读书笔记

 

Figure 6.2 A plain text label implemented using CATextLayer

 

If you look at this text closely, you'll see that something is a bit odd; the text is pixelated. That's because it's not being rendered at Retina resolution. Chapter 2 mentioned the contentsScale property, which is used to determine the resolution at which the layer contents are rendered. The contentsScale property always defaults to 1.0 instead of the screen scale factor. If we want Retina-quality text, we have to set the contentsScale of our CATextLayer to match the screen scale using the following line of code:需要设置contentsScale属性
来设为screen scale 因子。

textLayer.contentsScale = [UIScreen mainScreen].scale;

 

This solves the pixelation problem (see Figure 6.3).

6 Specialzed layers 特殊层  第一部分  读书笔记

The CATextLayer font property is not a UIFont, it's a CFTypeRef.

CATextLayer  font 属性不是一个UIFont,是CFTypeRef。

This allows you to specify the font using either a CGFontRef or a CTFontRef (a Core Text font), depending on your requirements. The font size is also set independently using the fontSize property, because CTFontRef and CGFontRef do not encapsulate the point size like UIFont does. The example shows how to convert from a UIFont to a CGFontRef.

 

Also, the CATextLayer string property is not an NSString as you might expect, but is typed as id.

CATextLayer string属性不是NSString,而是id类型的。

This is to allow you the option of using an NSAttributedString instead of an NSString to specify the text (NSAttributedString is not a subclass of NSString). Attributed strings are the mechanism that iOS uses for rendering styled text. They specify style runs, which are specific ranges of the string to which metadata such as font, color, bold, italic, and so forth are attached.

Rich Text

 

In iOS 6, Apple added direct support for attributed strings to UILabel and to other UIKit text views.

在iOS6上,苹果提供了一个attributed strings 给UILabel 和其他UIKit text views .

This is a handy feature that makes attributed strings much easier to work with, but CATextLayer has supported attributed strings since its introduction in iOS 3.2; so if you still need to support earlier iOS versions with your app, CATextLayer is a great way to add simple rich text labels to your interface without having to deal with the complexity of Core Text or the hassle of using a UIWebView.

Let's modify the example to use an NSAttributedString (see Listing 6.3). On iOS 6 and above we could use the new NSTextAttributeName constants to set up our string

attributes, but because the point of the exercise is to demonstrate that this feature also works on iOS 5 and below, we've used the Core Text equivalents instead. This means that you'll need to add the Core Text framework to your project; otherwise, the compiler won't recognize the attribute constants.

Figure 6.4 shows the result. (Note the red, underlined text.)

Listing 6.3 Implementing a Rich Text Label Using NSAttributedString

 

#import "DrawingView.h"

#import <QuartzCore/QuartzCore.h>

#import <CoreText/CoreText.h>

@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";

6 Specialzed layers 特殊层  第一部分  读书笔记

//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 Specialzed layers 特殊层  第一部分  读书笔记

 

 

Figure 6.4 A rich text label implemented using CATextLayer

Leading and Kerning

 

It's worth mentioning that the leading (line spacing) and kerning (spacing between letters) for text rendered using CATextLayer is not completely identical to that of the string rendering used by UILabel due to the different drawing implementations (Core Text and WebKit, respectively).

leading 行间距
和kerning 两个字符的间距 

 

The extent of the discrepancy varies (depending on the specific font and characters used) and is generally fairly minor, but you should keep this mind if you are trying to exactly match appearance between regular labels and a CATextLayer.

一般差距很小,但是一般情况下很少,但是如果你要注意这事在使用普通的labels和CATextLayer的时候。

A UILabel Replacement

We've established that CATextLayer has performance benefits over UILabel, as well as some additional layout options and support for rich text on iOS 5. But it's fairly cumbersome to use by comparison to a regular label. If we want to make a truly usable replacement for UILabel, we should be able to create our labels in Interface Builder, and they should behave as much as possible like regular views.

 

We could subclass UILabel and override its methods to display the text in a CATextLayer that we've added as a sublayer, but we'd still have the redundant empty backing image created by the presence of UILabel -drawRect: method. And because CALayer doesn't support autoresizing or autolayout, a sublayer wouldn't track the size of the view bounds automatically, so we would need to manually update the sublayer bounds every time the view is resized.

我们可以创建UILabel 的子类并重写它的方法来展示我们添加到为一个sublayer的CATextLayer.

What we really want is a UILabel subclass that actually uses a CATextLayer as its backing layer, then it would automatically resize with the view and there would be no redundant backing image to worry about.

 

As we discussed in Chapter 1, "The Layer Tree," every UIView is backed by an instance of CALayer. That layer is automatically created and managed by the view, so how can we substitute a different layer type? We can't replace the layer once it has been created, but if we subclass UIView, we can override the +layerClass method to return a different layer subclass at creation time. UIView calls the +layerClass method during its initialization, and uses the class it returns to create its backing layer.

但是如果我们继承UIView,我们可以重载+layerClass 方法
返回一个不同的layer 子类在创建的时候。UIView调用layerClass 方法在初始化的时候,使用这个类返回创建的backing layer.

 

Listing 6.4 shows the code for a UILabel subclass called LayerLabel that draws its text using a CATextLayer instead of the using the slower –drawRect: approach that an ordinary UILabel uses. LayerLabel instances can either be created program-matically or via Interface Builder by adding an ordinary label to the view and setting its class to LayerLabel.

 

Listing 6.4 LayerLabel, a UILabel Subclass That Uses CATextLayer

#import "LayerLabel.h"

 

#import <QuartzCore/QuartzCore.h>

@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

If you run the sample code, you'll notice that the text isn't pixelated even though we aren't setting the contentsScale anywhere. Another benefit of implementing CATextLayer as a backing layer is that its contentsScale is automatically set by the view.

In this simple example, we've only implemented a few of the styling and layout properties of UILabel, but with a bit more work we could create a LayerLabel class that supports the full functionality of UILabel and more (you will find several such classes already available as open source projects online).

If you only intend to support iOS 6 and above, a CATextLayer-based label may be of limited use. But in general, using +layerClass to create views backed by different layer types is a clean and reusable way to utilize CALayer subclasses in your apps.

CATransformLayer

When constructing complex objects in 3D, it is convenient to be able to organize the individual elements hierarchically. For example, suppose you were making an arm: You would want the hand to be a child of the wrist, which would be a child of the forearm, which would be a child of the elbow, which would be a child of the upper arm, which would be a child of the shoulder, and so on.

The reason for this is that it allows you to move each section independently. Pivoting the elbow would move the lower arm and hand but not the shoulder. Core Animation layers easily allow for this kind of hierarchical arrangement in 2D, but in 3D it's not possible because each layer flattens its children into a single plane (as explained in Chapter 5, "Transforms").

CATransformLayer solves this problem. A CATransformLayer is unlike a regular CALayer in that it cannot display any content of its own; it exists only to host a transform that can be applied to its sublayers. CATransformLayer does not flatten its sublayers, so it can be used to construct a hierarchical 3D structure, such as our arm example.

Creating an arm programmatically would require rather a lot of code, so we'll demonstrate this with something a bit simpler: In the cube example in Chapter 5, we worked around the layer-flattening problem by rotating the camera instead of the cube by using the sublayerTransform of the containing layer. This is a neat trick, but only works for a single object. If our scene contained two cubes, we would not be able to rotate them independently using this technique.

So, let's try it using a CATransformLayer instead. The first problem to address is that we constructed our cube in Chapter 5 using views rather than standalone layers. We cannot place a view-backing layer inside another layer that is not itself a view-backing layer without messing up the view hierarchy. We could create a new UIView subclass backed by a CATransformLayer (using the +layerClass method), but to keep things simple for our example, let's just re-create the cube using standalone layers instead of views. This means we can't display buttons and labels on our cube faces like we did in Chapter 5, but we don't need to do that right now.

Listing 6.5 contains the code. We position each cube face using the same basic logic we used in Chapter 5. But instead of adding the cube faces directly to the container view's backing layer as we did before, we place them inside a CATransformLayer to create a standalone cube object, and then place two such cubes into our container. We've colored the cube faces randomly so as to make it easier to distinguish them without labels or lighting. Figure 6.5 shows the result.

 

Listing 6.5 Assembling a 3D Layer Hierarchy Using CATransformLayer

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView; @end
@implementation ViewController

- (CALayer *)faceWithTransform:(CATransform3D)transform {

//create cube face layer

CALayer *face = [CALayer layer];
face.frame = CGRectMake(-50, -50, 100, 100);

//apply a random color

 

CGFloat red = (rand() / (double)INT_MAX);

CGFloat green = (rand() / (double)INT_MAX);

CGFloat blue = (rand() / (double)INT_MAX);

face.backgroundColor = [UIColor colorWithRed:red 

green:green blue:blue

 

alpha:1.0].CGColor;

 

//apply the transform and return

face.transform = transform;

 

return face;

}

 

 

- (CALayer *)cubeWithTransform:(CATransform3D)transform {

//create cube layer

CATransformLayer *cube = [CATransformLayer layer];

//add cube face 1

 

CATransform3D ct = CATransform3DMakeTranslation(0, 0, 50);

[cube addSublayer:[self faceWithTransform:ct]];

//add cube face 2

 

ct = CATransform3DMakeTranslation(50, 0, 0);

ct = CATransform3DRotate(ct, M_PI_2, 0, 1, 0);

[cube addSublayer:[self faceWithTransform:ct]];

//add cube face 3

 

ct = CATransform3DMakeTranslation(0, -50, 0);

ct = CATransform3DRotate(ct, M_PI_2, 1, 0, 0);

[cube addSublayer:[self faceWithTransform:ct]];

//add cube face 4

 

ct = CATransform3DMakeTranslation(0, 50, 0);

ct = CATransform3DRotate(ct, -M_PI_2, 1, 0, 0);

[cube addSublayer:[self faceWithTransform:ct]];

//add cube face 5

 

ct = CATransform3DMakeTranslation(-50, 0, 0);

ct = CATransform3DRotate(ct, -M_PI_2, 0, 1, 0);

[cube addSublayer:[self faceWithTransform:ct]];

//add cube face 6

 

ct = CATransform3DMakeTranslation(0, 0, -50);

ct = CATransform3DRotate(ct, M_PI, 0, 1, 0);

[cube addSublayer:[self faceWithTransform:ct]];

//center the cube layer within the container

 

CGSize containerSize = self.containerView.bounds.size;

cube.position = CGPointMake(containerSize.width / 2.0,

containerSize.height / 2.0);

//apply the transform and return

cube.transform = transform;

return cube; }

  • - &nbsp;&nbsp;(void)viewDidLoad {

[super viewDidLoad];

//set up the perspective transform

CATransform3D pt = CATransform3DIdentity; pt.m34 = -1.0 / 500.0; self.containerView.layer.sublayerTransform = pt;

//set up the transform for cube 1 and add it

 

CATransform3D c1t = CATransform3DIdentity;

c1t = CATransform3DTranslate(c1t, -100, 0, 0);

CALayer *cube1 = [self cubeWithTransform:c1t];

[self.containerView.layer addSublayer:cube1];

//set up the transform for cube 2 and add it

 

CATransform3D c2t = CATransform3DIdentity;

c2t = CATransform3DTranslate(c2t, 100, 0, 0);

c2t = CATransform3DRotate(c2t, -M_PI_4, 1, 0, 0);

c2t = CATransform3DRotate(c2t, -M_PI_4, 0, 1, 0);

CALayer *cube2 = [self cubeWithTransform:c2t];

[self.containerView.layer addSublayer:cube2];

}

 

@end

6 Specialzed layers 特殊层  第一部分  读书笔记

 

Figure 6.5 Two cubes with shared perspective but different transforms applied