UIView与CALayer动画原理
下面从以下两点结合具体代码来探索下CoreAnimation的一些原理
- 1.UIView动画实现原理
- 2.展示层(presentationLayer)和模型层(modelLayer)
UIView动画实现原理
UIView提供了一系列UIViewAnimationWithBlocks,我们只需要把改变可动画属性的代码放在animations的block中即可实现动画效果,比如:
[UIView animateWithDuration:1 animations:^(void){ if (_testView.bounds.size.width > 150) { _testView.bounds = CGRectMake(0, 0, 100, 100); } else { _testView.bounds = CGRectMake(0, 0, 200, 200); } } completion:^(BOOL finished){ NSLog(@"%d",finished); }];
效果如下:
之前说过,UIView对象持有一个CALayer,真正来做动画的是这个layer,UIView只是对它做了一层封装,可以通过一个简单的实验验证一下:我们写一个MyTestLayer类继承CALayer,并重写它的set方法;再写一个MyTestView类继承UIView,重写它的layerClass方法指定图层类为MyTestLayer:
MyTestLayer 实现:
@interface MyTestLayer : CALayer @end @implementation MyTestLayer - (void)setBounds:(CGRect)bounds { NSLog(@"----layer setBounds"); [super setBounds:bounds]; NSLog(@"----layer setBounds end"); } ... @end
MyTestView 实现:
@interface MyTestView : UIView - (void)setBounds:(CGRect)bounds { NSLog(@"----view setBounds"); [super setBounds:bounds]; NSLog(@"----view setBounds end"); } ... + (Class)layerClass { return [MyTestLayer class]; } @end
当我们给view设置bounds时,getter、setter的调用顺序是这样的:
也就是说,在view的setBounds方法中,会调用layer的setBounds;同样view的getBounds也会调用layer的getBounds。其他属性也会得到相同的结论。那么动画又是怎么产生的呢?当我们layer的属性发生变化时,会调用代理方法actionForLayer: forKey: 来获得这次属性变化的动画方案,而view就是它所持有的layer的代理:
@interface CALayer : NSObject <NSCoding, CAMediaTiming> ... @property(nullable, weak) id <CALayerDelegate> delegate; ... @end @protocol CALayerDelegate <NSObject> @optional ... /* If defined, called by the default implementation of the * -actionForKey: method. Should return an object implementating the * CAAction protocol. May return 'nil' if the delegate doesn't specify * a behavior for the current event. Returning the null object (i.e. * '[NSNull null]') explicitly forces no further search. (I.e. the * +defaultActionForKey: method will not be called.) */ - (nullable id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event; ... @end
注释中说明,该方法返回一个实现了CAAction的对象,通常是一个动画对象;当返回nil时执行默认的隐式动画,返回null时不执行动画。还是上面那个改变bounds的动画,我们在MyTestView中重写actionForLayer:方法
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event { id<CAAction> action = [super actionForLayer:layer forKey:event]; return action; }
观察它的返回值:
是一个内部使用的_UIViewAddtiveAnimationAction对象,其中包含一个CABassicAnimation,默认fillMode为both,默认时间函数为淡入淡出,只包含fromValue(即动画之前的值,会在这个值和当前值(block中修改过后的值)之间做动画)。我们可以尝试在重写的这个方法中强制返回nil,会发现我们不写任何动画的代码直接改变属性也将产生一个默认0.25s的隐式动画,这和上面的注释描述是一致的。
关于显式动画和隐式动画
显式动画是指用户自己通过beginAnimations:context:和commitAnimations创建的动画。
隐式动画(iOS 4.0后)是指通过UIView的animateWithDuration:animations:方法创建的动画。
如果两个动画重叠在一起会是什么效果呢?
还是最开始的例子,我们添加两个相同的UIView动画,一个时间为3s,一个时间为1s,并打印finished的值和两个动画的持续时间。先执行3s的动画,当它还没有结束时加上一个1s的动画,可以先看下实际效果:
log 打印
很明显,两个动画的finished都为true且时间也是我们设置好的3s和1s。也就是说第二个动画并不会打断第一个动画的执行,而是将动画进行了叠加。
动画的心酸路程:
- 最开始方块的bounds为(100,100),点击执行3s动画,bounds变为(200,200),并开始展示变大的动画;
- 动画过程中(假设到了(120,120)),点击1s动画,由于这时真实bounds已经是(200,200)了,所以bounds将变回100,并产生一个fromValue为(200,200)的动画。
但此时方块并没有从200开始,而是马上开始变小,并明显变到一个比100更小的值。
- 1s动画结束,finished为1,耗时1s。此时屏幕上的方块是一个比100还要小的状态,又缓缓变回到100—3s动画结束,finished为1,耗时3s,方块最终停在(100,100)的大小。
从这个现象我们可以猜想UIView动画的叠加方式:当我们通过改变View属性实现动画时,这个属性的值是会立即改变的,动画只是展示出来的效果。当动画还未结束时如果对同个属性又加上另一个动画,两个动画会从当前展示的状态开始进行叠加,并最终停在view的真实位置。
举个通俗点的例子,我们8点从家出发,要在9点到达学校,我们按照正常的步速行走,这可以理解为一个动画;假如我们半路突然想到忘记带书包了,需要回家拿书包(相当于又添加了一个动画),这时我们肯定需要加快步速,当我们拿到书包时相当于第二个动画结束了,但我们上学这个动画还要继续执行,我们要以合适的速度继续往学校赶,保证在9点准时到达终点—学校。
所以刚才那个方块为什么会有一个比100还小的过程就不难理解了:当第二个动画加上去的时候,由于它是一个1s由200变为100的动画,肯定要比3s动画执行的快,而且是从120的位置开始执行的,所以一定会朝反方向变化到比100还小;1s动画结束后,又会以适当的速度在3s的时间点回到最终位置(100,100)。当然叠加后的整个过程在内部实现中可能是根据时间函数已经计算好的。
这么做或许是为了让动画显得更流畅平滑,那么既然我们设置属性值是立即生效的,动画只是看上去的效果,那刚才叠加的时刻屏幕展示上的位置(120,120)又是什么呢?这就是本篇要讨论的下一个话题。
展示层(presentationLayer)和模型层(modelLayer)
我们知道UIView动画其实是layer层做的,而view是对layer的一层封装,我们对view的bounds等这些属性的操作其实都是对它所持有的layer进行操作,我们做一个简单的实验—在UIView动画的block中改变view的bounds后,分别查看下view和layer的bounds的实际值:
_testView.bounds = CGRectMake(0, 0, 100, 100); [UIView animateWithDuration:1 animations:^(void){ _testView.bounds = CGRectMake(0, 0, 200, 200); } completion:nil];
赋值完成后我们分别打印view,layer的bounds:
都已经变成了(200,200),这是肯定的,之前已经验证过set view的bounds实际上就是set 它的layer的bounds。可动画不是layer实现的么?layer也已经到达终点了,它是怎么将动画展示出来的呢?