核心动画(Core Animation)是iOS和OS X上图形渲染和动画的基础设施,用于为应用程序的视图和其他视觉元素设置动画。核心动画本身不是绘图系统。它是用于在硬件中合成和操纵应用程序内容的基础设施。这种基础设施的核心是层(layer)对象,用于管理和操纵您的内容。一个图层将您的内容捕获到位图中,这些位图可以通过图形硬件轻松操作。在大多数应用程序中,图层用作管理视图内容的一种方式,但也可以根据需要创建独立图层。
基于layer层的绘图与传统的基于视图的绘图技术有很大的不同。使用基于视图的绘图,对视图本身的更改通常会导致调用视图的drawRect:
方法以使用新参数重新绘制内容。但是以这种方式绘制是昂贵的,因为它是使用主线程上的CPU完成的。Core Animation可以通过在硬件中操作缓存的位图来实现相同或相似的效果,从而避免这种代价。
一、关于Layer的几个重要属性及方法
CALayer类中属性和方法有很多,大多数和使用View的方式相同,在此不多赘述,具体可以自行查看CALayer.h文件。下面主要介绍一下比较重要的属性方法。
1、属性:
- position(CGPoint):位置,与view中的center差不多,默认情况是layer的中心。但是会受锚点(anchorPoint)影响而改变。
- zPosition(CGFloat):layer的position在父类(super layer)上的Z轴分量,默认为零。
- anchorPoint(CGPoint):锚点,使用的是以自身为参考的单位坐标系,默认为(0.5, 0.5)。当操纵层的属性position或transform属性时,锚点的影响最为明显。position属性总是相对于图层的锚点指定,并且对于应用于图层的任何转换也会相对于锚点发生。
anchorPointZ(CGFloat):layer的anchorPoint在Z轴上的分量,默认为零。
-
transform(CATransform3D):作用于layer的3D变换。默认是单位矩阵CATransform3DIdentity。
- 每个layer都有两个变换矩阵属性(
transform、sublayerTransform
),可以使用它们来操纵图层及其内容。CALayer的transform属性指定要既适用于层和其嵌入式子层的变换。通常,当要修改图层本身时,可以使用此属性。sublayerTransform属性定义了仅适用于子层的附加变换,最常用于向场景内容添加透视视觉效果。 - 因为Core Animation值可以在三维中指定,所以每个坐标点有四个值必须乘以四乘四个矩阵(见下图)。核心动画提供了一套全面的功能,用于创建缩放,转换和旋转矩阵以及进行矩阵比较(具体可见CATransform3D.h文件,里面方法的使用可以参考CATransform3D -> 3D变换
)。除了使用函数操纵变换之外,Core Animation扩展了键值编码支持,允许使用关键路径修改变换。
- 每个layer都有两个变换矩阵属性(
sublayerTransform(CATransform3D):当将内容呈现到接收器的输出中时,将3D变换应用于“sublayers”数组的每个成员。 通常用作投影矩阵以将透视和其他观看效果添加到模型。默认是单位矩阵。
sublayers(NSArray<CALayer >):子layer的集合。
masksToBounds(BOOL):是否沿着边界裁剪。
-
contents(id):layer的内容由包含要显示的视觉数据的位图组成。您可以通过以下三种方式之一提供该位图的内容:
- 直接将图像对象直接分配给图层对象的
contents
属性。(这种技术最适合从未或很少改变的图层内容。注意必须为CGImageRef
类型) - 将一个委托对象分配给图层,让代理绘制图层的内容。(此技术最适合可能会周期性更改并可由外部对象(如视图)提供的图层内容。)
- 定义一个layer的子类并覆盖其绘图方法,自己提供图层内容。(如果您要创建自定义层子类,或者如果要更改图层的基本绘图行为,则此技术是适用的。)
- 直接将图像对象直接分配给图层对象的
contentsRect(CGRect):默认是{0, 0, 1,1}通俗来说就是将layer看成单位矩形,取其中的一部分。所有参数一般是[0-1]。
-
contentsGravity(NSString):决定分配给
contents
属性图像一什么方式呈现,一般分配给该属性的值分为两类:- 基于位置的重力常数允许将图像定向到图层边界矩形的特定边缘或角落,而 不缩放图像。具体见 PS-1。
- 基于缩放的重力常数允许来拉伸图像。具体见 PS-2。
contentsScale(CGFloat):默认值是1.0,属性主要作用是适应Retina屏与非Retina屏的,如果是Retina屏则设置为2.0。一般可以这样设置
[UIScreen mainScreen].scale
。contentsCenter(CGFloat):默认是{0, 0, 1,1},在contentsRect的基础上确定缩放比例,主要决定因子是前两个元素,决定了原图纵横/现图纵横的比例,如:0.2则原图被放大五倍!!!
opaque(BOOL):由
-drawInContext
提供的图层内容是完全不透明的。 默认为NO。opacity(float):layer的透明度,默认是1。
还有一些常见的属性:如frame
, hidden
, backgroundColor
, cornerRadius
, borderWidth
, borderColor
, shadowColor
,shadowOpacity
, shadowOffset
, shadowRadius
, shadowPath
等。
除此之外,核心动画对它所属的CAAnimation和CALayer类扩展了NSKeyValueCoding的协议 --- 必须使用setValue:forKeyPath:
和valueForKeyPath:
方法来设置和获取这些字段,并增加了关键路径对CGPoint
,CGRect
,CGSize
,和CATransform3D
类型的支持(表-1)。所以可以直接使用KVC对其 属性 进行赋取值,在这里需要特别注意对transform等结构体来使用keyPath的情形(表-2)
表-1
C类 | 包装类 |
---|---|
CGPoint /CGSize/CGRect/CATransform3D | NSValue |
表-2
字段路径 | 包装类及描述 |
---|---|
transform.translation | NSValue(包含CGSize数据类型) , 在x和y轴上平移的量 |
transform. translation.x | NSNunber , 沿x轴平移 |
transform. translation.y | NSNunber , 沿y轴平移 |
transform. translation.z | NSNunber , 沿z轴平移 |
transform. scale | NSNunber , xyz三个比例因子的平均值 |
transform. scale.x | NSNunber , 沿x轴缩放 |
transform. scale.y | NSNunber , 沿y轴缩放 |
transform. scale.z | NSNunber , 沿z轴缩放 |
transform. rotation | NSNunber , 沿z轴旋转的弧度,与transform. rotation.z相同 |
transform. rotation.x | NSNunber , 沿x轴旋转的弧度 |
transform. rotation.y | NSNunber , 沿y轴旋转的弧度 |
transform. rotation.z | NSNunber , 沿z轴旋转的弧度 |
*** | *** |
position | NSValue |
position.x | NSNunber |
position.y | NSNunber |
*** | *** |
bounds/frame | NSValue |
bounds.origin | 同position |
bounds. size | NSValue |
bounds. size.width | NSNumber |
bounds. size.height | NSNumber |
PS-1:基于位置的重力常数:
CA_EXTERN NSString * const kCAGravityCenter
CA_EXTERN NSString * const kCAGravityTop
CA_EXTERN NSString * const kCAGravityBottom
CA_EXTERN NSString * const kCAGravityLeft
CA_EXTERN NSString * const kCAGravityRight
CA_EXTERN NSString * const kCAGravityTopLeft
CA_EXTERN NSString * const kCAGravityTopRight
CA_EXTERN NSString * const kCAGravityBottomLeft
CA_EXTERN NSString * const kCAGravityBottomRight
PS-2:基于缩放的重力常数:
CA_EXTERN NSString * const kCAGravityResize
CA_EXTERN NSString * const kCAGravityResizeAspect
CA_EXTERN NSString * const kCAGravityResizeAspectFill
2、方法:
+ (nullable id)defaultValueForKey:(NSString *)key;
:在使用KVC时为键提供默认值。一般进行复写来覆盖原有方法进行默认值设置。- (void)display;
:重新加载图层的内容。 用法:如果实现了委托方法,默认会调用displayLayer:委托方法。否则,display方法会调用drawInContext方法,然后更新图层的“contents”属性。但是一般不主动调用!- (void)setNeedsDisplay;
、- (void)setNeedsDisplayInRect:(CGRect)r;
:与上面的方法差不多,但是可以主动调用。如果设置了rect,则只有该层的该区域无效。- (void)displayIfNeeded;
:绘图系统在需要时自动调用,如果已经调用了setNeedsDisplay,该方法无效。- (void)drawInContext:(CGContextRef)ctx;
默认的display方法会创建一个视图图形上下文并将其传递给drawInContext:方法,与[UIView drawRect:]方法相似。
代理方法:
- (void)displayLayer:(CALayer *)layer;
如果实现了代理同时定义了此方法则,-display方法会调用此方法,该实现负责创建位图并将其分配给图层的contents属性。- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)cox;
-drawInContext调用此方法,创建一个图形上下文来绘制该位图,然后调用委托方法来填充位图。如果-displayLayer: 方法存在,则该方法不调用,无效。- (void)layerWillDraw:(CALayer *)layer
:新加的方法,如果定义,则由-display方法的默认实现调用。 允许代理在-drawLayer之前配置影响内容的任何图层状态。同样,如果-displayLayer: 方法存在,则该方法不调用,无效。
所以综以上方法总结layer方法响应链有两种:
- [layer setNeedDisplay] / [layer displayIfNeed] -> [layer display] -> [layerDelegate displayLayer:] 。
- [layer setNeedDisplay] / [layer displayIfNeed] -> [layer display] -> [layer drawInContext:] -> [layerDelegate drawLayer: inContext:]。
关于动画的方法:
(void)addAnimation:(CAAnimation *)anim forKey:(nullable NSString *)key;
(void)removeAllAnimations;
(void)removeAnimationForKey:(NSString *)key;
示例代码:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
_blueLayer = [CALayer layer];
[_blueLayer setValue:(__bridge id)[UIColor blueColor].CGColor forKeyPath:@"backgroundColor"];
[_blueLayer setValue:[NSValue valueWithCGPoint:CGPointZero] forKeyPath:@"anchorPoint"];
[_blueLayer setValue:[NSValue valueWithCGRect:CGRectMake(0, 0, 1, 1)] forKeyPath:@"contentsRect"];
[_blueLayer setValue:(__bridge id)[[UIImage imageNamed:@"boy"] CGImage] forKeyPath:@"contents"];
[_blueLayer setValue:[NSValue valueWithCGRect:CGRectMake(50, 100, 200, 350)] forKeyPath:@"frame"];
_blueLayer.delegate = self;
[self.view.layer addSublayer:_blueLayer];
}
- (void)displayLayer:(CALayer *)layer
{
if (once) {
[_blueLayer setValue:[NSValue valueWithCGRect:CGRectMake(0, 0, 1, 0.7)] forKeyPath:@"contentsRect"];
[_blueLayer setValue:(__bridge id)[[UIImage imageNamed:@"boy"] CGImage] forKeyPath:@"contents"];
}else{
[_blueLayer setValue:[NSValue valueWithCGRect:CGRectMake(0, 0, 1, 1)] forKeyPath:@"contentsRect"];
[_blueLayer setValue:(__bridge id)[[UIImage imageNamed:@"girl"] CGImage] forKeyPath:@"contents"];
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[_blueLayer setValue:kCAGravityResizeAspect forKeyPath:@"contentsGravity"];
if (once) {
once = NO;
[UIView animateWithDuration:10.0 animations:^{
[_blueLayer setValue:[NSNumber numberWithFloat:100] forKeyPath:@"transform.translation.x"];
[_blueLayer setValue:[NSNumber numberWithFloat:100] forKeyPath:@"transform.translation.y"];
}];
}else{
[UIView animateWithDuration:10.0 animations:^{
[_blueLayer setValue:[NSValue valueWithCATransform3D:CATransform3DIdentity] forKeyPath:@"transform"];
}];
once = YES;
}
[_blueLayer setNeedsDisplay];
}
- (void)dealloc
{
// 在这里代理一定要置空!否则控制器无法释放
_blueLayer.delegate = nil;
}