动画由CoreAnimation
框架做为基础支持,理解动画以前要先理解CALayer
这个东西的扮演的角色,了解它是负责呈现视觉内容的东西,它有3个图层树,还有知道CATransaction
负责对layer的修改的捕获和提交。html
参考【重读iOS】认识CALayergit
除了系统实现层面的东西,仍是通用意义上的动画。动画就是动起来的画面,画面不断变换产生变化效果。并非真的有一个东西在动,一切都只是对大脑的欺骗。认识到这个,就知道动画须要:一系列的画面,这些画面之间具备相关性。github
因此对于动画系统而言,它须要:(1)知道变化规律,而后根据这个规律,(2)不断的去重绘画面。算法
有了这个认识,再来看最简单的UIView
的动画:spring
//建一个button
button = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 100, 40)];
button.backgroundColor = [UIColor orangeColor];
[self.view addSubview:button];
......
//一个简单的移动动画
[UIView animateWithDuration:3 animations:^{
button.frame = CGRectMake(0, 300, 100, 40);
}];
复制代码
这是一个移动的动画,移动是由于frame发生了改变。而后把这个修改放在UIView animateWithDuration:
的block里。对于系统而言,它有了button开始的位置,block里有告终束的位置,并且有了时间。bash
一个物体从一个点移动到另外一个点,并且时间已知,那么就能够求出在任何一个中间时间,这个物体的位置。这就是变化规律。而不断重绘这个就是屏幕的刷新了,这个是操做系统负责了,对于开发者而言,创造不一样动画就在于提供不一样的变化规律。app
UIView的一些动画方法只是提供了更方便的API,理解了CoreAnimation的动画,UIView的这些方法都天然清楚了,直接看CoreAnimation吧。框架
这个动画类的继承图,iOS9时又添加了CASpringAnimation
,继承自CABasicAnimation
。ide
每个动画类表明了某一种类型的动画,表明着它们有着不一样的变化规律。函数
这个是基类,因此它不会有特别有特点的属性,而是一些通用性的东西。在属性里值得注意的是timingFunction
和delegate
。timingFunction
提供了时间的变化函数,能够理解成时间流速变快或变慢。delegate
就两个方法,通知你动画开始了和结束了,没什么特别的。
这是一个协议,CAAnimation
实现了这个协议,里面有一些跟时间相关的属性挺有用的:
You do not create instances of CAPropertyAnimation: to animate the properties of a Core Animation layer, create instance of the concrete subclasses CABasicAnimation or CAKeyframeAnimation.
这个也仍是一个抽象类,跟UIGestureRecognizer
同样直接构建对象用不了的。但它的属性仍是值得解读一下:
keyPath
并且有一个以keyPath
为参数的构建方法,因此这个属性是核心级别。回到动画的定义上,除了须要变化规律外,还须要变化内容。巧妇难为无米之炊,动画是一种连续的变化,那就须要知道是什么在变化。这里选取内容的方式就是指定一个属性,这个属性是谁的属性?CALayer
的,动画是加载在layer上的,layer是动画的载体。打开CALayer
的文档,在属性的注释里写着Animatable
的就是能够进行动画的属性,也就是能够填入到这个keyPath里的东西。 之因此是keyPath而不是key,是由于能够像position.y
这样使用点语法指定连续一连串的key。从CAPropertyAnimation继承的动画,也都是按照这种方式来指定变化内容的。
additive
和cumulative
须要例子才好证明效果,到下面再说。valueFunction
这个属性类为CAValueFunction
,只能经过名称来构建,甚至没有数据输入的地方,也是从这忽然看明白CAPropertyAnimation
构建对象是没有意义的。由于没有数据输入,就没有动画,就无法实际应用,这个类只是为了封装的须要而建立的。总结一下,动画须要3个基本要素:内容、时间和变化规律,不一样的动画都是在这3者上有差别。
这个类就增长了3个属性:fromValue
toValue
byValue
。这3个属性就正好是提供了输入数据,肯定了开始和结束状态。
到如今,内容(keyPath)有了,时间(duration和timingFunction)有了,开始和结束状态有了。经过插值(Interpolates)就能够获得任意一个时间点的状态,而后渲染绘制造成一系列关联的图像,造成动画。
非空属性 | 开始值 | 结束值 |
---|---|---|
fromValue toValue |
fromValue | toValue |
fromValue byValue |
fromValue | fromValue+byValue |
toValue byValue |
toValue -byValue | toValue |
fromValue | fromValue | currentValue |
toValue | currentValue | toValue |
byValue | currentValue | byValue+currentValue |
上面的表表示的是当3个属性哪些是非空的时候,动画是从哪一个值开始、到哪一个值结束。并且上面的状况优先于下面的状况。
button.frame = CGRectMake(200, 400, 100, 40);
CABasicAnimation *basicAnim = [CABasicAnimation animationWithKeyPath:@"position"];
//mediaTiming
basicAnim.duration = 1;
basicAnim.repeatCount = 3;
//CAAnimation
basicAnim.removedOnCompletion = NO;
basicAnim.delegate = self;
//property
basicAnim.additive = NO;
basicAnim.cumulative = YES;
//basic
basicAnim.fromValue = [NSValue valueWithCGPoint:CGPointMake(100, 60)];
basicAnim.toValue = [NSValue valueWithCGPoint:CGPointMake(100, 200)];
[button.layer addAnimation:basicAnim forKey:@"move"];
复制代码
additive
为true时,变化值总体加上layer的当前值,如button开始位置为x为200,fromValue的x为100,开启additive
则动画开始时button的x为200+100=300,不开启则100.cumulative
这个指每次的值要加上上一次循环的的结束值。这个就须要repeatCount>1
的时候才能看出效果。好比这里button第一次动画结束后位置为(100, 200),再次开始时位置不是(100, 60),而是加上以前的结束值,即(200,260)。对于不一样类型的值叠加方式是不一样的,如矩阵,并非直接单个元素相加,而是使用矩阵加法。
终于到了明星关键帧动画。
关键帧动画,帧指一副画面,动画就是一帧帧画面连续变更而获得的。而关键帧,是特殊的帧,举个例子,一个物体按照矩形的路线运动,那么提供4个角的坐标就能够了,其余位置能够经过4个角的位置算出来。而关键帧就是那些不可缺乏的关键的画面,而其余帧能够经过这些关键帧推算出来。
因此关键帧动画就是提供若干关键的数据,系统经过这些关键数据,推算出整个流程,而后完成动画。
有了这个理解,再看CAKeyframeAnimation
的属性里的values
和keyTimes
就好理解了。
values
就是各个关键帧的数据,keyTimes
是各个关键帧的时间点,并且这两组数据时一一对应的,第一个value和第一个keyTime都是第一帧画面的,以此类推。
按照这种思路,其实整个动画就被切割成n个小阶段了,每一个节点有开始和结束数据和时间,就会发现这一小段其实就是一个CABasicAnimation
,而CABasicAnimation
也能够当作是一个特殊的关键帧动画,只有开始和结束两个关键帧。
因此在使用上和CABasicAnimation
并无特别的地方,只是从传from、to两个数据,变成传一组数据罢了。
属性path
这个是一种特殊的动画,若是要实现一个view按照某个路径进行移动,就使用这个属性,提供了路径后,values
属性会被忽略。路径能够经过贝塞尔曲线的类提供:
//内容
CAKeyframeAnimation *keyframeAnim = [CAKeyframeAnimation animationWithKeyPath:@"position"];
//时间
keyframeAnim.duration = 5;
//变化规律
UIBezierPath *path = [[UIBezierPath alloc] init];
[path addArcWithCenter:CGPointMake(200, 300) radius:100 startAngle:0 endAngle:M_PI*2 clockwise:YES];
keyframeAnim.path = [path CGPath];
复制代码
若是不提供这个path属性,那就要咱们提供许多的点来完成动画,哪怕是简单的转圈圈,点数据也超级多,越平滑的动画就须要越多的点。这个属性能够说是为了这种需求而提供的特殊福利。
属性calculationMode
这个属性影响着关键帧之间的数据如何进行推算,一个个来讲:
kCAAnimationLinear
默认属性,线性插值。kCAAnimationDiscrete
不进行插值,只显示关键帧的画面,看到的动画就是跳跃的kCAAnimationPaced
,这个也是线性插值,但跟第一个的区别是它是总体考虑的。举个例子,移动一个view,从A到B,再到C,假设A-B之间距离跟B-C之间距离同样,可是前者的时间是10s,后者是20s,那么动画里,后半段就会跑得慢。而Paced
类型,就忽略掉keyTimes属性,达到全局匀速的效果,从新计算keyTimes。这个例子里就变成A-B 15s,B-C也15s。kCAAnimationCubic
这个使用新的插值,算法是Catmull-Rom spline
,效果就是把转折点变得圆滑。看一下这两种路径对比就立马明白,第一个是线性插值。kCAAnimationCubicPaced
这个就是两种效果叠加。
属性rotationMode
这个是配合路径使用的,在使用路径动画时才有意义。当值为kCAAnimationRotateAuto
是,会把layer旋转,使得layer自身的x轴是跟路径相切的,而且x轴方向跟运动方向一致,使用kCAAnimationRotateAutoReverse
也是相切,但x轴方向跟运动方向相反。
这个看似简单,用起来却彷佛有点摸不着头脑。transition
过渡的意思,这个动画用来完成layer的两种状态之间的过渡。
问题的核心就在这个两种状态,查看CATransition
的属性,发现并无开始状态、结束状态之类的输入。那这两种状态怎么肯定?How does CATransition work?这个问题里的回答很清楚,截取一段:
The way the CATransition performs this animation to to take a snapshot of the view before the layer properties are changed, and a snapshot of what the view will look like after the layer properties are changed
两种状态分别是:layer修改以前和以后。也就是把CATransition
的动画加到layer上以后,这时会生成一个快照,这个开始状态;而后你要立马对layer进行修改,这时layer呈现出另外一种状态,这是修改后,也就是动画的结束状态。这时系统获得了两张快照,在这两张快照之间作过渡效果,就是这个动画。
因此若是你添加动画后不作修改,好像看不出什么效果。
一个例子:
[CATransaction begin];
UIView *container = [[UIView alloc] initWithFrame:CGRectMake(150, 200, 100, 100)];
container.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1];
[self.view addSubview:container];
UILabel *label1 = [[UILabel alloc] initWithFrame:container.bounds];
label1.backgroundColor = [UIColor redColor];
label1.text = @"1";
label1.font = [UIFont boldSystemFontOfSize:30];
label1.textAlignment = NSTextAlignmentCenter;
[container addSubview:label1];
UILabel *label2 = [[UILabel alloc] initWithFrame:container.bounds];
label2.backgroundColor = [UIColor orangeColor];
label2.text = @"2";
label2.font = [UIFont boldSystemFontOfSize:30];
label2.textAlignment = NSTextAlignmentCenter;
[container addSubview:label2];
[CATransaction commit];
CATransition *fade = [[CATransition alloc] init];
fade.duration = 2;
fade.type = kCATransitionPush;
fade.subtype = kCATransitionFromRight;
//位置1
[container.layer addAnimation:fade forKey:nil];
//位置2
[container insertSubview:label2 belowSubview:label1];
复制代码
一个view上面添加了两个子view,动画加载父视图上,添加动画后修改子view的上下关系来修改layer的样式。
为何要使用[CATransaction begin]
和[CATransaction commit]
把添加子视图的代码包起来呢?
这本是一个bug,没想到倒是一个对CATransaction
理解加深的好例子。缘由简单说:
container
的layer数据时空的,那么开始状态就没有,因此开始画面是空白。//位置1
[CATransaction begin];
UIView *container = [[UIView alloc] initWithFrame:CGRectMake(150, 200, 100, 100)];
container.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1];
[self.view addSubview:container];
[CATransaction commit];
//位置5
UILabel *label1 = [[UILabel alloc] initWithFrame:container.bounds];
label1.backgroundColor = [UIColor redColor];
label1.text = @"1";
label1.font = [UIFont boldSystemFontOfSize:30];
label1.textAlignment = NSTextAlignmentCenter;
[container addSubview:label1];
UILabel *label2 = [[UILabel alloc] initWithFrame:container.bounds];
label2.backgroundColor = [UIColor orangeColor];
label2.text = @"2";
label2.font = [UIFont boldSystemFontOfSize:30];
label2.textAlignment = NSTextAlignmentCenter;
[container addSubview:label2];
//位置2
[CATransaction begin];
container.backgroundColor = [UIColor colorWithWhite:0 alpha:1];
[CATransaction commit];
CATransition *fade = [[CATransition alloc] init];
fade.duration = 2;
fade.type = kCATransitionPush;
fade.subtype = kCATransitionFromRight;
//位置3
[container.layer addAnimation:fade forKey:nil];
//位置4
[container insertSubview:label2 belowSubview:label1];
复制代码
若是作一下简单的修改:改为位置1和位置2两个事务,位置1时container
颜色是灰色,位置2时是黑色。中间label1和label2的处理代码不加入显式事务。
结果会怎么样?
动画变成开始画面是灰色的container
,结束状态是label1的样式。
仍是开始状态的问题,有两个问题:
中间有一段(位置5)没有加入显式事务,那么它就开启了隐式事务,它要等到下一次runloop循环才提交,反正是要等到这个方法执行结束。那么这一段都没有加入到container
的layer里,因此不会是label2的样式。
由于隐式事务开启了,又尚未结束,因此位置2的事务变成了一个嵌套事务,而嵌套事务我只找到这么一句话文档位置:
Only after you commit the changes for the outermost transaction does Core Animation begin the associated animations.
很大的多是,嵌套时,内部的事务提交的东西是提给外层的事务,而后一层层提交,最后一层才把数据提交给CoreAnimation系统,系统这时才会获得数据刷新,才会更新layer的画面。
因此位置2的事务虽然提交了,可是它仍是等到隐式事务提交才能起做用。把位置5处代码删掉就能看出区别。
这个没什么可说的,让多个动画一块儿执行,显示出符合效果。值得注意的是:
Spring是弹簧的意思,这个动画就是像弹簧同样摆动的效果。
button.center = CGPointMake(0, 200);
CASpringAnimation *springAnim = [CASpringAnimation animationWithKeyPath:@"position"];
springAnim.toValue = [NSValue valueWithCGPoint:CGPointMake(200, 200)];
springAnim.duration = 10;
springAnim.mass = 10;
springAnim.stiffness = 50;
springAnim.damping = 1;
springAnim.initialVelocity = 0;
springAnim.delegate = self;
[button.layer addAnimation:springAnim forKey:@"spring"];
复制代码
这个类继承自CABasicAnimation
,因此仍是须要keyPath、fromValue、toValue等数据。由于keyPath
存在,因此它不仅是用于物体的运动,还能够是其余的,好比颜色。CASpringAnimation
提供了像弹簧同样的变化规律,而不仅是运动的动画。
而后CASpringAnimation
自身的属性用于计算弹簧的运动模式:
动画时间不影响动画的运行模式,这一点跟其余的动画不同,这里时间到了,物体还在动就会直接掐掉、动画中止。
CALayer
还有一系列的子类,每种layer还有它们本身特有的动画。一样,进文档查看属性的注释,带有Animatable
的是有动画的,配合CABasicAnimation
和CAKeyframeAnimation
使用。
CATextLayer
有两个动画属性,fontSize
和foregroundColor
。
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"fontSize"];
anim.duration = 5;
anim.fromValue = @(10);
anim.toValue = @(30);
CATextLayer *textLayer = [[CATextLayer alloc] init];
textLayer.foregroundColor = [UIColor blackColor].CGColor;
textLayer.string = @"一串字符串";
textLayer.frame = CGRectMake(0, 300, 300, 60);
[textLayer addAnimation:anim forKey:@"text"];
[self.view.layer addSublayer:textLayer];
复制代码
CAShapeLayer
里有许多动画属性,但最神奇的就是strokeStart
和strokeEnd
,特别是两个组合使用的使用简直刷新认知!!!
CAShapeLayer
的图形是靠路径提供的,而strokeStart
和strokeEnd
这两个属性就是用来设定绘制的开始和结束为止。0表明path的开始位置,1表明path的结束为止,好比strokeStart
设为0.5,strokeEnd
设为1,那么layer就只绘制path的后半段。
经过修改这两个属性,就能够达到只绘制path一部分的目的,而后它们还都支持动画,就能够创造出神奇的效果!
-(void)shaperLayerAnimations{
//图形开始位置的动画
CABasicAnimation *startAnim = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
startAnim.duration = 5;
startAnim.fromValue = @(0);
startAnim.toValue = @(0.6);
//图形结束位置的动画
CABasicAnimation *endAnim = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
endAnim.duration = 5;
endAnim.fromValue = @(0.4);
endAnim.toValue = @(1);
//把两个动画合并,绘制的区域就会不断变更
CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
group.animations = @[startAnim, endAnim];
group.duration = 5;
group.autoreverses = YES;
CAShapeLayer *shapeLayer = [[CAShapeLayer alloc] init];
shapeLayer.frame = self.view.bounds;
//图形是一大一小两个圆相切嵌套
UIBezierPath *path = [[UIBezierPath alloc] init];
[path addArcWithCenter:CGPointMake(100, 300) radius:100 startAngle:0 endAngle:M_PI*2 clockwise:YES];
[path addArcWithCenter:CGPointMake(150, 300) radius:50 startAngle:0 endAngle:M_PI*2 clockwise:YES];
shapeLayer.path = [path CGPath];
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor whiteColor].CGColor;
[shapeLayer addAnimation:group forKey:@"runningLine"];
[self.view.layer addSublayer:shapeLayer];
}
复制代码
iOS10有了UIViewPropertyAnimator
,能够控制动画的流程,核心是fractionComplete
这个参数,能够指定动画停留在某个位置。这里用一个pan手势来调整fractionComplete
,实现手指滑动时,动画跟随执行的效果。
这感受有点像,拖动进度条而后电影前进或后退,随意控制进度。
UIViewPropertyAnimator *animator;
-(void)interactiveAnimations{
button.frame = CGRectMake(200, 100, 100, 100);
button.layer.cornerRadius = button.bounds.size.width/2;
button.layer.masksToBounds = YES;
animator = [[UIViewPropertyAnimator alloc] initWithDuration:5 curve:(UIViewAnimationCurveEaseOut) animations:^{
button.transform = CGAffineTransformMakeScale(0.1, 0.1);
}];
[animator startAnimation];
[animator pauseAnimation];
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)];
[self.view addGestureRecognizer:pan];
}
float startFrac;
-(void)panAction:(UIPanGestureRecognizer *)pan{
if (pan.state == UIGestureRecognizerStateChanged) {
[animator pauseAnimation];
float delta = [pan translationInView:self.view].y / self.view.bounds.size.height;
animator.fractionComplete = startFrac+delta;
}else if (pan.state == UIGestureRecognizerStateBegan){
startFrac = animator.fractionComplete;
}else if (pan.state == UIGestureRecognizerStateEnded){
[animator startAnimation];
}
}
复制代码
两种,一个是navigation的push和pop,经过navigationController的delegate提供:
UIViewControllerAnimatedTransitioning
UIViewControllerInteractiveTransitioning
另外一种是VC的present和dismiss,经过VC自身的transitioningDelegate提供:
UIViewControllerAnimatedTransitioning
UIViewControllerInteractiveTransitioning
提供的数据时同样的类型,因此这两种其实逻辑上是同样的。
先看提供动画的UIViewControllerAnimatedTransitioning
,就两个方法:
transitionDuration:
让你提供动画的时间animateTransition:
在这里面执行动画站在设计者的角度来看一下整个流程,这样会帮助对这个框架的理解:
一切从push开始,nav开始push,它会去查看本身的delegate,有没有实现提供转场动画的方法,没有就使用默认的效果,结束。
有,那么就能够拿到实现UIViewControllerAnimatedTransitioning
的对象,而后从这个对象里拿到动画时间,用这个时间去同步处理其余的操做,好比导航栏的动画。 同时调用这个对象的animateTransition:
执行咱们提供的动画。
这个过程了解了,就明白每一个类在这个过程里的意义。由于这些名词都太长,命名也很像,很容易混淆意义。
一个例子:
-(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext{
return _duration;
}
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *fromView = fromVC.view;
UIView *toView = toVC.view;
if (self.type == TransitionTypePush) {
[transitionContext.containerView addSubview:toView];
float scale = 0.7f;
toView.transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(toView.bounds.size.width*(1+1/scale)/2, 0), CGAffineTransformMakeScale(scale, scale));
[UIView animateWithDuration:_duration animations:^{
fromView.transform = CGAffineTransformMakeScale(scale, scale);
toView.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
fromView.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:YES];
}];
}else if (self.type == TransitionTypePop){
[transitionContext.containerView insertSubview:toView belowSubview:fromView];
float scale = 0.7f;
toView.transform = CGAffineTransformMakeScale(scale, scale);
[UIView animateWithDuration:_duration animations:^{
fromView.transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(toView.bounds.size.width*(1+1/scale)/2, 0), CGAffineTransformMakeScale(scale, scale));
toView.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
[fromView removeFromSuperview];
[transitionContext completeTransition:YES];
}];
}
}
复制代码
push时的效果是进来的view,即toView从右边缘一边进来一边放大,直到铺满屏幕;退出的view,即fromView,逐渐缩小。合在一块儿有一种滚筒的感受。pop时就是反操做。
除了动画内容以外,值得注意的是:
toView
须要咱们本身加到containerView
上[transitionContext completeTransition:]
,这个标识这一次的VC切换结束了,不然后面的push、pop等都没效果。