iOS页面转场动画

前言

  之前一直对容器转场这一块很模糊,因为这里面涉及到好几个协议的实现,而且名字比较长.读来总是不想仔细去看协议的名称.对于视图切换经常都是使用iOS系统内部自带的present-dismiss动画,或者导航栏的push和pop动画.虽然这部分在我们公司目前的app内也是用的比较多的,但是我感觉在注重用户体验后,这部分也是会进一步优化的.正好前段时间看到了一些关于场景切换的博客,就正好学习一下,并且写一个小demo记录下.方便后面前来查找.

iOS转场动画分类

  iOS页面转场主要分为三种,uiviewcontroller页面转场,uinavigationviewcontroller页面转场,uitabbarcontroller页面转场,本文主要介绍前两种转场方式.其中对于uiviewcontroller的页面转场是通过present和dismiss的方式进行的页面切换.uinavigationcontroller的页面转场是通过navigtioncontroller管理的子vc的栈进行的push和pop切换.对于系统提供的页面切换,可以使用系统提供的API进行.

1
2
3
4
5
6
7
8
9
/// 模态弹出的方法
- (void)presentViewController:(UIViewController *)viewControllerToPresent animated: (BOOL)flag completion:(void (^ __nullable)(void))completion;

- (void)dismissViewControllerAnimated: (BOOL)flag completion: (void (^ __nullable)(void))completion;

/// 导航切换的方法
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated;

- (nullable UIViewController *)popViewControllerAnimated:(BOOL)animated;

  上面的后两个方法是通过导航栏控制调用的,如果要使用导航栏动画,需要设置当前页面容器的根视图是导航栏控制器.uinavigationcontroller里面有很多属性都是日常开发中经常会用到的,可以结合这篇设计透明导航栏的文章参考下.

  如果想要自定义场景切换动画,需要调用iOS提供的一些视图动画协议.我在demo中实现了如下几种效果的页面场景切换

catagray

present-dismiss转场

UIViewControllerContextTransitioning && UIViewControllerAnimatedTransitioning

   在开始前先熟悉下几个关于自定义转场的协议类

  • UIViewControllerContextTransitioning ,这个协议提供了视图切换上下文,在开发过程中一般不需要直接继承这个协议.在使用vc进行场景切换时,可以使用这个协议对象取得动画前后的视图容器.只看这个协议会觉得模糊不清,结合下一个协议一起来看会比较清楚.
  • UIViewControllerAnimatedTransitioning ,这个协议类中定义了动画执行的主要方法.其中包含了两个动画相关的主要方法
1
2
3
4
/// 返回动画执行的时长
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
/// 定义需要执行的动画
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;

  可以看到参数是实现了UIViewControllerContextTransitioning协议的参数.通过这个参数可以获取到视图和容器相关的一些参数

1
2
3
4
5
6
7
8
9
10
/// 获取转换前后的视图容器
- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;
/// 需要展示的视图需要添加到containerView中
@property(nonatomic, readonly) UIView *containerView;
/// 某个VC的初始位置,可以用来做动画的计算
-(CGRect)initialFrameForViewController:(UIViewController *)vc;
///与上面的方法对应,得到切换结束时某个VC应在的frame。
-(CGRect)finalFrameForViewController:(UIViewController *)vc;
/// 在动画完成后调用
-(void)completeTransition:(BOOL)didComplete;

  有了以上的动画方法,取到动画切换前后的视图,就可以定义一些简单的动画效果.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
/// 1、获取转换vc
UIViewController *toVc = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
/// 获取视图大小
CGRect finalFrame = [transitionContext finalFrameForViewController:toVc];
CGFloat oriY = [UIScreen mainScreen].bounds.size.height;
CGFloat oriX = [UIScreen mainScreen].bounds.size.width;
switch (self.type) {
case 0:
{
/// 动画视图初始位置
toVc.view.frame = CGRectOffset(finalFrame, -oriX,-oriY);
}
break;
case 1:
{
/// 动画视图初始位置
toVc.view.frame = CGRectOffset(finalFrame, 0, oriY);
}
break;
case 2:
{
return;
}
break;
default:
break;
}
/// 视图添加到containerView,不添加则展示不了
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toVc.view];

/// animation
/// usingSpringWithDamping 0-1 值越小,则弹性阻尼效果越明显
/// initialSpringVelocity 初始速度,值越大,动画效果越快
[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0 usingSpringWithDamping:0.6 initialSpringVelocity:1 options:UIViewAnimationOptionCurveLinear animations:^{
toVc.view.frame = finalFrame;
} completion:^(BOOL finished) {
/// 标记动画全部完成
[transitionContext completeTransition:YES];
}];
}

  上面自定义动画的过程中

  • 使用context取到目的视图的容器,(viewControllerForKey中的key可以取值fromVC和toVC)
  • 确定动画视图的初始位置,上图中展示了动画从下到上(1)和从左上到右下(0)的效果.
  • 将视图添加到containerView中准备进行动画.
  • 执行动画
  • 标记动画是否完成

UIViewControllerTransitioningDelegate

  有了动画实现类,只需要找到调用方即可,比如A容器推到B容器,那就可以考虑让A容器在调用动画实现.因此考虑让A容器作为代理实现动画调用过程的协议方法.

1
2
3
4
/// present动画调用方法,
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
/// dismiss动画调用方法
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

  在present动画执行时返回前面自定义的动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void) viewDidLoad {
/// 自定义present动画
_transition = [ViewControllerAnimatedTransitioning new];
/// 自定义dismiss动画
_dismisstransition = [ViewControllerDismisstransition new];
}
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
return self.transition;
}

/// dismissvc
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
return self.dismisstransition;
}

  自定义的通过手势滑动关闭页面的动画可以在文末参考demo实现. 通过上面的方法就可以实现一个自定义的页面场景切换动画.整个过程大致如下.

  • 自定一个类继承 UIViewControllerAnimatedTransitioning 协议,并实现其中的自定义动画方法和动画执行时长方法.
  • 在需要执行动画的VC中继承 UIViewControllerTransitioningDelegate,设置要执行动画的代理尾当前容器vc.transitioningDelegate = self;
  • 实现UIViewControllerTransitioningDelegate中的协议方法,在协议方法中返回自定义的动画效果
  • 调用控制器的presentViewController方法.系统在调用和这个方法后,如果有自定义动画,会走自定义动画,没有的话就走系统的present动画

  如果需要实现一些更加炫酷的动画效果,可以自行了解iOS动画机制.

push-pop动画

  push和pop的自定义动画实现过程非常类似.也有三个相关的协议类,只是最后在调用动画实现api时需要实现UINavigationControllerDelegate这个协议中的动画方法.需要注意的是,使用导航类动画的话需要保证当前所在的容器是包含导航栏的.因为在进行push-pop动画时其本质都是通过导航栏管理一个容器堆栈.下面实现一个自定的导航栏动画效果.

  • 自定义一个动画类,继承UIViewControllerAnimatedTransitioning,重新协议方法.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 0.8;
}

///CGAffineTransformMakeScale 视图按比例放缩
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
/// 此方法在push和pop时都会执行
UIViewController *fromVc = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVc = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *fromView = fromVc.view;
// CGRect screenFrame = fromView.frame;
UIView *toView = toVc.view;
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toView];
if (self.type == 4) {
[containerView bringSubviewToFront:fromView];
// [containerView bringSubviewToFront:toView];
// toView.transform = CGAffineTransformMakeScale(0.2, 0.2);
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromView.alpha = 0;
fromView.transform = CGAffineTransformMakeScale(0.2, 0.2);
// toView.transform = CGAffineTransformMakeScale(1, 1);
toView.alpha = 1;
} completion:^(BOOL finished) {
fromView.transform = CGAffineTransformMakeScale(1, 1);
[transitionContext completeTransition:YES];
}];
} else if (self.type == 5) {

toView.transform = CGAffineTransformMakeScale(0.2, 0.2);
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toView.transform = CGAffineTransformMakeScale(1, 1);
// toView.frame = screenFrame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];

}

}
  • 在需要实现动画的控制器中实现 UINavigationControllerDelegate,并将导航控制器的代理设置为容器本身.

    1
    2
    /// 如果在这一步将代理设置为被push的容器,那么自定义动画只会在pop返回时生效.   pushVc.navigationController.delegate = pushVc;
    self.navigationController.delegate = self;
  • 实现UINavigationControllerDelegate协议中的动画调用方法.返回自定义动画

1
2
3
4
5
6
7
8
9
- (void)viewDidLoad {
_naviTransition = [NavigationControllerTransitioning new];
}

/// navigation animation
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
{
return self.naviTransition;
}

   和present动画另一个不同的点是,此处的动画是push和pop都会调用的.如果想设置push和pop不同的效果可以判断何时是push,何时是pop来进行区分.自定义的push动画效果过程大致如下:

  • 自定一个类继承 UIViewControllerAnimatedTransitioning 协议,并实现其中的自定义动画方法和动画执行时长方法.
  • 在需要执行动画的VC中继承 UINavigationControllerDelegate,设置要执行动画的代理尾当前容器self.navigationController.delegate = self;
  • 实现UINavigationControllerDelegate中的协议方法,在协议方法中返回自定义的动画效果
  • 调用navigationcontroller的pushViewController:方法,触发动画,如果有自定义动画则走自定义动画,否则走系统自带动画效果.

参考文章

iOS7中的ViewController切换

ViewController自定义转场-基础

iOS导航控制器——UINavigationController使用详解

demo地址
demo里面比较杂,放的东西比较多.

之后了解下如何截gif图,尝试在博客中也添加一些gif图增加效果.