UIView层

Block动画

animationWithDuration

一般是通过block的方式来实现,主要有以下几个函数:

1
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations

这个函数是最简单的动画函数,duration定义了动画的时长,animation block定义了动画的内容,一般是设置动画的结束状态的语句,中间的具体动画过程则是又系统自动填充。

1
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion

这个函数是在上一个动画函数的基础上添加了动画结束时候的回调block函数

1
+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion

这个动画函数是在上一个动画函数的基础上添加了延迟时长delay和一些动画选项UIViewAnimationOptions。UIViewAnimationOptions主要包含四大类:

  • 一是基础属性相关,以UIViewAnimationOptions为前缀,如是否重复执行,是否反向执行等等;

  • 二是动画的运行平缓度相关,以UIViewAnimationOptionsCurve为前缀,主要是EaseInOut、EaseIn、EaseOut和Layer,用于定义动画如何开始和结束;

  • 三是方向相关,用于定义部分带方向动画,以UIViewAnimationOptionTransition为前缀;

  • 四是帧数相关,用于定义动画的帧数,以UIViewAnimationOptionPreferredFramesPerSecond为前缀。

1
+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion

这个动画函数是上一个动画函数的另一种情况,弹簧动画,主要的几个参数如下:

  • (CGFloat)dampingRatio,抑制系数,范围0-1之间,1表示在重点附近完全无摆动,越靠近1则摆动越大;

  • (CGFloat)velocity,相对的初始速度,范围0-1之间,表示属性值在一开始到达终止值的速度,设置为x则表示需要1/x秒可以到达终止值(这并不意味着动画的结束,因为是弹簧动画,物体的属性值还进行往返运动)。

1
+ (void)animateKeyframesWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewKeyframeAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion

关键帧动画,通过定义动画过程中的关键帧,来实现对动画的控制,其主要的两个参数如下:

  • (UIViewKeyframeAnimationOptions)options,主要分为两类:

    • 以UIViewKeyframeAnimationOption为前缀,类似UIViewAnimationOptions中的第一类;

    • 以UIViewKeyframeAnimationOptionCalculationMode为前缀,主要表示的是不同的插值方式;

  • (void (^)(void))animations,用于添加关键帧,有如下函数

1
+ (void)addKeyframeWithRelativeStartTime:(double)frameStartTime relativeDuration:(double)frameDuration animations:(void (^)(void))animations

其中frameStartTime和frameDuration均为0-1之间的值,即为相对于整个duration的值。

transitionFromView

1
+ (void)transitionFromView:(UIView *)fromView toView:(UIView *)toView duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options completion:(void (^ __nullable)(BOOL finished))completion;

此处显式指定了fromView和toView

1
+ (void)transitionWithView:(UIView *)view duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion;

此处指定的view为containerView,即所做动画均为对这个containerView所做,动画效果都是以整个containerView作为基础进行的。

Layer层

CATransaction事务类

CATransaction类是Core Animation类中的事务类,在iOS中的图层中,图层的每个改变都是事务的一部分,CATransaction可以对多个layer的属性同时进行修改,同时负责成批的把多个图层树的修改作为一个原子更新到渲染树。

CATransaction类分为隐式事务和显式事务,对隐式动画的修改只能通过CATransaction,而显式动画可以通过CATransaction也可以通过其他方式修改。

隐式事务

所有对layer的属性的修改都会触发隐式动画,固定时长为0.25s,如果需要关闭隐式动画则需要调用如下函数:

1
[CATransaction setDisableActions:YES];

显式事务

一般有如下结构:

1
2
3
4
5
6
7
8
9
10
// 开始动画
[CATransaction begin];
// 设定animation timing function
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
// 设定动画的duration
[CATransaction setAnimationDuration:5.f];
// 具体要修改的属性值
self.shapeLayer.strokeEnd = 1.f - self.shapeLayer.strokeEnd;
// 提交动画
[CATransaction commit];

此处在定义显式动画的同时会覆盖掉隐式动画,因此不需要手动关闭隐式动画。

⚠️UIView层的动画只能对指定view的rootLayer做动画,即

1
2
3
4
5
6
7
8
9
10
CAShapeLayer *shapeLayer = [[CAShapeLayer alloc] init];
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.strokeColor = [UIColor blueColor].CGColor;
shapeLayer.lineWidth = 15.f;
shapeLayer.strokeEnd = 0.f;
shapeLayer.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(200, 200) radius:50 startAngle:-M_PI / 2 endAngle:M_PI * 1.5f clockwise:YES].CGPath;
[self.view.layer addSublayer:shapeLayer];
[UIView animateWithDuration:5.f animations:^{
shapeLayer.strokeEnd = 1.f;
}];

该代码是无效的,其实际产生的效果是这个shapeLayer的隐式动画,也就是一个时长为0.25s的动画,而并非是5s。要实现5s长的动画,就需要采用Layer层的动画,即:

1
2
3
4
5
6
7
8
9
10
11
[CATransaction setDisableActions:YES]; // 取消layer的隐式动画
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
animation.fromValue = @(self.shapeLayer.strokeEnd);
animation.toValue = @(1.f - self.shapeLayer.strokeEnd);
animation.duration = 5.f;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[shapeLayer addAnimation:animation forKey:nil];
shapeLayer.strokeEnd = 1.f - self.shapeLayer.strokeEnd;
// 由于上面取消了隐式动画,这里的属性值的修改是不会有动画效果的,否则会有动画效果

或者使用CATransaction来实现,即:

1
2
3
4
5
[CATransaction begin];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[CATransaction setAnimationDuration:5.f];
shapeLayer.strokeEnd = 1.f - self.shapeLayer.strokeEnd;
[CATransaction commit];

这种方式则不需要关闭隐式动画。

CAAnimation类

主要实现类是CAAnimation及其子类,以及一个所有动画类都遵循的协议CAMediaTiming。

Layer层动画的添加方式:

1
- (void)addAnimation:(CAAnimation *)anim forKey:(nullable NSString *)key;

其中key表示了要变化的属性的名字,例如:

1
[self.curView.layer addAnimation:animation forKey:nil];

CAMediaTiming

所有动画类都需要实现的协议;

CAAnimation

动画基础类,不可直接使用;

CAPropertyAnimation

动画基础类,不可直接使用;

CABasicAnimation

基础属性动画,对keyPath所制定的属性进行变化,主要有以下几个函数和属性:

1
2
3
4
5
@property(nullable, strong) id fromValue; // 初始值
@property(nullable, strong) id toValue; // 终止值
@property(nullable, strong) id byValue; // 变化值
@property CFTimeInterval duration; // 动画时间
+ (instancetype)animationWithKeyPath:(nullable NSString *)path; // 构造函数

有两个属性比较关键,可以用于设置动画结束之后是否需要回到初始态:

1
2
3
// 不回到初始状态
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;

这里就要涉及到CALayer的两个子layer对象,persentationLayer和modelLayer,前者负责显示出来的位置,后者负责实际的位置。即使我们在这里设置了动画结束之后不会到初始态,我们也只是将presentationLayer的值设置到了终止态,modelLayer的值还是原来的值,也就是此时通过类似layer.backgroundColor这样的get方法获得到的值是不会变的,要改变这个值则还需要显式的调用set方法(或者点调用)。

例如以下代码,实现了边框的宽度从90变到0的效果:

1
2
3
4
5
6
7
8
9
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"borderWidth"];
animation.fromValue = @(90);
animation.toValue = @(0);
animation.duration = 5.f;
[animation setAutoreverses:YES];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
[self.view.layer addAnimation:animation forKey:nil];

这段动画结束后,虽然显示出来的边框宽度是90,但是实际上self.view.layer.borderWidth获取到的值还是0.f。

CASpringAnimation

弹簧动画,其主要就是在CABasicAnimation的基础上增加了damping和initialVelocity这两个弹簧相关的对象;

CAKeyframeAnimation

关键帧动画,通过定义关键帧或者路径来实现动画的控制

关键帧

主要的参数如下

1
2
@property(nullable, copy) NSArray *values;          // 关键帧的值数组 
@property(nullable, copy) NSArray<NSNumber *> *keyTimes; // 关键帧的时间 @property(copy) CAAnimationCalculationMode calculationMode; // 计算间隔的方式

#####路径

主要的参数如下

1
2
@property(nullable) CGPathRef path; 
// 一个UIBezierPath类型的变量,表示动画运行的path

UIBezierPath主要是通过点来确定路径,同时我们可以定义点和点之间的路径类型,主要的几个函数如下:

1
2
3
4
5
6
(void)moveToPoint:(CGPoint)point;       // 设置初始节点的位置
(void)addLineToPoint:(CGPoint)point;
(void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;
(void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint;
(void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
(void)closePath; // path结束,会自动将结束节点和初始节点连起来

例如下面的代码可以实现一个在固定路径上的关键帧动画:

1
2
3
4
5
6
7
8
9
10
11
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
UIBezierPath *path = [[UIBezierPath alloc] init];
[path moveToPoint:CGPointMake(self.caKeyframeAnimationView.frame.origin.x + self.caKeyframeAnimationView.frame.size.width / 2, self.caKeyframeAnimationView.frame.origin.y + self.caKeyframeAnimationView.frame.size.height / 2)];
[path addLineToPoint:CGPointMake(self.caKeyframeAnimationView.frame.origin.x + self.caKeyframeAnimationView.frame.size.width / 2 + 30, self.caKeyframeAnimationView.frame.origin.y + self.caKeyframeAnimationView.frame.size.height / 2)];
[path addLineToPoint:CGPointMake(self.caKeyframeAnimationView.frame.origin.x + self.caKeyframeAnimationView.frame.size.width / 2 - 30, self.caKeyframeAnimationView.frame.origin.y + self.caKeyframeAnimationView.frame.size.height / 2)];
[path closePath];
animation.path = path.CGPath;
animation.duration = 1.f;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
[self.caKeyframeAnimationView.layer addAnimation:animation forKey:nil];

CAAnimationGroup

组合动画,可以将多个动画组合起来,通过如下设置即可:

1
2
// animationArr是一个由CAAnimation的子类构成的数组  
animationGroup.animations = animationArr;

注意此处该数组内的动画都是并发执行的,所以如果希望串行执行的话就需要通过设置这个动画组内的动画的beginTime和duration来实现。

CATransition

转场动画类

1
2
3
4
5
6
7
8
9
10
11
CATransition *animation = [[CATransition alloc] init];
animation.type = kCATransitionFade; // 设置转场效果
animation.subtype = kCATransitionFromTop; // 设置转场动画的效果
animation.duration = 5.f;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
// 跳转方式1 - 模态跳转
[self.view.layer addAnimation:animation forKey:nil];
[self presentViewController:VC animated:NO completion:nil]
// 跳转方式2 - Nav导航跳转
[self.navigationController.view.layer addAnimation:animation forKey:nil];
[self.navigationController pushViewController:VC animated:NO];

UIViewController的自定义转场动画

设两个UIViewController类分别为FirstViewController和SecondViewController,对应的实例分别为firstVC和secondVC,且显示顺序为firstVC->secondVC,要实现从firstVC到secondVC的显示动画和secondVC到firstVC的取消动画,需要以下几步:

  1. 实现一个遵循UIViewControllerAnimatedTransitioning的动画类AnimationController,并定义当前的TransitionType
1
2
3
4
5
6
7
8
9
typedef NS_ENUM(NSUInteger, TransitionType) {
TransitionTypePresent = 0, // present和dismiss为模态弹出
TransitionTypeDismiss,
TransitionTypePush, // push和pop为nav弹出
TransitionTypePop
};
@interface AnimationController : NSObject <UIViewControllerAnimatedTransitioning>
@property (nonatomic, assign) TransitionType type;
@end
  1. 在AnimationController类中实现如下两个方法:
1
2
3
4
// 动画的duration
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
// 动画的具体实现
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
  1. duration的实现很简单,直接返回一个float即可;

  2. 动画的具体实现的主要流程如下:

  • 首先根据type判断当前的操作具体是什么

  • 对于push或者present,首先定义fromVC和toVC(注意这里在pop和dismiss的时候会产生fromVC和toVC的互换)

1
2
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; 
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
  • 获取当前界面的截图视图snapshot,作为后续动画的实际操作view
1
UIView *snapshot = [fromVC.view snapshotViewAfterScreenUpdates:NO];
  • 获取containerView作为存放整个过程的容器
1
UIView *containerView = [transitionContext containerView];
  • 将fromVC设为hidden,并分别将toVC.view和snapshot加入到containerView中
1
2
3
[fromVC.view setHidden:YES]; 
[containerView addSubview:toVC.view];
[containerView addSubview:snapshot];
  • 设定snapshot的初始设置和位置;

  • 写动画

1
2
3
4
5
6
7
8
9
10
11
12
NSTimeInterval duration = [self transitionDuration:transitionContext]; 
[UIView animateWithDuration:duration animations:^{
// 这里也可以是keyframeanimation或者其他类型的block animation(见第一部分)
} completion:^(BOOL finished) {
// 设置transition结果
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
// 如果transition失败,则将snapshot移除(因为下一次present或者push还会创建snapshot),并将fromVC显示出来
if ([transitionContext transitionWasCancelled]) {
[snapshot removeFromSuperview];
[fromVC.view setHidden:NO];
}
}];
  • 对于pop或者dismiss,首先定义fromVC和toVC(注意这里在pop和dismiss的时候会产生fromVC和toVC的互换)
1
2
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; 
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
  • 获取containerView作为存放整个过程的容器
1
UIView *containerView = [transitionContext containerView];
  • 获取present和push中的截图视图snapshot,此时snapshot应当为containerView的subviews中的最后一个view
1
UIView *snapshot = containerView.subviews.lastObject;
  • 在containerView中添加toVC.view
1
[containerView addSubview:toVC.view];
  • 设定snapshot的初始设置和位置;

  • 写动画

1
2
3
4
5
6
7
8
9
10
11
12
13
NSTimeInterval duration = [self transitionDuration:transitionContext]; 
[UIView animateWithDuration:duration animations:^{
// 这里也可以是keyframeanimation或者其他类型的block animation(见第一部分)
} completion:^(BOOL finished) {
// 设置transition结果
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
// 如果transition失败,则什么都不做
// 如果transition成功,则将toVC设为hidden,并移除snapshot
if (!transitionContext.transitionWasCancelled) {
[toVC.view setHidden:NO];
[snapshot removeFromSuperview];
}
}];
  1. 对于present和dismiss,在FirstViewController中遵循UIViewControllerTransitioningDelegate,并实现animationControllerForPresentedController函数和animationControllerForDismissedController函数,返回转场动画的实例
1
2
3
4
5
6
7
8
9
10
11
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
// 此处可以根据presented和presenting来进行判断需要返回哪一个转场动画类
return [[AnimationController alloc] initWithType:TransitionTypePresent];
}
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
SecondViewController *exampleVC = (SecondViewController *)dismissed;
if (exampleVC == nil) return nil;
return [[AnimationController alloc] initWithType:TransitionTypeDismiss];
}
  1. 对于push和pop,则需要在FirstViewController中遵循UINavigationControllerDelegate,并实现如下函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
{
if (operation == UINavigationControllerOperationPush) {
if ([fromVC isKindOfClass:FirstImageViewController.class]) {
FirstViewController *fiVC = (FirstViewController *)fromVC;
return [[AnimationController alloc] initWithType:TransitionTypePush];
}
}
else if (operation == UINavigationControllerOperationPop){
if ([fromVC isKindOfClass:SecondImageViewController.class]) {
SecondViewController *siVC = (SecondViewController *)fromVC;
return [[AnimationController alloc] initWithType:TransitionTypePop];
}
}
return nil;
}