iOS Core Animation - The Layer Beneath

前言

iOS Core Animation 读书笔记(一)The Layer Beneath

笔记

The Layer Tree

  1. Layer Kit: 高效的,硬件加速的混合框架, 用来替代以Quartz为基础的,相对低效的AppKit框架。它也就是Core Animation的基础。
  2. Core Animation 不仅仅只和动画有关,它是iOS的核心组件之一,你在屏幕上看到的所有东西都由它来power。
  3. Core Animation的主要工作是将各个可视化的layer显示在屏幕上面。也就是将layer tree显示出来。
  4. UIView handles touch events and supports Core Graphics-based drawing, affine transform, and simple animations such as sliding and fading.
  5. 但是UIView并不是亲自处理这些任务中的大部分。渲染,布局,动画…这些任务其实都是由Core Animation里面的CALayer来完成的。
  6. CALayer不负责用户交互(user interaction),和responder chain没有联系。
  7. 每一个UIView都有一个layer属性,也就是一个CALayer。实际上,UIView并没有做什么事情,关于在屏幕上的显示和动画基本上都是由CALayer来完成。UIView可以说只是一个CALayer的wrapper,负责一些手势处理,以及一些高层面的对于Core Animation的一些API的封装。
  8. 总结来说:UIView主要负责处理事件以及用户交互,而CALayer主要负责显示和动画。

The Backing Image

The Contents Image

  1. CALayer可以包含一个图片;图片里面,你想显示啥就显示啥。这就是CALayer的backing image

  2. CALayer有一个contents属性,给这个contents属性赋值入锅不是CGImage的话,这个Layer还是什么都没有显示。要这般赋值:

    layer.contents = (__bridge id)image.CGImage;
    

    因为,要真正赋值给contents属性的应该是一个CGImageRef,它是一个指针,指向CGImage的结构体;而UIImage的CGImage属性就是一个CGImageRef,但由于不是一个OC对象,无法赋值给id,所以需要使用(__bridge id)来转换。

  3. CALyer的contentsGravity属性类似于UIView的contentMode。

  4. CALayer的contentsScale属性与Retina有关。(有点不懂)

  5. contentsRect属性将backing image限制在当前CALayer矩形的一个子框框里面。它不是按点来计算的;按照统一的标尺,这个Rect的长和宽最小为0,最大为1。

  6. iOS中用到3中坐标类型:

    1. Points:最常用的坐标类型,point表示逻辑像素。在标准清晰度的设备中一个point代表一个pixel;在Retina设备中,一个point代表2x2个pixel。

    2. Pixels: 不常用,一般和使用pixel作为度量的CGImage等一同使用。

    3. Unit:就是contentsRect属性用到的坐标系。

      example: todo 插入例子

  1. contentsCenter:该属性是一个CGRect,它定义了一个可伸缩的区域。具体的见下图:(有点不清楚)

    example: todo 插入图片

    其中四个黄色区域组合起来是原图。黄色区域内部四个点其实原来是一个点,也就是原图的中心点。

Custom Drawing

除了将一个CGImage指派给一个CALayer的contents以外,还可以直接在CALayer的backing image上面绘图。先来看看整个CALayer生成backing image的过程。

  1. 有一个CALayerDelegate的协议,当CALayer需要对它的content进行填充时,会去请求该协议的方法。

  2. CALayer先会请求下列方法:

      - (void)displayLayer:(CALayer *)layer;
    

    在这个方法里面可以直接设置CALayer的contents属性。如果实现了这个方法,那么CALayer不会再去请求其他的方法来填充它的内容。

  3. 如果上一步未实现该方法,CALayer会去请求下面的方法:

      - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
    

    在请求这个方法之前,CALayer会创建一个空白的backing image,和一个匹配该backing image的context。然后调用这个方法进行绘图。

  4. 只有在调用了[layer display];方法之后,上面说的这些delegate才会被调用。

所以总结一下这个过程,首先可以直接对CALayer的contents进行设置;然后如果发现调用了[layer display];方法,会调用- (void)displayLayer:(CALayer *)layer;方法,在这个方法里面可以直接设置contents,如果没有设置就沿用之前的contents;但是如果这个方法没有实现的话,display方法会创建一个空白的backing image来取代之前设置的contents,然后再调用- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;方法。在这个方法里面进行绘图。

PS:CALayer的backgroundColor和contents没有一点关系,该怎么设置就怎么设置。

PS:我们一般是不需要去调用或者实现上面说的所有方法的,如果我们通过直接设置UIView或者是CALayer的属性无法达到我们的目的的时候,我们需要做的是重写UIView的-drawRect:方法。这个方法会帮我们在暗中处理上面说到的各种方法。

再再PS:如果不需要调用-drawRect:方法,那么久不要去重写它;因为即使是一个空的-drawRect:方法,程序也会花内存和CPU时间去创建一个新的,空的backing image。

Layer Geometry

Layout

  1. 对于设置UIView的framebounds以及center,其实就是调用对应CALayer的setter和getter函数,对CALayer的相对应的属性进行设置。
  2. CALayer对应的属性是:frame, bounds, positionsanchorPoint。其中frame和bounds好理解。position的值是父layer的坐标系中的,它表示layer的旋转中心。anchorPoint的值表示position的那个点在自己layer的bounds上的比例。比如position是中点,那么anchorPoint就是(0.5, 0.5);如果position在左上角,那么anchorPoint就是(0, 0)。
  3. 在iOS中,坐标系原点在左上角;在Mac OS中,坐标系原点在左下角。通过设置CALayer的geometryFlipped属性,可以改变坐标系从左上变到左下。只需将geometryFlipped设置为YES。
  4. UIView是严格的二维平面;但是,CALayer存在三维空间。CALayer有zPositionanchorPointZ属性。除了用在CATransform3D外,比较有用的就是改变layer的现实顺序,zPosition越大,越靠前显示。

Hit Testing

  1. CALayer不知道responder chain的存在,不能直接处理用户的交互动作。只有几个函数和用户交互有关。比如-hitTest:
  2. -hitTest: 返回CGPoint的点在哪个CALayer。

Automatic Layout

  1. 你可以通过UIVIew的UIViewAutoresizingMask和NSLayoutConstraint相关的API来使用autolayout。但是,如果你想直接通过操作CALayer来使用autolayout,最简单的方法就是使用CALayerDelegate的- (void)layoutSublayersOfLayer:(CALayer *)layer;方法。但是它不好用,所以最好是直接操作UIView…
  2. 上面是一个佐证:你最好使用带layer的view来组织你的界面,而不是使用单独的layer。

Visual Effects

  1. cornerRadius默认的只会影响background color,不对backing image产生影响。但是加上masksToBounds之后,layer就确保了又圆角的属性。
  2. CALayer的border是被花在CALayer的内部的,在任何的sublayer的上面。
  3. CALayer的shadow不同于border的是:它是基于backing image的边缘的,而不是bounds的边缘。有点厉害
  4. CALayer的shadow通常是画在bounds的外部的,所以会被masksToBounds给切掉。。。
  5. CALayer的shadowPath属性可以强制指定一个shadow的形状。该属性的值是一个CGPathRef。可以使用UIBezierPath。
  6. CALayer的mask属性也是一个CALayer。但是它的作用是定义这个父layer的可见的部分。这个mask layer最重要的就是它的轮廓(和上面的shadow的边缘有点类似),它的父layer只会显示轮廓内的部分。
  7. CALayer的minificationFiltermagnificationFilter属性用于指定图片放大缩小时图片的显示方式。专业一点说就是生成图片像素的算法。他们有3个选项:kCAFilterLinear,kCAFilterNearest和kCAFilterTrilinerar。其中,nearest适合用于小图片以及图片颜色变化很尖锐的;而其他两个用于图片颜色变化比较缓和的,也就是均匀的,不会两个相邻像素之间颜色发生突变。

Transforms

Affine Transforms

  1. 利用CFAffineTransform的时候,其实可以计算原来view的四个点对应的变化之后的四个点,来确定变化之后的view的形状以及位置。

  2. UIView的transform属性是一个CGAffineTransform,CALayer的transform属性是一个CATransform3D。CALayer中和UIView的transform等同的一个属性叫做affineTransform。其实对UIView的transform属性进行操作,实际上也是对CALayer进行操作,它只是一个向上的包装而已。

  3. 通过如下代码:

    CGAffineTransform transform = CGAffineTransformIdentity;
    transform = CGAffineTransformScale(transform, 0.5, 0.5);
    transform = CGAffineTransformRotate(transform, M_PI / 180 * 30);
    transform = CGAffineTransformTranslate(transform, 200, 0);
    

    记住,这里最后一步向右平移200点,其实并没有平移200点。因为,这样连续的变化,向右平移200点的transform受到了之前两步transform的影响。它其实被scale了一半,然后被旋转了30度。可以想象成向右200的向量吧(不确定)。

  4. 还有一种变换无法用translate, rotate和scale来表示。那就是:原View是矩形,变换之后了View变成了平行四边形。有点类似投影。那样的话就得直接修改CGAffineTransform矩阵中的值了。如下:

    CGAffineTransform transform = CGAffineTransformIdentity;
    transform.c = -x;
    transform.b = y;
    

3D Transforms

  1. Core Graphics framework是一个2维的画图的库。而CALayer的transform属性是一个CATransform3D,3D的。所以需要用到Core Animation framework。
  2. 控制透视效果,也就是远处的view看上去比近处的小。这个效果可以通过控制CATransform3D的m34值来实现。m34用来表示X,Y轴的放大系数与view距离camera的类似比值的一个东西。m34默认为0,将它定义成 -1.0/d,d表示view到镜头的距离,取500-1000之间的值都可以。d取值越小,透视的效果越明显。todo 来两张图
  3. 3D最远端的消失点定义在layer的anchorPoint。
  4. CALayer的sublayerTransform属性可以为sublayer统一指定他们的perspective和vanishing point。
  5. 沿Y轴旋转180度,就把layer翻转过来了,呈现镜像。但是如果将CALayer的doubleSided设置成NO,那么反面就不会被draw出来。
  6. 对于外部的layer外转,内部的layer内转,得分情况来讨论。如果是按照Z轴来旋转,那么内部的layer旋转被抵消,正面显示。但是如果按照Y轴旋转,虽然内部的旋转也被抵消,但是显示在屏幕上的其实是内部layer在外部layer上的投影;此时外部layer已经倾斜了,虽然内部layer很正,但是显示投影上去还是倾斜的。todo 来张图
  7. CATransform3D 先位移再旋转和先旋转再位移是不一样的。要始终找到旋转以及平移的中心点,以该中心点建立三维坐标系。当旋转或平移时,该坐标系也会跟着旋转和平移,那么下一步的选装或平移要以当前最新的坐标系为标准。
  8. UIView的touch event接收顺序与zPosition无关,还是与添加UIView的顺序相关。

Specialized Layers

CAShapeLayer

  1. CAShapeLayer使用矢量画图,而不是位图(bitmap image)。

  2. 相比与普通的CALayer,它更快:硬件加速;内存更高效:不需要创建backing image;不会被边界限制(backing image的context);放大缩小时不是像素化的:矢量图。

  3. 创建CAShapeLayer的CGPath时只能创建一次,不能分开多次创建。也就是说CGPath的路径,颜色,线段粗细等属性只能在创建时一次性全部设置。如果想画多个不同类型(比如:不同颜色,不同粗细等)的shape,那么就得创建多个layer。

  4. CAShapeLayer的path属性是一个CGPath,但是使用UIBezierPath,可以简化我们的使用,不用手动去释放CGPath。

  5. 创建一个只有3个角的bezierPath:

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

    可以把这个CGPath构成的CAShapeLayer当做某个CALayer的mask。

CATextLayer

  1. CATextLayer提供了UILabel的绝大部分功能,以及一些自己的功能,并且比UILabel渲染的更快。

  2. CATextLayer要注意提供contentsScale属性,以保证在各种屏幕上显示正确。

  3. 它的font属性是一个CFTypeRef,所以可以给他CTFontRef或者是CGFontRef;而且这两个和UIFont一样不会压缩point size,也就是字体大小;所以还需要自己指定fontSize属性。

  4. CATextLayer在iOS3.2的时候就已经支持attributed string了,比UILabel等UIKit元素更早。

  5. 但是它的行间距和字间距比UILabel的表现要差。

  6. CALayer不太支持autolayout和autoresizing…

  7. 如何让一个UIView的root layer不是普通的CALayer呢?UIView通过+ (Class)layerClass;方法来返回root layer的类型;所以只需要在我们自己的View类里面重写该方法就可以。

    @implementation LayerLabel  // 自定义的UIView
    + (Class)layerClass {  // override layerClass of UIView
          return [CATextLayer class];
    }
    

    那么这时就不会调用-drawRect:方法来绘图,而是使用CATextLayer自己来渲染文字。将CATextLayer作为UILabel的root layer还有个好处,就是不需要设置contentsScale。因为这个被UIView自己设置了。这也佐证了使用带layer的View比单独使用layer要方便。

CATransformLayer

  1. 它的存在解决了之前说的外部layer外转内部layer内转,但是内部layer显示奇怪的问题。这个问题的根本原因就是所有sublayer都被containerLayer给扁平化得显示在它自己的空间内,有点剥夺3d效果的感觉。

  2. CATransformLayer不能用于显示任何内容,它必须包含一些sublayer。它有可以应用于sublayer的transform,可以使sublayer立体化起来。

  3. 这个例子很赞:

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

    todo add picture

CAGradientLayer

  1. 测试了重写View的layerClass方法之后发现,对于CATextLayer,不会调用-drawRect;方法,但是对于其他种类的layer,还是会调用-drawRect;方法。
  2. 硬件加速。
  3. colors属性是一个数组。里面装的是CGColorRef,不是UIColor。所以还是需要使用(__bridge id)startPointendPoint指定渐变方向,这两个属性使用的是unit coordinates
  4. colors属性可以添加任意个颜色,默认的他们是均匀分布的;但是我们可以通过locations属性改变他们的分布。locations属性是一个装着NSNumber(float value)的数组,从0-1,表示各种color的终区段。

CAReplicatorLayer

  1. 它是用来高效的生成一系列相似的layer。

  2. instanceCount属性指定copy的个数,instanceTransform属性指定变化,各个变化是叠加的。

  3. 说不清楚,直接上代码…

    @interface ViewController ()
    @property (nonatomic, weak) IBOutlet UIView *containerView; 
    @end
    
    @implementation ViewController
    - (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];
    }
    @end
    

    todo add picture

CAScrollLayer

  1. 一个可以scroll的layer,有一个方法-scrollToPoint:。因为CALayer无法处理用户手势操作,所以这个方法也是这个layer所能做的所有事了。。。可以从UIView的层面获取手势所处的point,再将该point传递给CAScrollLayer。
  2. 该layer滑动的时候,改变的是layer的bounds。

OtherLayer

  1. Core Animation使用CPU来处理图片,而不是更快的GPU。

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器