iOS核心动画高级技巧--(六)专用图层

Core Animation图层不仅仅能作用于图片和颜色而已。本章就会学习其他的一些图层类,进一步扩展使用Core Animation绘图的能力。

CAShapeLayer

在前面『视觉效果』我们学习到了不使用图片的情况下用 CGPath去构造任意形状的阴影。如果我们能用同样的方式创建相同形状的图层就好了。

CAShapeLayer 是一个通过矢量图形而不是bitmap来绘制的图层子类。你指定诸如颜色和线宽等属性,用 CGPath 来定义想要绘制的图形,最后CAShapeLayer 就自动渲染出来了。当然,你也可以用Core Graphics直接向原始的CALayer的内容中绘制一个路径,相比直下,使用 CAShapeLayer有以下一 些优点:

  • 渲染快速。 CAShapeLayer使用了硬件加速,绘制同一图形会比用Core Graphics快很多。
  • 高效使用内存。一个 CAShapeLayer 不需要像普通 CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。
  • 不会被图层边界剪裁掉。一个 CAShapeLayer可以在边界之外绘制。你的图层路径不会像在使用Core Graphics的普通CALayer 一样被剪裁掉。
  • 不会出现像素化。当你给 CAShapeLayer3D变换时,它不像一个有寄宿图的普通图层一样变得像素化。
创建一个 CGPath

CAShapeLayer 可以用来绘制所有能够通过 CGPath 来表示的形状。这个形状不 一定要闭合,图层路径也不一定要不可破,事实上你可以在一个图层上绘制好几个不同的形状。你可以控制一些属性比如lineWidth(线宽,用点表示单位), lineCap(线条结尾的样子),和lineJoin(线条之间的结合点的样子);但是在图层层面你只有一次机会设置这些属性。如果你想用不同颜色或风格来绘制多个形状,就不得不为每个形状准备一个图层了。

Demo
使用代码用一个CAShapeLayer渲染一个简单的火柴人。CAShapeLayer属性是CGPathRef类型,但是我们用UIBezierPath 帮助类创建了图层路径,这样我们就不用考虑人工释放CGpath 了。

- (void)viewDidLoad
{
[super viewDidLoad];
//create path
UIBezierPath *path = [[UIBezierPath alloc] init]; [path moveToPoint:CGPointMake(175, 100)];
[path addArcWithCenter:CGPointMake(150, 100) radius:25 startAngle:0 endAngle:M_PI * 2 clockwise:true];
  [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];
}
CAShapeLayer 绘制一个简单的火柴人.png
圆角

前面提到了CAShapeLayer为创建圆角视图提供了一个方法,就是CALayercornerRadius属性。虽然使用CAShapeLayer类需要更多的工作,但是它有一个优势就是可以单独指定每个角。

我们创建圆角矩形其实就是人工绘制单独的直线和弧度,但是事实上 UIBezierPath有自动绘制圆角矩形的构造方法,下面这段代码绘制了一个有 三个圆角一个直角的矩形:

//define path parameters
CGRect rect = CGRectMake(50, 50, 100, 100);

CGSize radii = CGSizeMake(20, 20);
//create path
UIRectCorner corners = UIRectCornerTopRight | UIRectCornerBottomLeft | UIRectCornerTopRight;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rectbyRoundingCorners:corners cornerRadii:radii];

我们可以通过这个图层路径绘制一个既有直角又有圆角的视图。如果我们想依照此 图形来剪裁视图内容,我们可以把 CAShapeLayer 作为视图的宿主图层,而不是添加一个子视图。

CATextLayer

如果你想在一个图层里面显示文字,完全可以借助图层代理直接将字符串使用Core Graphics写入图层的内容(这就是UILabel的精髓)。如果越过寄宿于图层的视图,直接在图层上操作,那其实相当繁琐。你要为每一个显示文字的图层创建一个 能像图层代理一样工作的类,还要逻辑上判断哪个图层需要显示哪个字符串,更别 提还要记录不同的字体,颜色等一系列乱七八糟的东西。

万幸的是这些都是不必要的,Core Animation提供了一个CALayer的子类 CATextLayer ,它以图层的形式包含了UILabel几乎所有的绘制特性,并且额外提供了一些新的特性。

同样,CATextLayer也要比 UILabel渲染得快得多。很少有人知道在iOS 6及之 前的版本,UILabel其实是通过WebKit来实现绘制的,这样就造成了当有很多文 字的时候就会有极大的性能压力。而 CATextLayer 使用了Core text,并且渲染得非常快。

让我们来尝试用 CATextLayer 来显示一些文字即用 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);

NSString *text = @"街坊邻居撒砥砺奋进阿卡丽是点击分类卡萨丁";
  //set layer text
  textLayer.string = text;
}
@end 

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

 textLayer.contentsScale = [UIScreen mainScreen].scale;

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

另外, CATextLayerstring属性并不是你想象的NSString类型,而是id类型。这样你既可以用 NNString也可以用NSAttributedString 来指定文本了(注意, NSAttributedString并不是NSString 的子类)。属性化字符串是iOS用来渲染字体风格的机制,它以特定的方式来决定指定范围内的字符串的原始信息,比如字体,颜色,字重,斜体等。

富文本

iOS 6中,AppleUILabel和其他UIKit文本视图添加了直接的属性化字符串的支持,应该说这是一个很方便的特性。不过事实上从iOS3.2开始 CATextLayer就已经支持属性化字符串了。这样的话,如果你想要支持更低版本的iOS系统, CATextLayer无疑是你向界面中增加富文本的好办法,而且也不用去跟复杂 的Core Text打交道,也省了用UIWebView的麻烦。

iOS 6及以上我们可以用新的NSTextAttributeName实例来设置我们的字符串属性,但是练习的 目的是为了演示在iOS 5及以下,所以我们用了Core Text,也就是说你需要把Core Text framework添加到你的项目中。否则,编译器是无法识别属性常量的。

行距和字距

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

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

UILabel 的替代品

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

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

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

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

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

#import "LayerLabel.h"
@implementation LayerLabel
+ (Class)layerClass
{
  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 来 创建基于不同图层的视图是一个简单可复用的方法。

CATransformLayer

当我们在构造复杂的3D事物的时候,如果能够组织独立元素就太方便了。比如说, 你想创造一个孩子的手臂:你就需要确定哪一部分是孩子的手腕,哪一部分是孩子 的前臂,哪一部分是孩子的肘,哪一部分是孩子的上臂,哪一部分是孩子的肩膀等等。

当然是允许独立地移动每个区域的啦。以肘为指点会移动前臂和手,而不是肩膀。 Core Animation图层很容易就可以让你在2D环境下做出这样的层级体系下的变换, 但是3D情况下就不太可能,因为所有的图层都把他的孩子都平面化到一个场景中

CATransformLayer解决了这个问题, CATransformLayer不同于普通的CALayer,因为它不能显示它自己的内容。只有当存在了一个能作用域子图层的变换它才真正存在。 CATransformLayer并不平面化它的子图层,所以它能够用于构造一个层级的3D结构,比如我的手臂示例。

用代码创建一个手臂需要相当多的代码,所以我就演示得更简单一些吧:在第五章 的立方体示例,我们将通过旋转camara 来解决图层平面化问题而不是像立方体示例代码中用的sublayerTransform 。这是一个非常不错的技巧,但是只能作用于单个对象上,如果你的场景包含两个立方体,那我们就不能用这个技巧单独旋转他们了。

那么,就让我们来试一试 CATransformLayer吧,第一个问题就来了:在第五章,我们是用多个视图来构造了我们的立方体,而不是单独的图层。我们不能在不打乱已有的视图层次的前提下在一个本身不是有寄宿图的图层中放置一个寄宿图图层。我们可以创建一个新的 UIView 子类寄宿
CATransformLayer (用 + layerClass 方法)之上。但是,为了简化案例, 我们仅仅重建了一个单独的图层,而不是使用视图。这意味着我们不能像第五章一 样在立方体表面显示按钮和标签,不过我们现在也用不到这个特性。

#import "ViewController.h"

@interface ViewController ()

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

@implementation ViewController

- (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];
    
    
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}


- (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 * 0.5, containerSize.width * 0.5);
    //apply the transform and return
    cube.transform = transform;
    return cube;
}
@end
同一视角下的俩不同变换的立方体.png
CAGradientLayer

CAGradientLayer是用来生成两种或更多颜色平滑渐变的。用Core Graphics复制一个CAGradientLayer 并将内容绘制到一个普通图层的寄宿图也是有可能的,
但是CAGradientLayer的真正好处在于绘制使用了硬件加速。

基础渐变

我们将从一个简单的红变蓝的对角线渐变开始.这些渐变色彩放在一 个数组中,并赋给colors 属性。这个数组成员接受 CGColorRef 类型的值(并不是从 NSObject派生而来),所以我们要用通过bridge转换以确保编译正常。

CAGradientLayer 也有 startPointendPoint属性,他们决定了渐变的方向。这两个参数是以单位坐标系进行的定义,所以左上角坐标是{0, 0},右下角坐标 是{1, 1}

- (void)viewDidLoad
{
  [super viewDidLoad];
  //create gradient layer and add it to our container view
  CAGradientLayer *gradientLayer = [CAGradientLayer layer];
  gradientLayer.frame = self.containerView.bounds;
  [self.containerView.layer addSublayer:gradientLayer];
  //set gradient colors
  gradientLayer.colors = [UIColor redColor].CGColor;
  //set gradient start and end points
  gradientLayer.startPoint = CGPointMake(0, 0);
  gradientLayer.endPoint = CGPointMake(1, 1);
}
CAGradientLayer 实现简单的两种颜色的对角线渐变.png
多重渐变

如果你愿意, colors属性可以包含很多颜色,所以创建一个彩虹一样的多重渐变 也是很简单的。默认情况下,这些颜色在空间上均匀地被渲染,但是我们可以用locations属性来调整空间。 locations属性是一个浮点数值的数组(以NSNumber包装)。这些浮点数定义了 colors属性中每个不同颜色的位 置,同样的,也是以单位坐标系进行标定。0.0代表着渐变的开始,1.0代表着结束。

locations数组并不是强制要求的,但是如果你给它赋值了就一定要确保locations的数组大小和 colors数组大小一定要相同,否则你将会得到一个 空白的渐变。

Demo
基于上面代码对角线渐变的代码改造。现在变成了从红到黄最后到绿色的渐变。 locations 数组指定了0.00.250.5三个数值,这样这三个渐变就有点像挤在了左上角。

- (void)viewDidLoad
{
  [super viewDidLoad];
  //create gradient layer and add it to our container view
  CAGradientLayer *gradientLayer = [CAGradientLayer layer];
  gradientLayer.frame = self.containerView.bounds;
  [self.containerView.layer addSublayer:gradientLayer];
  //set gradient colors
  gradientLayer.colors = [UIColor redColor].CGColor;
  //set locations
  gradientLayer.locations = @[@0.0, @0.25, @0.5];
  //set gradient start and end points
  gradientLayer.startPoint = CGPointMake(0, 0);
  gradientLayer.endPoint = CGPointMake(1, 1);
}
locations 构造偏移至左上角的三色渐变.png
CAReplicatorLayer

CAReplicatorLayer的目的是为了高效生成许多相似的图层。它会绘制一个或多个图层的子图层,并在每个复制体上应用不同的变换。看上去演示能够更加解释这 些,我们来写个例子吧。

重复图层(Repeating Layers)

我们在屏幕的中间创建了一个小白色方块图层,然后用 CAReplicatorLayer 生成十个图层组成一个圆圈。 instanceCount属性指定了图层需要重复多少次。instanceTransform指定了一个 CATransform3D 3D变换(这种情况下,下一图层的位移和旋转将会移动到圆圈的下一个点)。

变换是逐步增加的,每个实例都是相对于前一实例布局。这就是为什么这些复制体 最终不会出现在统一位置上。

- (void)viewDidLoad
{
    [super viewDidLoad];
    //create a replicator layer and add it to our view
    CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
    replicator.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:replicator];
    //configure the replicator
    replicator.instanceCount = 10;
    //apply a transform for each instance
    CATransform3D transform = CATransform3DIdentity;
    transform = CATransform3DTranslate(transform, 0, 200, 0);
    transform = CATransform3DRotate(transform, M_PI / 5.0, 0, 0, 1);
    transform = CATransform3DTranslate(transform, 0, -200, 0);
    replicator.instanceTransform = transform;
   //apply a color shift for each instance
    replicator.instanceBlueOffset = -0.1;
    replicator.instanceGreenOffset = -0.1;
    //create a sublayer and place it inside the replicator
    CALayer *layer = [CALayer layer];
    layer.frame = CGRectMake(100.0f, 100.0f, 100.0f, 100.0f);
    layer.backgroundColor = [UIColor whiteColor].CGColor;
    [replicator addSublayer:layer];
}

注意到当图层在重复的时候,他们的颜色也在变化:这是用 instanceBlueOffsetinstanceGreenOffset 属性实现的。通过逐步减少 蓝色和绿色通道,我们逐渐将图层颜色转换成了红色。这个复制效果看起来很酷, 但是CAReplicatorLayer 真正应用到实际程序上的场景比如:一个游戏中导弹的 轨迹云,或者粒子爆炸(尽管iOS 5已经引入了 CAEmitterLayer ,它更适合创建 任意的粒子效果)。除此之外,还有一个实际应用是:反射。

反射

使用 CAReplicatorLayer并应用一个负比例变换于一个复制图层,你就可以创建指定视图(或整个视图层次)内容的镜像图片,这样就创建了一个实时的『反射』 效果。让我们来尝试实现这个创意:指定一个继承于 UIViewReflectionView ,它会自动产生内容的反射效果。实现这个效果的代码很简单,实际上用ReflectionView 实现这个效果会更简单,我们只需要把 ReflectionView的实例放置于Interface Builder(见代码一), 它就会实时生成子视图的反射,而不需要别的代码.
代码一

#import "ReflectionView.h"
@implementation ReflectionView

+ (Class)layerClass
{
    return [CAReplicatorLayer class];
}
- (void)setUp
{
    //configure replicator
    CAReplicatorLayer *layer = (CAReplicatorLayer *)self.layer;
    layer.instanceCount = 2;
    //move reflection instance below original and flip vertically
    CATransform3D transform = CATransform3DIdentity;
    CGFloat verticalOffset = self.bounds.size.height + 2;
    transform = CATransform3DTranslate(transform, 0, verticalOffset, 0);
    transform = CATransform3DScale(transform, 1, -1, 0);
    layer.instanceTransform = transform;
    //reduce alpha of reflection layer
    layer.instanceAlphaOffset = -0.6;
}
- (id)initWithFrame:(CGRect)frame
{
    //this is called when view is created in code
    if ((self = [super initWithFrame:frame])) {
        [self setUp];
    }
    return self;
}
- (void)awakeFromNib
{
    //this is called when view is created from a nib
    [self setUp];
}
@end
CAScrollLayer

对于一个未转换的图层,它的 bounds 和它的 frame是一样的, frame 属性是由 bounds属性自动计算而出的,所以更改任意一个值都会更新其他值。

但是如果你只想显示一个大图层里面的一小部分呢。比如说,你可能有一个很大的 图片,你希望用户能够随意滑动,或者是一个数据或文本的长列表。在一个典型的 iOS应用中,你可能会用到UITableView 或是UIScrollView,但是对于独立的图层来说,什么会等价于刚刚提到的 UITableViewUIScrollView呢?

在前面,我们探索了图层的 contentsRect 属性的用法,它的确是能够解决 在图层中小地方显示大图片的解决方法。但是如果你的图层包含子图层那它就不是 一个非常好的解决方案,因为,这样做的话每次你想『滑动』可视区域的时候,你 就需要手工重新计算并更新所有的子图层位置。

这个时候就需要 CAScrollLayer了。CAScrollLayer 有一个- scrollToPoint:方法,它自动适应bounds 的原点以便图层内容出现在滑动的地方。注意,这就是它做的所有事情。前面提到过,Core Animation并不处理用户输 入,所以 CAScrollLayer 并不负责将触摸事件转换为滑动事件,既不渲染滚动 条,也不实现任何iOS指定行为例如滑动反弹(当视图滑动超多了它的边界的将会 反弹回正确的地方)。

让我们来用CAScrollLayer来常见一个基本的UIScrollView替代品。我们将会用CAScrollLayer作为视图的宿主图层,并创建一个自定义的 UIView,然后用UIPanGestureRecognizer实现触摸事件响应。

#import "ScrollView.h"
@implementation ScrollView
+ (Class)layerClass
{
    return [CAScrollLayer class];
}
- (void)setUp
{
    //enable clipping
    self.layer.masksToBounds = YES;
    //attach pan gesture recognizer
    UIPanGestureRecognizer *recognizer = nil;
    recognizer = [UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(pan:)
    [self addGestureRecognizer:recognizer];
}

- (id)initWithFrame:(CGRect)frame
{
    //this is called when view is created in code
    if ((self = [super initWithFrame:frame])) {
        [self setUp];
    }
    return self;
}
- (void)awakeFromNib {
    //this is called when view is created from a nib
    [self setUp];
}
- (void)pan:(UIPanGestureRecognizer *)recognizer
{
    //get the offset by subtracting the pan gesture
    //translation from the current bounds origin
    CGPoint offset = self.bounds.origin;
    offset.x -= [recognizer translationInView:self].x;
    offset.y -= [recognizer translationInView:self].y;
    //scroll the layer
    [(CAScrollLayer *)self.layer scrollToPoint:offset];
    //reset the pan gesture translation
    [recognizer setTranslation:CGPointZero inView:self];
}
@end

不同于 UIScrollView ,我们定制的滑动视图类并没有实现任何形式的边界检查 (bounds checking)。图层内容极有可能滑出视图的边界并无限滑下去。CAScrollLayer 并没有等同于 UIScrollViewcontentSize的属性,所以当 CAScrollLayer 滑动的时候完全没有一个全局的可滑动区域的概念,也无法 自适应它的边界原点至你指定的值。它之所以不能自适应边界大小是因为它不需 要,内容完全可以超过边界。

那你一定会奇怪用 CAScrollLayer 的意义到底何在,因为你可以简单地用一个普 通的CALayer 然后手动适应边界原点啊。真相其实并不复杂,UIScrollView并 没有用CAScrollLayer ,事实上,就是简单的通过直接操作图层边界来实现滑 动。

CAScrollLayer 有一个潜在的有用特性。如果你查看 CAScrollLayer的头文 件,你就会注意到有一个扩展分类实现了一些方法和属性:

- (void)scrollPoint:(CGPoint)p;
- (void)scrollRectToVisible:(CGRect)r;
@property(readonly) CGRect visibleRect;

看到这些方法和属性名,你也许会以为这些方法给每个 CALayer实例增加了滑动功能。但是事实上他们只是放置在 CAScrollLayer 中的图层的实用方法。scrollPoint: 方法从图层树中查找并找到第一个可用
CAScrollLayer ,然后滑动它使得指定点成为可视的。 scrollRectToVisible: 方法实现了同样的事情只不过是作用在一个矩形上 的。 visibleRect 属性决定图层(如果存在的话)的哪部分是当前的可视区域。 如果你自己实现这些方法就会相对容易明白一点,但是 CAScrollLayer 帮你省了 这些麻烦,所以当涉及到实现图层滑动的时候就可以用上了。

CATiledLayer

有些时候你可能需要绘制一个很大的图片,常见的例子就是一个高像素的照片或者 是地球表面的详细地图。 iOS应用通常运行在内存受限的设备上,所以读取整个图 片到内存中是不明智的。载入大图可能会相当地慢,那些对你看上去比较方便的做 法(在主线程调用UIImage-imageNamed: 方法或者 - imageWithContentsOfFile:方法)将会阻塞你的用户界面,至少会引起动画卡顿现象。

能高效绘制在iOS上的图片也有一个大小限制。所有显示在屏幕上的图片最终都会 被转化为OpenGL纹理,同时OpenGL有一个最大的纹理尺寸(通常是2048*2048, 或4096*4096,这个取决于设备型号)。如果你想在单个纹理中显示一个比这大的 图,即便图片已经存在于内存中了,你仍然会遇到很大的性能问题,因为Core Animation强制用CPU处理图片而不是更快的GPU(见第12章『速度的曲调』,和 第13章『高效绘图』,它更加详细地解释了软件绘制和硬件绘制)。

CATiledLayer为载入大图造成的性能问题提供了一个解决方案:将大图分解成 小片然后将他们单独按需载入。让我们用实验来证明一下。

小片裁剪

这个示例中,我们将会从一个2048*2048分辨率的雪人图片入手。为了能够从 CATiledLayer 中获益,我们需要把这个图片裁切成许多小一些的图片。你可以通过代码来完成这件事情,但是如果你在运行时读入整个图片并裁切,那 CATiledLayer这些所有的性能优点就损失殆尽了。理想情况下来说,最好能 够逐个步骤来实现。

Demo演示了一个简单的Mac OS命令行程序,它用 CATiledLayer 将一个图片 裁剪成小图并存储到不同的文件中。

Retina小图

你也许已经注意到了这些小图并不是以Retina的分辨率显示的。为了以屏幕的原生分辨率来渲染 ,我们需要设置图层的 contentsScale 来匹配UIScreenscale属性:

 tileLayer.contentsScale = [UIScreen mainScreen].scale;

有趣的是, titleSize是以像素为单位,而不是点,所以增大了contentScale 就自动有了默认的小图尺寸(现在它是128*128的点而不是256*256).所以,我们不需要手工更新小图的尺寸或是在Retina分辨率下指定一个 不同的小图。我们需要做的是适应小图渲染代码以对应安排scale 的变化,然 而:

//determine tile coordinate
CGRect bounds = CGContextGetClipBoundingBox(ctx);
CGFloat scale = [UIScreen mainScreen].scale;
NSInteger x = floor(bounds.origin.x / layer.tileSize.width * scale);
NSInteger y = floor(bounds.origin.y / layer.tileSize.height * scale);

通过这个方法纠正 scale 也意味着我们的雪人图将以一半的大小渲染在Retina设备上(总尺寸是1024*1024,而不是2048*2048)。这个通常都不会影响到用CATiledLayer正常显示的图片类型(比如照片和地图,他们在设计上就是要 支持放大缩小,能够在不同的缩放条件下显示),但是也需要在心里明白。

CAEmitterLayer

iOS 5中,苹果引入了一个新的CALayer子类叫做 CAEmitterLayerCAEmitterLayer 是一个高性能的粒子引擎,被用来创建 实时例子动画如:烟雾,火,雨等等这些效果。

CAEmitterLayer看上去像是许多 CAEmitterCell的容器,这些 CAEmitterCell定义了一个粒子效果. 你将会为不同的粒子效果定义一个或多个CAEmitterCell作为模板,同时CAEmitterLayer负责基于这些模板实例化一个粒子流.一个CAEmitterCell类似一个CALayer:它有一个 contents属性可以定义为一个 CGImage,另外还有一些可设置属性控制着表 现和行为。我们不会对这些属性逐一进行详细的描述,你们可以在 CAEmitterCell 类的头文件中找到。

我们来举个例子。我们将利用在一圆中发射不同速度和透明度的粒子创建一个火爆 炸的效果。

#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];  
    //create particle emitter layer
    CAEmitterLayer *emitter = [CAEmitterLayer layer];
    emitter.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:emitter];
  //configure emitter
   emitter.renderMode = kCAEmitterLayerAdditive;
   emitter.emitterPosition = CGPointMake(emitter.frame.size.width,emitter.frame.size.height);

    //create a particle template
    CAEmitterCell *cell = [[CAEmitterCell alloc] init];
  cell.contents = (__bridge id)[UIImage imageNamed:@"Spark.png"];
    cell.birthRate = 150;
    cell.lifetime = 5.0;
cell.color = [UIColor colorWithRed:1 green:0.5 blue:0.1 alpha:3.0];
    cell.alphaSpeed = -0.4;
    cell.velocity = 50;
    cell.velocityRange = 50;
    cell.emissionRange = M_PI * 2.0;
    //add particle template to emitter
    emitter.emitterCells = @[cell];
}
@end

CAEMitterCell的属性基本上可以分为三种:

  • 这种粒子的某一属性的初始值。比如,color属性指定了一个可以混合图片 内容颜色的混合色。在示例中,我们将它设置为桔色。
  • 例子某一属性的变化范围。比如 emissionRange 属性的值是,这意味着例 子可以从360度任意位置反射出来。如果指定一个小一些的值,就可以创造出 一个圆锥形
  • 指定值在时间线上的变化。比如,在示例中,我们将 alphaSpeed 设置 为-0.4,就是说例子的透明度每过一秒就是减少0.4,这样就有发射出去之后逐渐小时的效果。

CAEmitterLayer的属性它自己控制着整个例子系统的位置和形状。一些属性比如 birthRate,lifetimecelocity,这些属性在CAEmitterCell 中也有。这些属性会以相乘的方式作用在一起,这样你就可以用一个值来加速或者扩大整个例子系统。其他值得提到的属性有以下这些:

  • preservesDepth ,是否将3D例子系统平面化到一个图层(默认值)或者可 以在3D空间中混合其他的图层
  • renderMode ,控制着在视觉上粒子图片是如何混合的。你可能已经注意到 了示例中我们把它设置为 kCAEmitterLayerAdditive它实现了这样一个效果:合并例子重叠部分的亮度使得看上去更亮。如果我们把它设置为默认的 kCAEmitterLayerUnordered,效果就没那么好看了
CAEAGLLayer

iOS要处理高性能图形绘制,必要时就是OpenGL。应该说它应该是最后的杀手 锏,至少对于非游戏的应用来说是的。因为相比Core AnimationUIkit框架,它不可思议地复杂。

OpenGL提供了Core Animation的基础,它是底层的C接口,直接和iPhoneiPad 的硬件通信,极少地抽象出来的方法。OpenGL没有对象或是图层的继承概念。它只是简单地处理三角形。OpenGL中所有东西都是3D空间中有颜色和纹理的三角形。用起来非常复杂和强大,但是用OpenGL绘制iOS用户界面就需要很多很多的 工作了。

为了能够以高性能使用Core Animation,你需要判断你需要绘制哪种内容(矢量图 形,粒子,文本,等等),然后选择合适的图层去呈现这些内容,Core Animation中只有一些类型的内容是被高度优化的;所以如果你想绘制的东西并不能找到标准 的图层类,想要得到高性能就比较费事情了。

因为OpenGL根本不会对你的内容进行假设,它能够绘制得相当快。利用 OpenGL,你可以绘制任何你知道必要的集合信息和形状逻辑的内容。所以很多游 戏都喜欢用OpenGL(这些情况下,Core Animation的限制就明显了:它优化过的 内容类型并不一定能满足需求),但是这样依赖,方便的高度抽象接口就没了。

iOS 5中,苹果引入了一个新的框架叫做GLKit,它去掉了一些设置OpenGL的复杂性,提供了一个叫做 CLKViewUIView的子类,帮你处理大部分的设置和绘制工作。前提是各种各样的OpenGL绘图缓冲的底层可配置项仍然需要你用 CAEAGLLayer 完成,它是 CALayer 的一个子类,用来显示任意的OpenGL图 形。

大部分情况下你都不需要手动设置CAEAGLLayer (假设用GLKView),过去的日 子就不要再提了。特别的,我们将设置一个OpenGL ES 2.0的上下文,它是现代的iOS设备的标准做法。

尽管不需要GLKit也可以做到这一切,但是GLKit囊括了很多额外的工作,比如设置顶点和片段着色器,这些都以类C语言叫做GLSL自包含在程序中,同时在运行时载 入到图形硬件中。编写GLSL代码和设置 EAGLayer 没有什么关系,所以我们将用 GLKBaseEffect 类将着色逻辑抽象出来。其他的事情,我们还是会有以往的方 式。

在一个真正的OpenGL应用中,我们可能会用 NSTimerCADisplayLink周期性地每秒钟调用-drawRrame方法60次,同时会将几何图形生成和绘制分开以便不会每次都重新生成三角形的顶点(这样也可以让我们绘制其他的一些东西而不是 一个三角形而已),不过上面这个例子已经足够演示了绘图原则了。

AVPlayerLayer

最后一个图层类型是 AVPlayerLayer 。尽管它不是Core Animation框架的一部分 (AV前缀看上去像), AVPlayerLayer 是有别的框架(AVFoundation)提供 的,它和Core Animation紧密地结合在一起,提供了一个 CALayer子类来显示自定义的内容类型。

AVPlayerLayer是用来在iOS上播放视频的。他是高级接口例如MPMoviePlayer的底层实现,提供了显示视频的底层控制。AVPlayerLayer的使用相当简单:你可以用 +playerLayerWithPlayer: 方法创建一个已经绑定了视频播放器的图层,或者你可以先创建一个图层,然后
player 属性绑定一个 AVPlayer 实例。

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //get video URL
    NSURL *URL =  [[NSBundle mainBundle]URLForResource:@"" withExtension:@""];
    //create player and player layer
    AVPlayer *player = [AVPlayer playerWithURL:URL];
    AVPlayerLayer *playerLayer =  [AVPlayerLayer playerLayerWithPlayer:player];
   //set player layer frame and attach it to our view
    playerLayer.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:playerLayer];
    //play the video
    [player play];
}

我们用代码创建了一个 AVPlayerLayer,但是我们仍然把它添加到了一个容器视 图中,而不是直接在controller中的主视图上添加。这样其实是为了可以使用自动布局限制使得图层在最中间;否则,一旦设备被旋转了我们就要手动重新放置位置, 因为Core Animation并不支持自动大小和自动布局。

当然,因为 AVPlayerLayerCALayer 的子类,它继承了父类的所有特性。我们并不会受限于要在一个矩形中播放视频;下面代码演示了在3D,圆角,有色边框,蒙板,阴影等效果.

- (void)viewDidLoad
{
    ...
    //set player layer frame and attach it to our view
    playerLayer.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:playerLayer];
    //transform layer
    CATransform3D transform = CATransform3DIdentity; transform.m34 = -1.0 / 500.0;
    transform = CATransform3DRotate(transform, M_PI_4, 1, 1, 0); playerLayer.transform = transform;
 
    //add rounded corners and border
    playerLayer.masksToBounds = YES;
    playerLayer.cornerRadius = 20.0;
    playerLayer.borderColor = [UIColor redColor].CGColor; playerLayer.borderWidth = 5.0;
    //play the video
    [player play];
}
3D视角下的边框和圆角 AVPlayerLayer.png
总结

这一章我们简要概述了一些专用图层以及用他们实现的一些效果,我们只是了解到 这些图层的皮毛,像CATiledLayerCAEMitterLayer 这些类可以单独写一章 的。但是,重点是记住 CALayer是用处很大的,而且它并没有为所有可能的场景 进行优化。为了获得Core Animation最好的性能,你需要为你的工作选对正确的工 具,希望你能够挖掘这些不同的 CALayer子类的功能。 这一章我们通过 CAEmitterLayerAVPlayerLayer类简单地接触到了一些动画,在下面,我们将继续深入研究动画,就从隐式动画开始。

iOS核心动画高级技巧--目录

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容