Erlo

iOS:探究视图控制器的转场动画

时间:2019-11-09 19:30   阅读:94次   来源:博客园
页面报错
点赞

一、介绍

在iOS开发中,转场动画的使用无处不见,不只是我们自己更多的使用UIViewblock动画实现一个转场动画,其实,在我们实现VC控制器跳转的时候都是转场动画的实现,例如标签栏控制器的切换、模态动画present和dismiss、导航控制器的push和pop。实现它们的转场动画,只需要实现它们的动画协议即可,说起来有点太笼统,不如看下面的图吧:

 

二、分析

对于上面的三种类型的控制器,系统都会为它们设置一个代理,通过这个代理方法去监测它们切换VC的过程,这个过程仅仅是出现和消失的过程,至于这个过程是什么过渡效果,这个代理是不管的。要想这个过程是有动画的,那么在这些过程中,也就是代理函数中,需要另外再返回一个实现动画的对象,这个对象必须遵循实现动画的协议,在这个协议中开发者可以重写自定义转场动画。下面会慢慢演示这三种类型控制器的自定义转场动画。

重写不可交互转场动画的核心协议内容:

//重写动画协议
@protocol UIViewControllerAnimatedTransitioning <NSObject>

//动画执行时间
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;

//自定义动画效果
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;

@end

重写可交互转场动画的核心协议内容:

//重写动画协议
@protocol UIViewControllerInteractiveTransitioning <NSObject>

//自定义动画效果
- (void)startInteractiveTransition:(id <UIViewControllerContextTransitioning>)transitionContext;

@end

系统提供的一个百分比可交互转场动画核心类内容:

//系统提供的百分比动画类,已经遵循了可交互协议
@interface UIPercentDrivenInteractiveTransition : NSObject <UIViewControllerInteractiveTransitioning>
- (void)pauseInteractiveTransition; - (void)updateInteractiveTransition:(CGFloat)percentComplete; - (void)cancelInteractiveTransition; - (void)finishInteractiveTransition; @end

 

三、转场动画View之间的切换

 

四、实现一个自定义的模态动画

1、概述

正如我们所知,系统为我们提供的模态动画默认是从底部present出,然后dismiss回到底部。 虽然说这个基本能够满足使用,但是如果我们还想使用其他形式的模态动画例如从顶部present出dismiss回到顶部,这个时候就需要对系统默认的转场动画进行自定义了。

2、详解

(1)要自定义模态转场动画,首先需要给被模态的控制器设置一个实现了UIViewControllerAnimatedTransitioning协议的代理,这些协议方法可以监测动画执行的过程,代理和协议如下:

//代理
@protocol UIViewControllerTransitioningDelegate;
@interface UIViewController(UIViewControllerTransitioning)
@property (nullable, nonatomic, weak) id <UIViewControllerTransitioningDelegate> transitioningDelegate API_AVAILABLE(ios(7.0));
@end

//协议
@protocol
UIViewControllerTransitioningDelegate <NSObject> @optional //present时调用,返回一个实现了不可交互转场动画协议的代理 - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source; //dismiss时调用,返回一个实现了不可交互转场动画协议的代理 - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed; //presnt过程中交互时调用,返回一个实现了可交互的转场动画协议的代理 - (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator; //dismiss过程中交互时调用,返回一个实现了可交互的转场动画协议的代理 - (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator; //返回新的模态弹框控制器(这个是对模态风格进行自定义时调用,后面会说到) - (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source API_AVAILABLE(ios(8.0)); @end

(2)然后在上面的协议方法中返回一个实现了UIViewControllerAnimatedTransitioning协议的代理,在这个代理的协议方法中可以真正重写转场动画了,协议如下:

@protocol UIViewControllerAnimatedTransitioning <NSObject>
//动画执行时间
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
//自定义转场动画
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
@optional

(3)自定义转场动画实现如下【注意:Singleton单例类和UIView+Extesion分类需要自己去拷贝引入】

  • 设置UIViewControllerAnimatedTransitioning代理对象TransitionDelegate,监测动画执行过程,将其设置为单例

    #import <UIKit/UIKit.h>
    #import "Singleton.h"
    @interface TransitionDelegate : NSObject<UIViewControllerTransitioningDelegate>
    SingletonH(TransitionDelegate);
    @end

    #import "TransitionDelegate.h"
    #import "CustomAnimationTransition.h"
    
    @implementation TransitionDelegate
    SingletonM(TransitionDelegate);
    
    #pragma mark - <UIViewControllerTransitioningDelegate>
    
    //展示的动画
    - (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
    {
        CustomAnimationTransition *animation = [[CustomAnimationTransition alloc]init];
        animation.presented = YES;
        return animation;
    }
    
    //关闭的动画
    - (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
    {
        CustomAnimationTransition *animation = [[CustomAnimationTransition alloc]init];
        animation.presented = NO;
        return animation;
    }
    @end

  • 设置UIViewControllerAnimatedTransitioning代理对象CustomTransitionAnimationTransition,重写动画效果

    #import <UIKit/UIKit.h>
    
    @interface CustomAnimationTransition : NSObject<UIViewControllerAnimatedTransitioning>
    //判断是present还是dismiss, YES:present  NO:dismisss
    @property (assign,nonatomic)BOOL presented;
    @end

  • //设置过渡动画(modal和dismiss的动画都需要在这里处理)
    - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
    {
        // UITransitionContextToViewKey,
        // UITransitionContextFromViewKey.
        
        //出来的动画
        if (self.presented) {
            
            //获取并添加转场视图
            UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
            [transitionContext.containerView addSubview:toView];
             
            //设置动画从上往下出来
            toView.y = -toView.height;
            
            [UIView animateWithDuration:duration animations:^{
                
                toView.y = 0;
                
            } completion:^(BOOL finished) {
                
                //移除视图
                BOOL cancle = [transitionContext transitionWasCancelled];
                if (cancle) {
                    [toView removeFromSuperview];
                }
                
                //动画完成后,视图上的事件才能处理
                [transitionContext completeTransition:!cancle];
            }];
        }
        //销毁的动画
        else
        {
            //获取转场视图
            UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
            
            [UIView animateWithDuration:duration animations:^{
                
                fromView.y = -fromView.height;
    
            } completion:^(BOOL finished) {
                
                //移除视图
                BOOL cancle = [transitionContext transitionWasCancelled];
                if (!cancle) {
                    [fromView removeFromSuperview];
                }
                
                //动画完成后,视图上的事件才能处理
                [transitionContext completeTransition:!cancle];
            }];
        }
    }

  • 开始执行,结果如gif图 

    //present
    UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:[[SecondViewController alloc] init]];
    nav.transitioningDelegate = [TransitionDelegate sharedTransitionDelegate];//自定义转场动画
    nav.modalPresentationStyle = UIModalPresentationFullScreen;
    [self presentViewController:nav animated:YES completion:nil];

 (4)我们已经实现了一个简单的自定义模态不可交互的转场动画,其实,在模态控制器的时候,我们还可以自定义可交互的转场动画以及设置自定义的模态风格。可交互的转场动画一会儿再讨论,先来讨论一下模态风格,系统在iOS13之前默认都是满屏模式的UIModalPresentationFullScreen,但是iOS13之后,默认是UIModalPresentationPageSheet。系统提供的模态风格如下:

//模态风格枚举
typedef NS_ENUM(NSInteger, UIModalPresentationStyle) {
    UIModalPresentationFullScreen = 0,
    UIModalPresentationPageSheet ,
    UIModalPresentationFormSheet ,
    UIModalPresentationCurrentContext ,
    UIModalPresentationCustom , //自定义
    UIModalPresentationOverFullScreen ,
    UIModalPresentationOverCurrentContext ),
    UIModalPresentationPopover ,
    UIModalPresentationBlurOverFullScreen ,
    UIModalPresentationNone,
    UIModalPresentationAutomatic , 
};

 (5)从上面的枚举可以看到,系统是支持我们实现自己的风格的,也就是自定义。在实现自定义之前,一定得知道UIPresentationController这个类,这个是弹出框控件,模态的控制器都是由它进行管理,主要代码如下:

//重写此方法可以在弹框即将显示时执行所需要的操作
- (void)presentationTransitionWillBegin;
//重写此方法可以在弹框显示完毕时执行所需要的操作 - (void)presentationTransitionDidEnd:(BOOL)completed;
//重写此方法可以在弹框即将消失时执行所需要的操作 - (void)dismissalTransitionWillBegin;
//重写此方法可以在弹框消失之后执行所需要的操作 - (void)dismissalTransitionDidEnd:(BOOL)completed;
//重写决定了弹出框的frame - (CGRect)frameOfPresentedViewInContainerView;
//重写对containerView进行布局 - (void)containerViewWillLayoutSubviews; - (void)containerViewDidLayoutSubviews;
//初始化方法 - (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(UIViewController *)presentingViewController;

(6)额外再提一个知识点,因为一会儿在自定义模态风格时会涉及到。在本文开篇结构图中介绍了创建的转场动画都是在转场动画上下文UIViewControllerContextTransitioning协议中完成的,那么这个转场动画的执行是谁管理呢?看结构图如下,没错,是由UIViewControllerTransitionCoordinator这个代理协调器在协调器上下文中完成的,系统给UIViewController提供了一个分类,这个分类持有这个代理协调器,通过这个代理协调器可以拿到执行转场动画的方法。最终,我们可以自己添加一些操作与转场动画同步执行。

 

UIViewControllerContextTransitioning协议核心内容

@protocol UIViewControllerTransitionCoordinatorContext <NSObject>
// 执行的属性
@property(nonatomic, readonly, getter=isAnimated) BOOL animated;
@property(nonatomic, readonly) UIModalPresentationStyle presentationStyle;
@property(nonatomic, readonly) NSTimeInterval transitionDuration;
@property(nonatomic, readonly) UIView *containerView;
@property(nonatomic, readonly) CGAffineTransform targetTransform 

// 参与控制器
// UITransitionContextToViewControllerKey、UITransitionContextFromViewControllerKey
- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;

// 参与的视图
// UITransitionContextToViewKey、UITransitionContextFromViewKey
- (nullable __kindof UIView *)viewForKey:(UITransitionContextViewKey)key API_AVAILABLE(ios(8.0));
@end

UIViewControllerTransitionCoordinator协议核心内容

// 与动画控制器中的转场动画同步,执行其他动画
- (BOOL)animateAlongsideTransition:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))animation
                        completion:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))completion;
 
// 与动画控制器中的转场动画同步,在指定的视图内执行动画
- (BOOL)animateAlongsideTransitionInView:(nullable UIView *)view
                               animation:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))animation
                              completion:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))completion;

UIViewController(UIViewControllerTransitionCoordinator) 分类核心内容

//持有转场动画执行协调器
@interface UIViewController(UIViewControllerTransitionCoordinator)
@property(nonatomic, readonly, nullable) id <UIViewControllerTransitionCoordinator> transitionCoordinator;
@end

(7)自定义模态风格实现如下【注意:Singleton单例类和UIView+Extesion分类需要自己去拷贝引入】

  • 设置UIViewControllerAnimatedTransitioning代理对象TransitionDelegate,监测动画执行过程并返回模态风格,将其设置为单例

    #import <UIKit/UIKit.h>
    #import "Singleton.h"
    
    @interface TransitionDelegate : NSObject<UIViewControllerTransitioningDelegate>
    SingletonH(TransitionDelegate);
    @end

    #import "TransitionDelegate.h"
    #import "CustomPresentationController.h"
    #import "CustomAnimationTransition.h"
    
    @implementation TransitionDelegate
    SingletonM(TransitionDelegate);
    
    #pragma mark - <UIViewControllerTransitioningDelegate>
    //返回模态风格
    -(UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source
    {
        return [[CustomPresentationController alloc] initWithPresentedViewController:presented presentingViewController:presenting];
    }
    
    //展示的动画
    - (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
    {
        CustomAnimationTransition *animation = [[CustomAnimationTransition alloc]init];
        animation.presented = YES;
        return animation;
    }
    
    //关闭的动画
    - (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
    {
        CustomAnimationTransition *animation = [[CustomAnimationTransition alloc]init];
        animation.presented = NO;
        return animation;
    }
    @end

  • 设置UIViewControllerAnimatedTransitioning代理对象CustomTransitionAnimationTransition,重写动画效果

    #import <UIKit/UIKit.h>
    
    @interface CustomAnimationTransition : NSObject<UIViewControllerAnimatedTransitioning>
    //判断是present还是dismiss, YES:present  NO:dismisss
    @property (assign,nonatomic)BOOL presented;
    @end

    #import "CustomAnimationTransition.h"
    #import "UIView+Extension.h"
    
    const CGFloat duration = 0.5f;
    
    @implementation CustomAnimationTransition
    
    #pragma mark -<UIViewControllerAnimatedTransitioning>
    //动画时间
    - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
    {
        return duration;
    }
    
    //设置过渡动画(modal和dismiss的动画都需要在这里处理)
    - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
    {
        // UITransitionContextToViewKey,
        // UITransitionContextFromViewKey.    
        
       //返现此处并没有添加toView到containerView中以及从containerView中移除toView,与上面的有区别。
    //我把添加和移除toView的操作放到了下面的自定义的模态风格类中完成的

    //
    出来的动画 if (self.presented) { UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey]; //设置动画从上往下出来 toView.y = -toView.height; [UIView animateWithDuration:duration animations:^{ toView.y = 0; } completion:^(BOOL finished) { //动画完成后,视图上的事件才能处理 [transitionContext completeTransition:YES]; }]; } //销毁的动画 else { [UIView animateWithDuration:duration animations:^{ UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey]; fromView.y = -fromView.height; } completion:^(BOOL finished) { //动画完成后,视图上的事件才能处理 [transitionContext completeTransition:YES]; }]; } }

  • 设置自定义的模态风格类CustomPresenttationController

    #import "CustomPresentationController.h"
    
    @implementation CustomPresentationController
    
    //可以改变被模态的控制器视图的尺寸
    - (CGRect)frameOfPresentedViewInContainerView
    {
       //CGRectInset: 在containerView的frame基础上,将width减小100,将height减小200
    //containerView是容纳presentedView的一个容器 return CGRectInset(self.containerView.bounds, 50, 100); } //将上面重置的frame完成布局 - (void)containerViewDidLayoutSubviews { self.presentedView.frame = self.frameOfPresentedViewInContainerView; [super containerViewDidLayoutSubviews]; } //过渡即将展示时的处理 //这个过程可以改变视图属性、或者添加视图等 - (void)presentationTransitionWillBegin { self.presentedView.frame = self.containerView.frame; [self.containerView addSubview:self.presentedView]; } //过渡展示完成 //做清理工作 - (void)presentationTransitionDidEnd:(BOOL)completed { if (!completed) { [self.presentedView removeFromSuperview]; } } //过渡即将消失时的处理 //这个过程可以改变视图属性等 - (void)dismissalTransitionWillBegin { //例如改变透明度,与转场控制器中的转场动画同步执行 [self.presentingViewController.transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) { self.presentedView.alpha = 0.f; } completion:nil]; } //过渡消失完成 //做清理工作 - (void)dismissalTransitionDidEnd:(BOOL)completed { if (completed) { [self.presentedView removeFromSuperview]; } } @end

  • 开始执行,结果如gif图

    //present
    UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:[[SecondViewController alloc] init]];
    nav.transitioningDelegate = [TransitionDelegate sharedTransitionDelegate];//自定义转场动画
    nav.modalPresentationStyle = UIModalPresentationCustom; //自定义模态风格
    [self presentViewController:nav animated:YES completion:nil];

 (8)自定义模态转场动画和自定义模态风格我们都实现完了,但是上面的动画过程中都是不可交互的,那么要想实现可交互的动画该怎么做呢?如上面所说的,在dismiss时返回一个实现了UIViewControllerInteractiveTransitioning协议的代理或者直接是原生类UIPercentDrivenInteractiveTransition对象。其中,UIPercentDrivenInteractiveTransition是系统封装好了百分比驱动,用起来很简单,那么真正的实现原理还是我们去实现一下。下面咱们来实现导航模式的交互效果,如下:

  • TransitioningDelegate

    #import <UIKit/UIKit.h>
    #import "Singleton.h"

    @interface TransitioningDelegate : NSObject<UIViewControllerTransitioningDelegate> SingletonH(TransitioningDelegate); @end

    #import "TransitioningDelegate.h"
    #import "CustomAnimationTransition.h"
    #import "CustomInteractiveTransition.h"
    
    @implementation TransitioningDelegate
    SingletonM(TransitioningDelegate);
    
    #pragma mark - UIViewControllerTransitioningDelegate
    
    //present
    - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
        
         //这里还是采用自定义的转场动画方式进行present,使其present时从屏幕右侧滑入
    CustomAnimationTransition
    *animation = [[CustomAnimationTransition alloc]init]; animation.presented = YES; return animation; } //dismiss,必须重写 - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed { //这里采用自定义的转场动画覆盖系统的dismiss效果,在dismiss时,由于自定义了交互动画,所以系统自己的dismiss动画不会执行
    CustomAnimationTransition
    *animation = [[CustomAnimationTransition alloc] init]; animation.presented = NO; return animation; } //将要dismiss时的交互行为,必须重写 - (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator { CustomInteractiveTransition *animation = [[CustomInteractiveTransition alloc] init]; return animation; } @end

  • CustomAnimationTransition

    #import <UIKit/UIKit.h>
    
    @interface CustomAnimationTransition : NSObject<UIViewControllerAnimatedTransitioning>
    //判断是present还是dismiss, YES:present  NO:dismisss
    @property (assign,nonatomic)BOOL presented;
    @end

    #import "CustomAnimationTransition.h"
    #import "UIView+Extension.h"
    
    const CGFloat duration = 0.5f;
    
    @implementation CustomAnimationTransition
    
    #pragma mark -<UIViewControllerAnimatedTransitioning>
    //动画时间
    - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
    {
        return duration;
    }
    
    //设置过渡动画(modal和dismiss的动画都需要在这里处理)
    - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
    {
        // UITransitionContextToViewKey,
        // UITransitionContextFromViewKey.
        
        //出来的动画
        if (self.presented) {
            
            //获取并添加转场视图
            UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
            [transitionContext.containerView addSubview:toView];
             
            //设置动画从右往左出来
            toView.x = toView.width;
    
            [UIView animateWithDuration:duration animations:^{
    
                toView.x = 0;
    
            } completion:^(BOOL finished) {
                
                //移除视图
                BOOL cancle = [transitionContext transitionWasCancelled];
                if (cancle) {
                    [toView removeFromSuperview];
                }
                
                //动画完成后,视图上的事件才能处理
                [transitionContext completeTransition:!canc
                
            

评论留言

还没有评论留言,赶紧来抢楼吧~~

吐槽小黑屋()

* 这里是“吐槽小黑屋”,所有人可看,只保留当天信息。

  • Erlo吐槽

    Erlo.vip2020-07-08 09:37:56Hello、欢迎使用吐槽小黑屋,这就是个吐槽的地方。
  • 返回顶部