要做出具有相当高的用户体验的 APP,适当的使用动画必不可少。iOS 中的动画有很多种,从其他的博文中,你可能会找到很多诸如基础动画
、关键帧动画
, 转场动画
等等。这些动画其实都是基于 CALayer 的变换和隐式动画的组合。这篇文章将详细介绍 CALayer 和它的隐式动画。
一、CALayer
CALayer 是一个很基础的类,它被包含在 Core Animation 中,Core Animation 不能根据它的字面意思来理解,它的真实身份来源于 Layer Kit ,动画只是它的一部分,并且是很小的一部分。Core Animation 是一个复合引擎,它的存在目的是帮助系统以尽可能快的速度将屏幕上不同的可视内容组合起来,存放于图层树的体系中。我们在 iOS 上看到的一切东西都由 Core Animation 来呈现,包括动画。CALayer 作为 Core Animation 中基本的图层,它提供了可视化功能,我们使用的 UIView 本身不可见,只是因为其内部承载了一个 CALayer 才具有可见功能。而 UIView 则提供了响应事件的能力,本质上,UIView 负责事件链的传递和响应,CALayer 则负责视图的呈现。
CALayer 之所以能够显示内容,因为它包含一个 contents 属性,这个属性的类型虽然为 id
类型,但是,如果我们填入了非图片类型的数据之后,会使得 CALayer 显示空白。CALayer的contents接受来自其他的(通常是 Core Graphics)绘制内容组成的图片,加入其中,这个图片被称之为寄宿图。
每一个 UIView 都有一个 CALayer 相联系, UIView 默认是对应 CALayer 的代理,我们通过 UIView 的 layer 属性就能够访问到它。大体上,UIView 已经很完善了,不仅能够看见,还能够进行用户交互,对于更加底层的 CALayer,我们是在没有过多接触的必要。但不代表我们不会使用到它 —— CALayer 相比 UIView 具有如下优势:
- 设置视图的圆角、阴影、边框
- 3D变换
- 不规则的形状
- 透明遮罩
以上的功能,在日常的开发中或多或少的会使用到。而这些通过 CALayer 可很容易的做到,UIView 并没有这些功能。
CALayer 绘制
同时在绘制方面,UIView 也是基于 CALayer 来进行的。 通常我们会在代码中使用
- (void) DrawRect;
为 UIView 进行重绘。当我们实现了这个方法的时候,它会在 UIView 呈现的时候被调用。或者我们也可以主动使用-(void)setNeedsDisplay
方法来间接调用, 另外,在 UIView 的 bounds 被修改的时候,这个重绘方法也会被调用。
根本原因是,UIView 被当作 CALayer 的代理,调用 - (void) DrawRect;
将自动运行 CALayer 的绘制功能代码,也就是说, UIView 帮我们将如何调用 CALayer 绘制的动作给做了。
CALayerDelegate的代理方法中,有以下两个方法:
-(void)displayLayer:(CAlayer)layer;
-(void)drawLayer:(CALayer)layer inContet:(CgContentRef)ctx;
-(void)displayLayer:(CAlayer)layer;
可以给layer接受一个来自于 UIView 的绘制内容,我们也可以不实现这个方法,那么代理会继续调用下面的-(void)drawLayer:(CALayer)layer inContet:(CgContentRef)ctx;
方法,这个方法中将直接绘制内容到 layer 上。当然这些工作不需要我们自己来做,UIView 会替我们完成,我们做的仅仅是实现UIView的- (void) DrawRect;
就可以了。
我们可以自己设置 layer 的代理,然后进行代理方法的实现,这样也能得到和 UIView 自动完成代理一样的效果。
坐标系
之前说了,UIView 的一切可视化都来自于它的 layer,因此包括 UIView 的frame
,bounds
,center
内容都来自CALayer, 这三个属性在CALayer中分别对应CALayer的属性:frame
,bounds
,position
。frame
其实是一个虚拟属性,它并不实际存在,而是通过 bounds
和position
计算得到的。他们的主要功能是描述视图的位置属性。UIVIew 中这三个属性任何一个进行改变,实际上都是改变了对应layer的三个对应属性。
除此之外,CALayer 还有一个很关键的属性anchorPoint
-- 锚点。它是一个单位量点,取值范围在(0-1)之间, 这个点在默认状态下是(0.5,0.5),我们改变它会将整个视图的位置偏移:
CALayer的 zPosition
一个很简单的例子是,当我们在同一个位置覆盖两个 UIView 的时候,后添加的 View 将会覆盖 底下的 View,这个顺序将决定事件响应链的传递之机制。
如果我们设置底下 view 对应的 CALayer 的 zPosition
在原来的基础上加1,那么我们将看到:
zPosition表示的是图层的层级属性数值,每个图层的该默认值都是0.如果有一个图层比0大,哪怕是0.001,这个图层就会被推到最前面。
但是要注意的是,尽管 zPosition 改变了图层的显示顺序,却对每个视图之间的事件传递没有影响。上图中,绿色矩形原先被覆盖的部分会收到红色矩形的影响,尽管我们从视觉上看到它在上面。所以,事件的响应和图层的显示顺序并没有必然的联系。
CALayer 的事件处理
Core Animation 没有给 CALayer 添加事件处理机制,我们不能使用它来进行交互,如果要使用交互的话,我们可以使用两个方法检测用户的交互事件:
- (Bool) containPoint;
- (CALayer*)hitTest;
- (Bool) containPoint;
将检测一个 layer 是不是包含了某个点,它返回一个 Bool 值。而 - (CALayer*)hitTest;
则检测当前点处在某个 layer 之上,并将 layer 返回。
我们可以通过这两个方法,结合 屏幕Touch回调函数来检测用户的交互事件。
更多 CALayer 的使用知识,在我的另一篇博文中有介绍:
http://www.cnblogs.com/FBiOSBlog/p/6900534.html
二、CALayer 的隐式动画
Core Animation中,假设 CALayer 每个属性的改动都是有动画的(如果有可能)。对于单独的 CALayer,每一个属性的的改动,如果存在对应的属性动画,则会被实现并展现出来。因为 CALayer 这种特质的动画不需要进行任何动画代码的修饰就能够体现动画效果,所以这些动画被统称为隐式动画。但是CALayer 的隐式动画在 UIView 中不会被触发, 更确切地说是被 UIVIew 禁用了。
为了证明 UIView 禁用了隐式动画的事实,我写了一段代码:
_layer = [CALayer layer];
_layer.frame = CGRectMake(100, 100, 100, 100);
_layer.backgroundColor = [UIColor blueColor].CGColor;
_view = [[UIView alloc]initWithFrame:CGRectMake(220, 100, 100, 100)];
_view.backgroundColor = [UIColor blueColor];
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
btn.frame = CGRectMake([UIScreen mainScreen].bounds.size.width/2.0 - 50, 250, 100, 50);
[btn setTitle:@"点击" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(changeColor) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_view];
[self.view addSubview:btn];
[self.view.layer addSublayer:_layer];
}
- (void)changeColor{
UIColor *color = [UIColor colorWithRed:random()%255/256.0 green:random()%255/256.0 blue:random()%255/256.0 alpha:1];
// 设置 隐式动画的时长
[CATransaction setAnimationDuration:1];
_layer.backgroundColor = color.CGColor;
_view.backgroundColor = color;
}
效果:同样的都是改变视图的颜色,CALayer 会自动执行动画,(其中为了区别,我将隐式动画的事件拉长了,隐式动画的默认时长为0.25s),但是 UIView 却没有动画,尽管两者都没有任何其他的有关动画代码的修饰,这就是我们所说的 CALayer 的隐式动画。和隐式动画相对的还有显示动画,显示动画的使用其实是在隐式动画的基础上加上动画的事务 CATransaction ,CATransaction 是一个很特殊的类,它不提供任何初始化的方法,只暴露了部分 API,它的使用目的是让我们手动设置隐式动画为显式动画,并能够改动动画的效果。比如上面的代码:[CATransaction setAnimationDuration:1];
就是改变了动画的时长。我们如果要使用 CATransaction 构成显式动画可以这样做:
//给CALayer 开放显示动画
[CATransaction begin];
[CATransaction setAnimationDuration:1.5];
_layer.backgroundColor = color.CGColor;
[CATransaction commit];
按照我们之前说的内容,UIView 是通过 CALayer 的显示特性来呈现视图的,既然 CALayer 具有隐式动画效果,UIView 应该也能够获取这个特性才是,原因是它将隐式动画效果禁用了。 UIView 中,默认禁用隐式动画,但是我们可以通过 UIView 的一些 API 来开放这些动画。将上面的修改 UIView 颜色的那部分代码修改:
//给 UIVIew 添加动画
[UIView beginAnimations:@"animation" context:nil];
[UIView setAnimationDuration:1.5];
_view.backgroundColor = color;
[UIView commitAnimations];
再看看效果:由此可见,UIView 并非不能执行动画,而是需要在特定的环境中进行。iOS 中,我们除了使用 [UIView beginAnimations:@"animation" context:nil];
和 [UIView commitAnimations];
之外,还有一系列的其他手段开启动画。像 UIView 的Block动画 animationWithDuration:animations:
将动画的内容放在 block 中,以避免开发者动画开始到提交的闭合操作失误。但不管是哪一种动画,其内部都会调用CALayer 的事务 CATransaction 来进行相关的设置。
动画在APP的合理使用能够提升用户的体验,但是也会有部分性能的消耗,最主要的是,我们需要在合适的场景使用动画才更有意义,所以将隐式动画的开启开关交给开发者更为可靠。
那 UIView 禁用隐式动画的原理是什么呢?
在 CALayer 中,所有的动画被统称为: action —— 行为。CALayer 每次执行隐式动画之前,会执行一个函数:
- actionForkey()
这个函数内部其实做了以下事情:
- 首先查看layer本身是否有委托,并且检查这个委托是否实现了
- actionForLayer: forKey
代理方法,如果有这个方法,直接调用并返回结果。 否则进入到下一步。 - layer 接下来将会检查包含属性名对应行为映射的字典 actions。 actions 字典以属性名为key,以属性的行为为 value 存储。这是将决定一个属性是否有动画的过程之一。如果 actions 没有属性名,则到下一步。
- 除了 actions 字典,layer 还有一个 style 字典,这个字典中包含了一些固定的类型对应的动画,这是对不太好在 actions 定义的属性行为的补充。
- 最后,如果在上述中都找不到行为,那么图层调用每个属性对应的标准行为。也就是隐式动画的本身。
我们在使用 CATransaction 事务对动画做出修改的时候,实际上是在 actions 字典中添加行为,它具有比标准行为更高的优先级,一次每次改动总能生效。
我们知道 UIView 默认是对应的 CALayer 的代理,禁用动画的方式就是实现- actionForLayer: forKey
代理方法,并在方法中直接返回 nil,这样动画就生生被切掉了。
当我们使用了 UIView 的动画 API 的时候,这段期间 - actionForLayer: forKey
方法将返回我们实现的动画内容。这就是为什么我们调用这些 API 能够显示动画的原因了。那么如何给 UIView 永久开启隐式动画我们就有了思路了。
给 UIView 永久开启隐式动画。
其实开启隐式动画的方式我们已经做过了,就是在 UIView 的动画 API 中书写动画有关的代码。如果想要像 CALayer 一样,不需要其他的 API 也能给予 UIView 动画的话,我们或许可以创建一个类,继承自 UIView,并重写它的- actionForLayer: forKey
方法。当然这么做,可能有点麻烦,你可能更倾向于直接使用一个显示的动画吧。不过我们试试总归是可以的,我创建了一个MyAnimationView 类,它继承自 UIView ,然后实现它的- actionForLayer: forKey
方法:
#import "MyAnimationView.h"
@implementation MyAnimationView
- (nullable id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event;{
return [layer animationForKey:event];
}
@end
然后就像平常使用View一样使用它:
- (void)viewDidLoad {
[super viewDidLoad];
_view = [[MyAnimationView alloc]initWithFrame:CGRectMake(220, 100, 100, 100)];
_view.backgroundColor = [UIColor blueColor];
[self.view addSubview:_view];
btn.frame = CGRectMake([UIScreen mainScreen].bounds.size.width/2.0 - 50, 250, 100, 50);
[btn setTitle:@"点击" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(changeColor) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
}
- (void)changeColor{
UIColor *color = [UIColor colorWithRed:random()%255/256.0 green:random()%255/256.0 blue:random()%255/256.0 alpha:1];
// 这里我并没有添加任何和动画有关的代码 直接修改颜色
_view.backgroundColor = color;
}
隐式动画就介绍到这里,CALayer 还有很多其他有用的特性,有兴趣的同学可以多多挖掘。