文章分享至我的个人技术博客: https://cainluo.github.io/14807833712288.html
作者感言
在上一篇文章《Core Animation》CALayer的Specialized Layers中, 我们了解了CALayer的许多子类特性, 可以为我们在遇到一些特殊的开发需求中提供一定的帮助, 既然我们这次学的的Core Animation, 那怎么会和动画不挂钩呢? 这次让我们来初体验一下.
**最后:** **如果你有更好的建议或者对这篇文章有不满的地方, 请联系我, 我会参考你们的意见再进行修改, 联系我时, 请备注**`Core Animation`**如果觉得好的话, 希望大家也可以打赏一下~嘻嘻~祝大家学习愉快~谢谢~**
简介
Implicit Animations也称为隐式动画, 啥? 什么叫做隐式动画? 百度去吧~~哈哈哈(后面会讲解的), 这些问题就不在这里做解释了, 还是进入主题才比较重要.
Transactions
其实在Core Animation中, 动画效果并不需要我们去手动打开, 因为系统默认就是Open状态, 相反过来, 如果我们不需要动画的话, 我们需要手动的去关闭. 如果我们用一个CALayer的一个动画属性, 并且尝试去改变它, 这个效果并不会马上就显示出来, 因为它要从一个默认值平滑的过度到一个新的值, 而这些所有的内部操作我们都不需要去理会, 因为系统默认就是这么做的. 我们可以先来看个Demo:
- (void)transactionsColor {
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0,
100,
self.view.frame.size.width,
self.view.frame.size.width)];
view.backgroundColor = [UIColor grayColor];
[self.view addSubview:view];
UIButton *button = [[UIButton alloc] init];
button.center = CGPointMake(self.view.frame.size.width / 2, 50);
button.bounds = CGRectMake(0, 0, 100, 50);
button.backgroundColor = [UIColor blueColor];
[button setTitle:@"改变颜色"
forState:UIControlStateNormal];
[button addTarget:self
action:@selector(changeLayerColor)
forControlEvents:UIControlEventTouchUpInside];
[view addSubview:button];
self.colorLayer = [CALayer layer];
self.colorLayer.position = CGPointMake(view.frame.size.width / 2, view.frame.size.height / 2);
self.colorLayer.bounds = CGRectMake(0, 0, 150, 150);
self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
[view.layer addSublayer:self.colorLayer];
}
- (void)changeLayerColor {
CGFloat redColor = arc4random() / (CGFloat)INT_MAX;
CGFloat greenColor = arc4random() / (CGFloat)INT_MAX;
CGFloat blueColor = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:redColor
green:greenColor
blue:blueColor
alpha:1.0f].CGColor;
}
复制代码
看完这个Demo其实就已经知道神马叫做隐式动画了, 所谓的隐式动画就是我们没有给它指定任何的动画类型, 仅仅只是改变某个属性, 当然Core Animation也是支持显示动画, 不然我们就没那么多的兴趣来学习Core Animation了~ 那么当我们去改变一个属性的时候, Core Animation是如何去判断动画类型还有动画的持续时间呢? 这个问题其实也很简单, 动画的执行时间取决于Transactions的设置, 而动画类型是取决于CALayer的行为. 其实Transactions实际上是Core Animation用来包含一堆属性动画集合的机制, 任何用指定Transactions去改变可以做动画效果的图层属性都不会马上发生变化, 而是需要Transactions在提交的一瞬间, 才会开始用一个动画效果过渡到新设置的值. 而Transactions是需要通过CATransaction这个类来进行管理的, 奇怪的是, CATransaction这个类并不是管理一个简单的Transactions, 而是管理了一堆我们不能访问的Transactions, 由于CATransaction并没有属性和实例化方法, 也不能用**+ (instancetype)alloc;和- (instancetype)init;方法来创建它, 只有它所提供的+ (void)begin;和+ (void)commit;来控制. 虽然我们再上面的Demo里没有设置动画时间, 但Core Animation会在每一个run looop周期中自动开始一次新的Transactions**, 即使我们不手动的去调用**[CATransaction begin];, 但在每一次run loop**的循环中, 被修改的属性都会集中起来, 然后统一做一次0.25秒的动画, 这个是系统默认的. 说了那么多, 我们实际上来改改修改颜色的那个代码块, 让它有一个显示动画的效果:
- (void)changeLayerColorAgain {
[CATransaction begin];
[CATransaction setAnimationDuration:2.0f];
CGFloat redColor = arc4random() / (CGFloat)INT_MAX;
CGFloat greenColor = arc4random() / (CGFloat)INT_MAX;
CGFloat blueColor = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:redColor
green:greenColor
blue:blueColor
alpha:1.0f].CGColor;
[CATransaction commit];
}
复制代码
看起来的效果让人觉得是真的有动画效果了, 如果大家在之前就已经用过UIView来做过动画的话, 那么大家应该对这个动画模式不会感觉到陌生, 因为UIView就有两个类似的方法, + (void)beginAnimations:(nullable NSString *)animationID context:(nullable void *)context;和+ (void)commitAnimations;, 其实这两个这两个方法也是因为在内部设置了CATransaction的原因. 在iOS 4的时候, 苹果就已经对UIView添加了一种基于Block的动画方法, + (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations NS_AVAILABLE_IOS(4_0);, 用起来更加的方便, 但实际上是做同样的事情, 但使用这种方法就可以避免**+ (void)begin;和+ (void)commit;**匹配的问题造成一些蛋疼的事情.
Completion Blocks
这里我们就使用一下基于UIView的Block动画方法, 我们可以在动画结束之后再对这个图层进行一些操作, 当然这里还是基于上面的Demo来做演示:
- (void)changeLayerColorWithCompletion {
[CATransaction begin];
[CATransaction setAnimationDuration:2.0f];
[CATransaction setCompletionBlock:^{
CGAffineTransform transform = self.colorLayer.affineTransform;
transform = CGAffineTransformRotate(transform, M_PI_4);
self.colorLayer.affineTransform = transform;
}];
CGFloat redColor = arc4random() / (CGFloat)INT_MAX;
CGFloat greenColor = arc4random() / (CGFloat)INT_MAX;
CGFloat blueColor = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:redColor
green:greenColor
blue:blueColor
alpha:1.0f].CGColor;
[CATransaction commit];
}
复制代码
Layer Actions
开始的时候我们就用一个Demo来进行演示:
- (void)addLayerView {
self.layerView = [[UIView alloc] init];
self.layerView.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
self.layerView.bounds = CGRectMake(0, 0, 150, 150);
self.layerView.backgroundColor = [UIColor redColor];
[self.view addSubview:self.layerView];
UIButton *button = [[UIButton alloc] init];
button.center = CGPointMake(self.view.frame.size.width / 2, 200);
button.bounds = CGRectMake(0, 0, 100, 50);
button.backgroundColor = [UIColor blueColor];
[button setTitle:@"改变颜色"
forState:UIControlStateNormal];
[button addTarget:self
action:@selector(changeLayerViewColor)
forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
- (void)changeLayerViewColor {
[CATransaction begin];
[CATransaction setAnimationDuration:2.0f];
CGFloat redColor = arc4random() / (CGFloat)INT_MAX;
CGFloat greenColor = arc4random() / (CGFloat)INT_MAX;
CGFloat blueColor = arc4random() / (CGFloat)INT_MAX;
self.layerView.layer.backgroundColor = [UIColor colorWithRed:redColor
green:greenColor
blue:blueColor
alpha:1.0f].CGColor;
[CATransaction commit];
}
复制代码
看完这个Demo, 有很多人肯定会有疑问, 为啥没有了之前的那个平滑过渡效果呢? 好像是被干掉了, 这是啥回事? 其实我们可以仔细想一想, 如果UIView里的属性都有动画特性的话, 那我们去修改这些属性时, 肯定会注意到的, 可为啥UIKit要把这个隐式动画给禁止呢? 我们都知道Core Animation通常会对CALayer所有的可做动画的属性都赋予了动画特性, 但在UIView中就不一样了, 它会默认把所关联在一起的CALayer的这个特性给关闭掉, 这里就要了解一下隐式动画是如何实现的. 当我们改变CALayer属性时, CALayer自动应用的动画, 我们可以成为CALayer的行为, 每当CALayer的属性被修改的时候, 它会去调用**- (nullable id)actionForKey:(NSString *)event;**这个方法去传递属性的名称, 然后就会去执行如下几步:
首先CALayer会去检测它是否有Delegate, 并且看看这个Delegate有没有实现CALayerDelegate协议里的**- (nullable id)actionForLayer:(CALayer *)layer forKey:(NSString *)event;**方法, 如果有, 就直接调用并返回结果.
如果CALayer没有Delegate的话, 或者Delegate没有实现**- (nullable id)actionForLayer:(CALayer *)layer forKey:(NSString *)event;方法, 那么图层就会接着去检查包含属性名称对应的CALayer行为所映射的Actions**字典.
如果Actions字典没有包含对应的属性, 那么图层接着会在它的style字典里接着搜索属性名.
最后, 在style里也找不到对应的行为, 那么图层就会直接调用**+ (nullable id)defaultActionForKey:(NSString *)event;实现系统所提供的每个属性的默认行为. 如果一轮完整的搜索结束之后, - (nullable id)actionForKey:(NSString *)event;返回为空的话, 那么肯定不会有动画效果, 如果返回CAAction协议对应的对象, CALayer会拿这个结果去对比先前和当前的值, 并且做一个动画效果. 知道这个原理之后, 我们就知道UIKit**是肿么把隐式动画给禁止掉了:
每一个UIView对它所关联的图层都是充当一个Delegate对象, 并且提供了**- (nullable id)actionForKey:(NSString *)event;**的实现方法.
当不在一个动画块的实现中, 那么UIView就会对所有CALayer的行为返回nil, 如果在动画的Block范围之内, UIView就会返回一个非空的值. 这里我们简单的Log一下结果:
- (void)checkViewAction {
UIView *layerView = [[UIView alloc] init];
layerView.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
layerView.bounds = CGRectMake(0, 0, 150, 150);
layerView.backgroundColor = [UIColor redColor];
[self.view addSubview:layerView];
NSLog(@"Before: %@", [layerView actionForLayer:layerView.layer
forKey:@"backgroundColor"]);
[UIView beginAnimations:nil
context:nil];
NSLog(@"After: %@", [layerView actionForLayer:layerView.layer
forKey:@"backgroundColor"]);
[UIView commitAnimations];
}
复制代码
2016-12-04 12:45:28.178 7.ImplicitAnimations[57079:2126402] Before: <null>
2016-12-04 12:45:28.179 7.ImplicitAnimations[57079:2126402] After: <CABasicAnimation: 0x6000000327c0>
复制代码
这样子我们就可以知道, 当属性在Block之外发生改变, UIView会直接通过返回nil来禁用隐式动画, 但如果在动画块的范围之内, 就会根据动画的具体类型来返回相应的属性, 这个后续会讲到. 其实除了通过返回nil并不是唯一禁止隐式动画的方法, 我们也可以通过CATransacition的**+ (void)setDisableActions:(BOOL)flag;方法, 通过flag来对所有属性打开或者关闭隐式动画, 哪怕你是在[CATransaction begin];之后来添加, 也是一样可以关闭的. 这里还有一个Demo**, 使用CATransaction来实现的一个叫做推进过渡动画, 其实说白也就是一个Push动画:
- (void)pushAnimation {
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0,
100,
self.view.frame.size.width,
self.view.frame.size.width)];
view.backgroundColor = [UIColor grayColor];
[self.view addSubview:view];
UIButton *button = [[UIButton alloc] init];
button.center = CGPointMake(self.view.frame.size.width / 2, 50);
button.bounds = CGRectMake(0, 0, 100, 50);
button.backgroundColor = [UIColor blueColor];
[button setTitle:@"改变颜色"
forState:UIControlStateNormal];
[button addTarget:self
action:@selector(pushChangeColor)
forControlEvents:UIControlEventTouchUpInside];
[view addSubview:button];
self.colorLayer = [CALayer layer];
self.colorLayer.position = CGPointMake(view.frame.size.width / 2, view.frame.size.height / 2);
self.colorLayer.bounds = CGRectMake(0, 0, 150, 150);
self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
CATransition *transition = [CATransition animation];
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromLeft;
self.colorLayer.actions = @{@"backgroundColor": transition};
[view.layer addSublayer:self.colorLayer];
}
- (void)pushChangeColor {
[CATransaction begin];
[CATransaction setAnimationDuration:2.0f];
CGFloat redColor = arc4random() / (CGFloat)INT_MAX;
CGFloat greenColor = arc4random() / (CGFloat)INT_MAX;
CGFloat blueColor = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:redColor
green:greenColor
blue:blueColor
alpha:1.0f].CGColor;
[CATransaction commit];
}
复制代码
Presentation Versus Model
其实仔细想想, CALayer的属性行为并不太正常, 为何这么说呢, 因为当我们去改变一个图层的属性时, 我们会发现, 这个值的确是立即发生了改变, 但在屏幕上并没有马上生效, 为何呢? 因为我们在设置属性的时候, 并没有直接去调整图层的显示外观, 仅仅只是定义了图层动画结束之后即将要发生改变的外观. Core Animation在这里充当了一个控制器的角色, 并且根据Layer Actions和Transactions来更新视图在屏幕上显示的状态. 在于用户交互的界面中, CALayer的行为更像是保存着视图如何去显示和动画的执行数据模型. 在iOS中, 屏幕会以每秒钟重绘60次, 如果动画市场比60分之一秒还要长, 那么在这段时间里, Core Animation就会对屏幕上的图层进行重新的组合, 这就意味着CALayer除了我们给予的值之外, 还必须要知道当前显示在屏幕上的属性值的记录. 而每个图层属性的显示值都会被存储在一个叫做呈现图层的独立图层当中, 我们可以通过**- (nullable instancetype)presentationLayer;方法来访问, 而这个所谓的呈现图层**, 实际上就是模型图层的复制, 但它的好处是它的属性值代表了在任何指定时间当前所显示的外观效果, 通俗点来讲, 就是我们可以通过获取呈现图层的值来获取当前屏幕上真正显示出来的值. 这里需要注意的一点就是, 如果在呈现图层仅仅当CALayer首次被提交的时候创建, 那么去调用**- (nullable instancetype)presentationLayer;方法就会返回nil**. 这里我们或许还会注意到另一个方法**- (instancetype)modelLayer;, 如果我们在呈现图层上调用这个方法, 那么就会返回一个它正在呈现所以来的CALayer**, 而通常在一个图层上调用这个方法, 就会返回self. 在大多数开发的场景下, 我们都不需要直接访问呈现图层, 我们可以通过和模型图层的交互, 来让Core Animation更新并且显示, 但在以下两种场景下呈现图层就非常有用了, 一个是在同步动画里, 一个是在处理用户交互的时候:
- 如果我们在实现一个基于定时器的动画, 而不仅仅是基于Transactions的动画, 这个时候我们就要准确的知道在某一时刻图层显示在什么位置, 这就会对正确的布局起非常大的作用了.
- 如果我们想让做动画的图层对于用户有交互, 我们可以使用**- (nullable CALayer *)hitTest:(CGPoint)p;方法来判断指定的图层是否被点击了, 这个时候就会显示更加的友好, 因为呈现图层代表了用户当前看到的图层位置, 而不是当动画效果结束之后的位置. 说了那么多, 还是直接上Demo**比较直接:
- (void)presentationVersusModel {
self.colorLayer = [CALayer layer];
self.colorLayer.position = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
self.colorLayer.bounds = CGRectMake(0, 0, 150, 150);
self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
[self.view.layer addSublayer:self.colorLayer];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
CGPoint point = [[touches anyObject] locationInView:self.view];
if ([self.colorLayer.presentationLayer hitTest:point]) {
CGFloat redColor = arc4random() / (CGFloat)INT_MAX;
CGFloat greenColor = arc4random() / (CGFloat)INT_MAX;
CGFloat blueColor = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:redColor
green:greenColor
blue:blueColor
alpha:1.0f].CGColor;
} else {
[CATransaction begin];
[CATransaction setAnimationDuration:4.0f];
self.colorLayer.position = point;
[CATransaction commit];
}
}
复制代码
总结
总结一下:
- Core Animation默认是打开动画效果的, 并且默认的动画效果是平滑过渡滴.
- 我们知道了隐式动画的实现方式.
- UIView关联的图层默认都禁用了隐式动画, 对这种图层做动画的唯一办法就是使用UIView的动画函数, 或者是继承与UIView并且重写**- (nullable id)actionForLayer:(CALayer *)layer forKey:(NSString *)event;**方法, 最直接的方法就是直接创建一个显示动画.
- 对于一个单独存在的图层来讲, 我们可以通过实现图层的**- (nullable id)actionForLayer:(CALayer *)layer forKey:(NSString *)event;方法, 或者是提供一个Actions**的字典来控制隐式动画.
- 除此之外, 我们来了解了呈现图层和模型图层, 知道了这两个家伙的一些皮毛. 好了, 这次就到这里了, 谢谢大家~
工程地址
项目地址: https://github.com/CainRun/CoreAnimation