[关闭]
@zhangyuhangk 2015-08-09T08:29:36.000000Z 字数 14229 阅读 3534

Core Animation Advanced Techniques 学习笔记 (一)

CoreAnimation


教程:https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques

偶然发现了这个免费教程,正想深入了解一下Core Animation的说。挽起袖子这就开始了~~
(以下只记录一些零碎的知识点,只作为自己日后回顾的关键字。需要完整的教程还请访问上面的链接。)

先定义一些关键字:
* 图层 = CALayer
* 视图 = UIView

1. 图层树

每个UIView有一个CALayer, CALayer只负责显示
UIView负责响应事件(如tap),保存状态数据,再通过CALayer进行显示
一般情况下直接操作UIView就足够了,除非你需要:

2. 寄宿图

CALayer的contents相关属性:

CALayerDelegate

当CALayer需要一个内容特定的信息就会从CALayerDelegate中获取。实际上不存在一个叫CALayerDelegate的protocol。CALayer的delegate属性的类型是AnyObject?。UIView上的layer的delegate必须指向UIView本身。

当需要被重绘时,CALayer会请求它的代理给它一个寄宿图来显示。它通过调用下面这个方法做到的:

  1. func displayLayer(layer: CALayer)

趁着这个机会,如果代理想直接设置contents属性的话,它就可以这么做,不然没有别的方法可以调用。如果代理不实现-displayLayer:方法,CALayer就会转而尝试调用下面这个方法:

  1. func drawLayer(layer: CALayer, inContext ctx: CGContext)

在调用这个方法之前,CALayer创建了一个合适尺寸的空寄宿图(尺寸由bounds和contentsScale决定)一个Core
Graphics的绘制上下文环境,为绘制寄宿图做准备,它作为ctx参数传入。

3. 图层几何学

基本概念

CALayer的frame, bounds, postion属性对应了UIView的frame, bounds, center
frame是外部坐标,表示在父layer上占据的空间
bounds是内部坐标
position是layer的anchorPoint在父layer中的位置。

frame, bounds, position
frame是根据bounds、position和transform计算出来的

记住当对图层做变换的时候,比如旋转或者缩放,frame实际上代表了覆盖在图层旋转之后的整个轴对齐的矩形区域,也就是说frame的宽高可能和bounds的宽高不再一致了
rotate

计算layer的frame(不考虑transform的情况下):
1. 取layer的position,比如:(30, 30)
2. 取layer的bounds的size,比如:(100, 100)
3. 取layer的anchorPoint,比如:(0.25, 0.25),这个值说明宽有1/4在anchorPoint的左边,3/4在anchorPoint的右边。高也一样。
4. frame的值为:
frame.x = position.x - bounds.width * anchorPoint.x
frame.y = position.y - bounds.height * anchorPoint.y
frame.size = bounds.size
anchorPoint

绝大多数情况下,anchorPoint都是默认值(0.5, 0.5),可以直接把position理解为layer的中心在父layer坐标系里的位置。

坐标转换

  1. func convertPoint(point: CGPoint, fromLayer: CALayer) -> CGPoint
  2. func convertPoint(point: CGPoint, toLayer: CALayer) -> CGPoint
  3. func convertRect(rect: CGRect, fromLayer: CALayer) -> CGPoint
  4. func convertRect(rect: CGRect, toLayer: CALayer) -> CGPoint

geometryFlipped:为true时,整个坐标系会垂直翻转,原点变成左下角。
zPosition:Z轴位置,默认值为0,越大越靠近上面。
anchorPointZ:(没作介绍,但是想必是做3D变换的时候才会影响吧,回头试试)

Hit Testing

  1. func containsPoint(point: CGPoint) -> Bool // 判断点是不是在frame内
  2. func hitTest(point: CGPoint) -> CALayer? // 根据layer树的顺序,返回命中的layer。

注意当调用图层的-hitTest:方法时,测算的顺序严格依赖于图层树当中的图层顺序(和UIView处理事件类似)。之前提到的zPosition属性可以明显改变屏幕上图层的顺序,但不能改变事件传递的顺序。
这意味着如果改变了图层的z轴顺序,你会发现将不能够检测到最前方的视图点击事件,这是因为被另一个图层遮盖住了,虽然它的zPosition值较小,但是在图层树中的顺序靠前。

经代码测试证明,zPosition可以改变hitTest的顺序,不太理解上面说的“不能改变事件传递的顺序”的意思。如果UIView也是调用layer的hitTest做点击测试,那应该事件传递顺序应该也一致才对。(看到第五单再说吧)

auto layout

  1. func layoutSublayersOfLayer(layer: CALayer)

当图层的bounds发生改变,或者图层的setNeedsLayout方法被调用的时候,这个函数将会被执行。这使得你可以手动地重新摆放或者重新调整子图层的大小,但是不能像UIView的autoresizingMask和constraints属性做到自适应屏幕旋转。

视觉效果

变换

2D变换

CALayer的affineTransform属性(对应UIView的transform)是一个CGAffineTransform类型,用于作二维变换(如缩放、旋转、平移)。

CGAffineTransform中的“仿射”的意思是无论变换矩阵用什么值,图层中平行的两条线在变换之后仍然保持平行。

  1. CGAffineTransformMakeRotation(angle: CGFloat)
  2. CGAffineTransformMakeScale(sx: CGFloat, sy: CGFloat)
  3. CGAffineTransformMakeTranslation(tx: CGFloat, ty: CGFloat)
  1. CGAffineTransformRotate(t: CGAffineTransform, angle: CGFloat)
  2. CGAffineTransformScale(t: CGAffineTransform, sx: CGFloat, sy: CGFloat)
  3. CGAffineTransformTranslate(t: CGAffineTransform, tx: CGFloat, ty: CGFloat)
  1. CGAffineTransformConcat(t1: CGAffineTransform, t2: CGAffineTransform)
  1. func CGAffineTransformMakeShear(x: CGFloat, y: CGFloat) -> CGAffineTransform
  2. {
  3. var transform = CGAffineTransformIdentity;
  4. transform.c = -x;
  5. transform.b = y;
  6. return transform;
  7. }

3D变换

基本变换

CALayer的transform属性是一个CATransform3D类型,可以对layer作3D变换。
常用的几个创建CATransform3D的接口:

  1. func CATransform3DMakeRotation(angle: CGFloat, x: CGFloat, y: CGFloat, z: CGFloat)
  2. func CATransform3DMakeScale(sx: CGFloat, sy: CGFloat, sz: CGFloat)
  3. func CATransform3DMakeTranslation(tx: CGFloat, ty: CGFloat, tz: CGFloat)

投影变换(z变换)

以下代码表示绕y轴旋转45度(M_PI_4 = PI / 4)

  1. CATransform3DMakeRotation(M_PI_4, 0, 1, 0)

看起来像这样
y轴旋转
看着像是变窄了而已,这是因为没有作投影变换(或叫z变换)。修正方法也简单,只要在旋转前给transform.m34赋值(默认0)为-0.1/d。

d代表了想象中视角相机和屏幕之间的距离...通常500-1000就已经很好了。

  1. var transform = CATransform3DIdentity;
  2. transform.m34 = - 1.0 / 500.0;
  3. transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);

z变换

灭点

当在透视角度绘图的时候,远离相机视角的物体将会变小变远,当远离到一个极限距离,它们可能就缩成了一个点,于是所有的物体最后都汇聚消失在同一个点。

可以理解成当layer缩小到极限时,剩下的那个点的位置。Core Animation把它定义为变换前的anchorPoint。

当改变一个图层的position,你也改变了它的灭点,做3D变换的时候要时刻记住这一点,当你视图通过调整m34来让它更加有3D效果,应该首先把它放置于屏幕中央,然后通过平移来把它移动到指定位置(而不是直接改变它的position),这样所有的3D图层都共享一个灭点。

sublayerTransform

sublayerTransform属性可以指定所有sublayer都作同一个变换。可以理解为,实际显示时,每个sublayer都先执行这个变换,再执行自身的transform。
像上面的投影变换,就可以先指定父layer的sublayerTransform,这样每个sublayer就都得到投影效果

  1. var transform = CATransform3DIdentity;
  2. transform.m34 = - 1.0 / 500.0;
  3. containerLayer.sublayerTransform = transform

doubleSided: 表示是否双面渲染,就是翻面时是否仍可见。默认为true。(为啥默认为true呢?绝大部分情况下都不需要看到背面的吧)

注意:layer并不能构建复杂的3D场景,因为它的3D转换效果在计算后会渲染到寄宿图,最后其实显示的是一个平面(称为扁平化)。比如,建立两个嵌套的layer

  1. let outerLayer = CALayer()
  2. outerLayer.bounds = CGRect(x: 0, y: 0, width: 300, height: 300)
  3. outerLayer.position = view.center
  4. outerLayer.backgroundColor = UIColor.lightGrayColor().CGColor
  5. view.layer.addSublayer(outerLayer)
  6. let innerLayer = CALayer()
  7. innerLayer.frame = CGRect(x: 50, y: 50, width: 200, height: 200)
  8. innerLayer.backgroundColor = UIColor.yellowColor().CGColor
  9. outerLayer.addSublayer(innerLayer)

normal

innerLayer按y轴旋转45度。

  1. transform = CATransform3DIdentity
  2. transform.m34 = -1 / 500
  3. transform = CATransform3DRotate(transform, CGFloat(-M_PI_4), 0, 1, 0) // 绕y轴反向旋转45度
  4. innerLayer.transform = transform

outerLayer反向旋转了45度之后,innerLayer无法显示为正向的矩形了。

  1. var transform = CATransform3DIdentity
  2. transform.m34 = -1 / 500
  3. transform = CATransform3DRotate(transform, CGFloat(M_PI_4), 0, 1, 0)
  4. outerLayer.transform = transform

事件

  1. let layerWidth: CGFloat = 250
  2. func addViewWithName(name: String, color: UIColor, transform: CATransform3D) {
  3. let view = UIView(frame: CGRect(x: 0, y: 0, width: layerWidth, height: layerWidth))
  4. view.backgroundColor = color
  5. view.center = self.view.center
  6. view.layer.transform = transform
  7. let button = UIButton(frame: view.bounds.rectByInsetting(dx: 30, dy: 30))
  8. button.layer.cornerRadius = 5
  9. button.backgroundColor = UIColor.lightGrayColor()
  10. let title = NSMutableAttributedString(string: name, attributes: [NSFontAttributeName: UIFont.systemFontOfSize(100)])
  11. button.setAttributedTitle(title, forState: .Normal)
  12. button.setAttributedTitle(NSMutableAttributedString(string: name, attributes: [NSFontAttributeName: UIFont.systemFontOfSize(100)]), forState: .Normal)
  13. button.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "tapButton:"))
  14. view.addSubview(button)
  15. self.view.addSubview(view)
  16. }
  17. func tapButton(gesture: UITapGestureRecognizer) {
  18. let button = gesture.view as! UIButton
  19. print(button.titleLabel?.text)
  20. }
  21. func test3DLayers() {
  22. let z = layerWidth / 2
  23. var transform: CATransform3D
  24. transform = CATransform3DMakeTranslation(0, 0, z)
  25. addViewWithName("1", color: UIColor.yellowColor(), transform: transform)
  26. transform = CATransform3DMakeRotation(CGFloat(M_PI_2), 1, 0, 0)
  27. transform = CATransform3DTranslate(transform, 0, 0, z)
  28. addViewWithName("2", color: UIColor.greenColor(), transform: transform)
  29. transform = CATransform3DMakeRotation(CGFloat(-M_PI_2), 0, 1, 0)
  30. transform = CATransform3DTranslate(transform, 0, 0, z)
  31. addViewWithName("3", color: UIColor.blueColor(), transform: transform)
  32. var perspective = CATransform3DIdentity
  33. perspective.m34 = -1 / 500
  34. perspective = CATransform3DRotate(perspective, CGFloat(-M_PI_4), 1, 0, 0)
  35. perspective = CATransform3DRotate(perspective, CGFloat(M_PI_4), 0, 1, 0)
  36. view.layer.sublayerTransform = perspective
  37. }
  38. override func viewDidLoad() {
  39. super.viewDidLoad()
  40. test3DLayers()
  41. }

效果如下:
3D
点击各个button可以打印相应的数字(这一点跟教程上的不符……先这样吧……)

专用图层(CALayer的各种子类)

CAShapeLayer

用于绘制几何图形(能利用硬件加速,比用Core Graphics绘制快)。主要属性:

CATextLayer

用来显示文本的(包括富文本)。主要属性:

记得要设置contentsScale

  1. textLayer.contentsScale = UIScreen.mainScreen.scale

CATransformLayer

用来作变换的layer。本身是不能显示的,也不会把子layer扁平化。

CAGradientLayer

用于显示渐变的颜色。主要属性:

CAReplicatorLayer

用于显示多个有规律(指颜色、透明度、变换等有递进关系)的layer。比如:

  1. view.backgroundColor = UIColor.lightGrayColor()
  2. let layer = CAReplicatorLayer()
  3. view.layer.addSublayer(layer)
  4. layer.frame = view.bounds
  5. layer.instanceCount = 10
  6. layer.instanceTransform = CATransform3DMakeRotation(CGFloat(M_PI / 5), 0, 0, 1)
  7. layer.instanceBlueOffset = -0.1
  8. layer.instanceGreenOffset = -0.1
  9. let colorLayer = CALayer()
  10. layer.addSublayer(colorLayer)
  11. colorLayer.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
  12. colorLayer.position = layer.position
  13. colorLayer.position.y -= 200
  14. colorLayer.backgroundColor = UIColor.whiteColor().CGColor

此处输入图片的描述

主要属性:

可以利用CAReplicatorLayer来实现反射效果

  1. view.backgroundColor = UIColor.grayColor()
  2. let containerLayer = CAReplicatorLayer()
  3. view.layer.addSublayer(containerLayer)
  4. containerLayer.frame = view.bounds
  5. containerLayer.instanceCount = 2
  6. containerLayer.instanceAlphaOffset = -0.6
  7. containerLayer.instanceTransform = CATransform3DIdentity.rotate(angle: CGFloat(M_PI), x: 1, y: 0, z: 0)
  8. let textLayer = CATextLayer()
  9. containerLayer.addSublayer(textLayer)
  10. textLayer.bounds = CGRect(x: 0, y: 0, width: 300, height: 50)
  11. textLayer.position = view.center
  12. textLayer.position.y -= 30
  13. textLayer.string = "Hello world!"
  14. textLayer.alignmentMode = kCAAlignmentCenter
  15. textLayer.font = CGFontCreateWithFontName(UIFont.systemFontOfSize(40).fontName)
  16. textLayer.fontSize = 40
  17. textLayer.foregroundColor = UIColor.redColor().CGColor
  18. textLayer.backgroundColor = UIColor.whiteColor().CGColor

此处输入图片的描述

CAScrollLayer

可以滚动的layer。默认装maskToBounds设为true。调用scrollToPoint,layer会将指定的点滚动到左上角,成为bounds.orginal。下面的的代码可以滚动一个渐变的颜色块。

  1. lazy var colorScrollLayer: CAScrollLayer = {
  2. let layer = CAScrollLayer()
  3. layer.frame = self.view.bounds.rectByInsetting(dx: 100, dy: 200)
  4. let colorLayer = CAGradientLayer()
  5. layer.addSublayer(colorLayer)
  6. colorLayer.frame = self.view.bounds
  7. colorLayer.startPoint = CGPoint(x: 0, y: 0)
  8. colorLayer.endPoint = CGPoint(x: 1, y: 1)
  9. colorLayer.colors = [UIColor.yellowColor(), .greenColor(), .blueColor(), .orangeColor(), .purpleColor()].map{ $0.CGColor }
  10. return layer
  11. }()
  12. func testScrollLayer() {
  13. view.layer.addSublayer(colorScrollLayer)
  14. view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: "pan:"))
  15. }
  16. func pan(gesture: UIPanGestureRecognizer) {
  17. let offset = gesture.translationInView(view)
  18. print(colorScrollLayer.bounds.origin)
  19. var position = colorScrollLayer.bounds.origin
  20. position.x -= offset.x
  21. position.y -= offset.y
  22. colorScrollLayer.scrollPoint(position)
  23. gesture.setTranslation(CGPoint.zeroPoint, inView: view)
  24. }

CATiledLayer

用于优化大图的显示性能。比如一个2048*2048的地图,一次性载入内容就很卡,而且也没必要。只载入和显示当前窗口需要显示的部分即可。所以要将这个大图裁成许多小图。指定CATiledLayer的delegate,在其drawLayer:inContext方法中绘制需要的小图就行了。(感觉没什么机会用到,先放着吧)

CAEmitterLayer

实现粒子效果(如下图)。估计也不多用,先放着)
此处输入图片的描述

CAEAGLLayer

简单地讲,就是可以使用OpenGL绘制任何图形的layer。(太复杂,先放着)

AVPlayerLayer

用于视频播放。还没试过,姑且把教程里的代码贴进来。

  1. //get video URL
  2. NSURL *URL = [[NSBundle mainBundle] URLForResource:@"Ship" withExtension:@"mp4"];
  3. //create player and player layer
  4. AVPlayer *player = [AVPlayer playerWithURL:URL];
  5. AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
  6. //set player layer frame and attach it to our view
  7. playerLayer.frame = self.containerView.bounds;
  8. [self.containerView.layer addSublayer:playerLayer];
  9. //play the video
  10. [player play];
  11. playerLayer作各种transform也是可以播放滴。
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注