@zhangyuhangk
2015-08-09T08:29:36.000000Z
字数 14229
阅读 3534
CoreAnimation
教程:https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques
偶然发现了这个免费教程,正想深入了解一下Core Animation的说。挽起袖子这就开始了~~
(以下只记录一些零碎的知识点,只作为自己日后回顾的关键字。需要完整的教程还请访问上面的链接。)
先定义一些关键字:
* 图层 = CALayer
* 视图 = UIView
每个UIView有一个CALayer, CALayer只负责显示
UIView负责响应事件(如tap),保存状态数据,再通过CALayer进行显示
一般情况下直接操作UIView就足够了,除非你需要:
kCAGravityResize
:填充整个bounds,不保持原始比例。 kCAGravityResizeAspect
:保持图像比例,并且显示完整图案(有可能会有白边)kCAGravityResizeAspectFill
:保持图像比例,并且填满所有空间(有可能部分会被裁剪掉,如果masksToBounds为true的话)kCAGravityResizeAspect
或kCAGravityResizeAspectFill
时,contentsScale属性被忽略,因为这两个属性是根据当前layer的大小来决定contents的大小,而不是根据分辨率。当CALayer需要一个内容特定的信息就会从CALayerDelegate中获取。实际上不存在一个叫CALayerDelegate的protocol。CALayer的delegate属性的类型是AnyObject?
。UIView上的layer的delegate必须指向UIView本身。
当需要被重绘时,CALayer会请求它的代理给它一个寄宿图来显示。它通过调用下面这个方法做到的:
func displayLayer(layer: CALayer)
趁着这个机会,如果代理想直接设置contents属性的话,它就可以这么做,不然没有别的方法可以调用。如果代理不实现-displayLayer:方法,CALayer就会转而尝试调用下面这个方法:
func drawLayer(layer: CALayer, inContext ctx: CGContext)
在调用这个方法之前,CALayer创建了一个合适尺寸的空寄宿图(尺寸由bounds和contentsScale决定)一个Core
Graphics的绘制上下文环境,为绘制寄宿图做准备,它作为ctx参数传入。
CALayer的frame, bounds, postion属性对应了UIView的frame, bounds, center
frame是外部坐标,表示在父layer上占据的空间
bounds是内部坐标
position是layer的anchorPoint在父layer中的位置。
frame是根据bounds、position和transform计算出来的
记住当对图层做变换的时候,比如旋转或者缩放,frame实际上代表了覆盖在图层旋转之后的整个轴对齐的矩形区域,也就是说frame的宽高可能和bounds的宽高不再一致了
计算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都是默认值(0.5, 0.5),可以直接把position理解为layer的中心在父layer坐标系里的位置。
func convertPoint(point: CGPoint, fromLayer: CALayer) -> CGPoint
func convertPoint(point: CGPoint, toLayer: CALayer) -> CGPoint
func convertRect(rect: CGRect, fromLayer: CALayer) -> CGPoint
func convertRect(rect: CGRect, toLayer: CALayer) -> CGPoint
geometryFlipped
:为true时,整个坐标系会垂直翻转,原点变成左下角。
zPosition
:Z轴位置,默认值为0,越大越靠近上面。
anchorPointZ
:(没作介绍,但是想必是做3D变换的时候才会影响吧,回头试试)
func containsPoint(point: CGPoint) -> Bool // 判断点是不是在frame内
func hitTest(point: CGPoint) -> CALayer? // 根据layer树的顺序,返回命中的layer。
注意当调用图层的-hitTest:方法时,测算的顺序严格依赖于图层树当中的图层顺序(和UIView处理事件类似)。之前提到的zPosition属性可以明显改变屏幕上图层的顺序,但不能改变事件传递的顺序。
这意味着如果改变了图层的z轴顺序,你会发现将不能够检测到最前方的视图点击事件,这是因为被另一个图层遮盖住了,虽然它的zPosition值较小,但是在图层树中的顺序靠前。
经代码测试证明,zPosition可以改变hitTest的顺序,不太理解上面说的“不能改变事件传递的顺序”
的意思。如果UIView也是调用layer的hitTest做点击测试,那应该事件传递顺序应该也一致才对。(看到第五单再说吧)
func layoutSublayersOfLayer(layer: CALayer)
当图层的bounds发生改变,或者图层的setNeedsLayout方法被调用的时候,这个函数将会被执行。这使得你可以手动地重新摆放或者重新调整子图层的大小,但是不能像UIView的autoresizingMask和constraints属性做到自适应屏幕旋转。
cornerRadius
: 圆角半径borderWidth
: 边框宽度(圆角时,边框也是圆角。边框位于所有子layer之上)borderColor
: 边框颜色shadowOpacity
: 阴影透明度,取值0~1之间,默认为0,就是没阴影。shadowOffset
: 阴影偏移,默认为(0, -3)shadowRadius
: 阴影模糊半径,值越大阴影边缘越模糊shadowPath
: 指定阴影的形状。如果没有设置这个属性,则系统根据内容自动计算阴影形状(这就比较慢了)。mask
: 遮罩(蒙板)layer。对于mask layer来讲,只有轮廓是有用的,颜色什么的都没用,因为它本身不显示。设置mask属性后,layer只会显示与mask layer重叠部分。minificationFilter
和magnificationFilter
: 缩小和放大的算法。有3个选项:
总的来说,对于比较小的图或者是差异特别明显,极少斜线的大图,最近过滤算法会保留这种差异明显的特质以呈现更好的结果。但是对于大多数的图尤其是有很多斜线或是曲线轮廓的图片来说,最近过滤算法会导致更差的结果。换句话说,线性过滤保留了形状,最近过滤则保留了像素的差异。
默认值是kCAFilterLinear。具体效果可以用时再试。
opacity
: 透明度(对应UIView的alpha)。
这是由透明度的混合叠加造成的,当你显示一个50%透明度的图层时,图层的每个像素都会一般显示自己的颜色,另一半显示图层下面的颜色。这是正常的透明度的表现。但是如果图层包含一个同样显示50%透明的子图层时,你所看到的视图,50%来自子视图,25%来了图层本身的颜色,另外的25%则来自背景色。
在我们的示例中,按钮和表情都是白色背景。虽然他们都是50%的可见度,但是合起来的可见度是75%,所以标签所在的区域看上去就没有周围的部分那么透明。所以看上去子视图就高亮了,使得这个显示效果都糟透了。
其实没完全看懂上面这段话,不过大概的意思就是直接设置opacity,会由于子view和父view的透明度不一致,导致子视图看起来没那么透明(我尽力了……)。
解决的办法就是让layer在显示时先把子view的内容光栅化到寄宿图内,再一起应用半透明算法。只要指定shouldRasterize = true和rasterizationScale = UIScreen.mainScreen.scale就可以了。
(这个教程是iOS6时代写的,不知道现在是不是还是这样。从我在iOS9上测试的结果看来,貌似直接设置opacity就妥妥的了。姑且记在这里吧。)
CALayer的affineTransform属性(对应UIView的transform)是一个CGAffineTransform类型,用于作二维变换(如缩放、旋转、平移)。
CGAffineTransform中的“仿射”的意思是无论变换矩阵用什么值,图层中平行的两条线在变换之后仍然保持平行。
CGAffineTransformMakeRotation(angle: CGFloat)
CGAffineTransformMakeScale(sx: CGFloat, sy: CGFloat)
CGAffineTransformMakeTranslation(tx: CGFloat, ty: CGFloat)
CGAffineTransformRotate(t: CGAffineTransform, angle: CGFloat)
CGAffineTransformScale(t: CGAffineTransform, sx: CGFloat, sy: CGFloat)
CGAffineTransformTranslate(t: CGAffineTransform, tx: CGFloat, ty: CGFloat)
CGAffineTransformConcat(t1: CGAffineTransform, t2: CGAffineTransform)
func CGAffineTransformMakeShear(x: CGFloat, y: CGFloat) -> CGAffineTransform
{
var transform = CGAffineTransformIdentity;
transform.c = -x;
transform.b = y;
return transform;
}
CALayer的transform属性是一个CATransform3D类型,可以对layer作3D变换。
常用的几个创建CATransform3D的接口:
func CATransform3DMakeRotation(angle: CGFloat, x: CGFloat, y: CGFloat, z: CGFloat)
func CATransform3DMakeScale(sx: CGFloat, sy: CGFloat, sz: CGFloat)
func CATransform3DMakeTranslation(tx: CGFloat, ty: CGFloat, tz: CGFloat)
以下代码表示绕y轴旋转45度(M_PI_4 = PI / 4)
CATransform3DMakeRotation(M_PI_4, 0, 1, 0)
看起来像这样
看着像是变窄了而已,这是因为没有作投影变换(或叫z变换)。修正方法也简单,只要在旋转前给transform.m34赋值(默认0)为-0.1/d。
d代表了想象中视角相机和屏幕之间的距离...通常500-1000就已经很好了。
var transform = CATransform3DIdentity;
transform.m34 = - 1.0 / 500.0;
transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
当在透视角度绘图的时候,远离相机视角的物体将会变小变远,当远离到一个极限距离,它们可能就缩成了一个点,于是所有的物体最后都汇聚消失在同一个点。
可以理解成当layer缩小到极限时,剩下的那个点的位置。Core Animation把它定义为变换前的anchorPoint。
当改变一个图层的position,你也改变了它的灭点,做3D变换的时候要时刻记住这一点,当你视图通过调整m34来让它更加有3D效果,应该首先把它放置于屏幕中央,然后通过平移来把它移动到指定位置(而不是直接改变它的position),这样所有的3D图层都共享一个灭点。
sublayerTransform属性可以指定所有sublayer都作同一个变换。可以理解为,实际显示时,每个sublayer都先执行这个变换,再执行自身的transform。
像上面的投影变换,就可以先指定父layer的sublayerTransform,这样每个sublayer就都得到投影效果
var transform = CATransform3DIdentity;
transform.m34 = - 1.0 / 500.0;
containerLayer.sublayerTransform = transform
doubleSided
: 表示是否双面渲染,就是翻面时是否仍可见。默认为true。(为啥默认为true呢?绝大部分情况下都不需要看到背面的吧)
注意:layer并不能构建复杂的3D场景,因为它的3D转换效果在计算后会渲染到寄宿图,最后其实显示的是一个平面(称为扁平化)。比如,建立两个嵌套的layer
let outerLayer = CALayer()
outerLayer.bounds = CGRect(x: 0, y: 0, width: 300, height: 300)
outerLayer.position = view.center
outerLayer.backgroundColor = UIColor.lightGrayColor().CGColor
view.layer.addSublayer(outerLayer)
let innerLayer = CALayer()
innerLayer.frame = CGRect(x: 50, y: 50, width: 200, height: 200)
innerLayer.backgroundColor = UIColor.yellowColor().CGColor
outerLayer.addSublayer(innerLayer)
innerLayer按y轴旋转45度。
transform = CATransform3DIdentity
transform.m34 = -1 / 500
transform = CATransform3DRotate(transform, CGFloat(-M_PI_4), 0, 1, 0) // 绕y轴反向旋转45度
innerLayer.transform = transform
outerLayer反向旋转了45度之后,innerLayer无法显示为正向的矩形了。
var transform = CATransform3DIdentity
transform.m34 = -1 / 500
transform = CATransform3DRotate(transform, CGFloat(M_PI_4), 0, 1, 0)
outerLayer.transform = transform
let layerWidth: CGFloat = 250
func addViewWithName(name: String, color: UIColor, transform: CATransform3D) {
let view = UIView(frame: CGRect(x: 0, y: 0, width: layerWidth, height: layerWidth))
view.backgroundColor = color
view.center = self.view.center
view.layer.transform = transform
let button = UIButton(frame: view.bounds.rectByInsetting(dx: 30, dy: 30))
button.layer.cornerRadius = 5
button.backgroundColor = UIColor.lightGrayColor()
let title = NSMutableAttributedString(string: name, attributes: [NSFontAttributeName: UIFont.systemFontOfSize(100)])
button.setAttributedTitle(title, forState: .Normal)
button.setAttributedTitle(NSMutableAttributedString(string: name, attributes: [NSFontAttributeName: UIFont.systemFontOfSize(100)]), forState: .Normal)
button.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "tapButton:"))
view.addSubview(button)
self.view.addSubview(view)
}
func tapButton(gesture: UITapGestureRecognizer) {
let button = gesture.view as! UIButton
print(button.titleLabel?.text)
}
func test3DLayers() {
let z = layerWidth / 2
var transform: CATransform3D
transform = CATransform3DMakeTranslation(0, 0, z)
addViewWithName("1", color: UIColor.yellowColor(), transform: transform)
transform = CATransform3DMakeRotation(CGFloat(M_PI_2), 1, 0, 0)
transform = CATransform3DTranslate(transform, 0, 0, z)
addViewWithName("2", color: UIColor.greenColor(), transform: transform)
transform = CATransform3DMakeRotation(CGFloat(-M_PI_2), 0, 1, 0)
transform = CATransform3DTranslate(transform, 0, 0, z)
addViewWithName("3", color: UIColor.blueColor(), transform: transform)
var perspective = CATransform3DIdentity
perspective.m34 = -1 / 500
perspective = CATransform3DRotate(perspective, CGFloat(-M_PI_4), 1, 0, 0)
perspective = CATransform3DRotate(perspective, CGFloat(M_PI_4), 0, 1, 0)
view.layer.sublayerTransform = perspective
}
override func viewDidLoad() {
super.viewDidLoad()
test3DLayers()
}
效果如下:
点击各个button可以打印相应的数字(这一点跟教程上的不符……先这样吧……)
用于绘制几何图形(能利用硬件加速,比用Core Graphics绘制快)。主要属性:
path
: 指定图形strokeColor
和fillColor
: 指定线条和填充颜色。lineWidth
: 线条宽度lineJoin
: 线条间结合点的样子lineCap
: 线条结尾的样子 用来显示文本的(包括富文本)。主要属性:
foregroundColor
: 文本颜色alignmentMode
: 对齐方式wrapped
: 是否自动换行font
和fontSize
: 字体及大小string
: 文本内容(只能是String或者NSAttributedString)记得要设置contentsScale
textLayer.contentsScale = UIScreen.mainScreen.scale
用来作变换的layer。本身是不能显示的,也不会把子layer扁平化。
用于显示渐变的颜色。主要属性:
startPoint
和endPoint
: 渐变的起点和终点。CGPoint类型,x和y的取值都在0和1之间。用于显示多个有规律(指颜色、透明度、变换等有递进关系)的layer。比如:
view.backgroundColor = UIColor.lightGrayColor()
let layer = CAReplicatorLayer()
view.layer.addSublayer(layer)
layer.frame = view.bounds
layer.instanceCount = 10
layer.instanceTransform = CATransform3DMakeRotation(CGFloat(M_PI / 5), 0, 0, 1)
layer.instanceBlueOffset = -0.1
layer.instanceGreenOffset = -0.1
let colorLayer = CALayer()
layer.addSublayer(colorLayer)
colorLayer.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
colorLayer.position = layer.position
colorLayer.position.y -= 200
colorLayer.backgroundColor = UIColor.whiteColor().CGColor
主要属性:
instanceCount
: 要重复的实例数量instanceTransform
: 递进的实例变换。每个实例在前一个实例的基础上作这个变换。instanceRedOffset
、instanceGreenOffset
和instanceBlueOffset
: 递进的颜色变化 比例。比如第一个实例的颜色是(50, 50, 100),这3个值分别设置为0.1, 0.2, 0.3的话,则第二个实例的颜色 就是(50 * 1.1, 50 * 1.2, 100 * 1.3)。假如第一个实例的颜色是黑色,则这几个设置都会失效,应该黑色是(0, 0, 0)instanceAlpha
: 递进透明度。可以利用CAReplicatorLayer来实现反射效果
view.backgroundColor = UIColor.grayColor()
let containerLayer = CAReplicatorLayer()
view.layer.addSublayer(containerLayer)
containerLayer.frame = view.bounds
containerLayer.instanceCount = 2
containerLayer.instanceAlphaOffset = -0.6
containerLayer.instanceTransform = CATransform3DIdentity.rotate(angle: CGFloat(M_PI), x: 1, y: 0, z: 0)
let textLayer = CATextLayer()
containerLayer.addSublayer(textLayer)
textLayer.bounds = CGRect(x: 0, y: 0, width: 300, height: 50)
textLayer.position = view.center
textLayer.position.y -= 30
textLayer.string = "Hello world!"
textLayer.alignmentMode = kCAAlignmentCenter
textLayer.font = CGFontCreateWithFontName(UIFont.systemFontOfSize(40).fontName)
textLayer.fontSize = 40
textLayer.foregroundColor = UIColor.redColor().CGColor
textLayer.backgroundColor = UIColor.whiteColor().CGColor
可以滚动的layer。默认装maskToBounds设为true。调用scrollToPoint,layer会将指定的点滚动到左上角,成为bounds.orginal。下面的的代码可以滚动一个渐变的颜色块。
lazy var colorScrollLayer: CAScrollLayer = {
let layer = CAScrollLayer()
layer.frame = self.view.bounds.rectByInsetting(dx: 100, dy: 200)
let colorLayer = CAGradientLayer()
layer.addSublayer(colorLayer)
colorLayer.frame = self.view.bounds
colorLayer.startPoint = CGPoint(x: 0, y: 0)
colorLayer.endPoint = CGPoint(x: 1, y: 1)
colorLayer.colors = [UIColor.yellowColor(), .greenColor(), .blueColor(), .orangeColor(), .purpleColor()].map{ $0.CGColor }
return layer
}()
func testScrollLayer() {
view.layer.addSublayer(colorScrollLayer)
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: "pan:"))
}
func pan(gesture: UIPanGestureRecognizer) {
let offset = gesture.translationInView(view)
print(colorScrollLayer.bounds.origin)
var position = colorScrollLayer.bounds.origin
position.x -= offset.x
position.y -= offset.y
colorScrollLayer.scrollPoint(position)
gesture.setTranslation(CGPoint.zeroPoint, inView: view)
}
用于优化大图的显示性能。比如一个2048*2048的地图,一次性载入内容就很卡,而且也没必要。只载入和显示当前窗口需要显示的部分即可。所以要将这个大图裁成许多小图。指定CATiledLayer的delegate,在其drawLayer:inContext方法中绘制需要的小图就行了。(感觉没什么机会用到,先放着吧)
实现粒子效果(如下图)。估计也不多用,先放着)
简单地讲,就是可以使用OpenGL绘制任何图形的layer。(太复杂,先放着)
用于视频播放。还没试过,姑且把教程里的代码贴进来。
//get video URL
NSURL *URL = [[NSBundle mainBundle] URLForResource:@"Ship" withExtension:@"mp4"];
//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];
对playerLayer作各种transform也是可以播放滴。