iOS开发中的ViewController转场切换效果实现简介

在iOS7之前,View Controller的切换主要有4种:

  1. Push/Pop,NavigationViewController
  2. Present and dismis Modal
  3. UITabBarController
  4. addChildViewController(一般用于自定义的继承于 UIViewController 的容器子类)

iOS5,调用- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion NS_AVAILABLE_IOS(5_0);

(1)前面3种方法这里就不多说了,很常见的系统方法.至于第四种,我在前面文章-剖析网易标签栏的效果中已经做了阐述,但是它提供的容器转场动画只可以实现一些简单的UIView动画,但是难以重用,耦合高.
(2)关键的API:
A.动画控制器 (Animation Controllers) 遵从 UIViewControllerAnimatedTransitioning 协议,并且负责实际执行动画。
B.交互控制器 (Interaction Controllers) 通过遵从 UIViewControllerInteractiveTransitioning 协议来控制可交互式的转场。
C.转场代理 (Transitioning Delegates) 根据不同的转场类型方便的提供需要的动画控制器和交互控制器。
   其中UINavigationControllerDelegate delegate 中新增了2个方法给NavigationController
               UIViewControllerTransitioningDelegate 新增transitioningDelegate  给Modal的Present和Dismis

复制代码 代码如下:

UITabBarControllerDelegate delegate
- (id <UIViewControllerInteractiveTransitioning>)tabBarController:(UITabBarController *)tabBarController
interactionControllerForAnimationController: (id <UIViewControllerAnimatedTransitioning>)animationController NS_AVAILABLE_IOS(7_0);
- (id <UIViewControllerAnimatedTransitioning>)tabBarController:(UITabBarController *)tabBarController
animationControllerForTransitionFromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC NS_AVAILABLE_IOS(7_0);

D.转场上下文 (Transitioning Contexts) 定义了转场时需要的元数据,比如在转场过程中所参与的视图控制器和视图的相关属性。 转场上下文对象遵从 UIViewControllerContextTransitioning 协议,并且这是由系统负责生成和提供的。
E.转场协调器(Transition Coordinators) 可以在运行转场动画时,并行的运行其他动画。 转场协调器遵从 UIViewControllerTransitionCoordinator 协议。
(3)新的API主要提供了2种VC切换的方式:
A.非交互式切换,即定义一种从一个VC到另一个VC的动画效果,切换的时候自动播放,
B.交互式切换,这种方式同样需要定义动画效果,只是这个动画效果会根据跟随交互式手势来切换VC并同时播放动画效果。iOS7提供了一个默认的基于百分比的动画实现 UIPercentDrivenInteractiveTransition,而且根据WWDC的说明,最简单的实现交互式动画的方法就是通过继承 UIPercentDrivenInteractiveTransition。

苹果给我们开发者提供的是都是协议接口,所以我们能够很好的单独提出来写成一个个类,在里面实现我们各种自定义效果.
(4)来看看实现UIViewControllerAnimatedTransitioning的自定义动画类

复制代码 代码如下:

/**
 *  自定义的动画类
 *  实现协议------>@protocol UIViewControllerAnimatedTransitioning
 *  这个接口负责切换的具体内容,也即“切换中应该发生什么”
 */ 
@interface MTHCustomAnimator : NSObject <UIViewControllerAnimatedTransitioning> 
 
@end 
 
@implementation MTHCustomAnimator 
 
// 系统给出一个切换上下文,我们根据上下文环境返回这个切换所需要的花费时间 
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext 

    return 1.0; 

 
// 完成容器转场动画的主要方法,我们对于切换时的UIView的设置和动画都在这个方法中完成 
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext 

    // 可以看做为destination ViewController 
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; 
    // 可以看做为source ViewController 
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; 
    // 添加toView到容器上 
        // 如果是XCode6 就可以用这段 
    if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) 
    { 
        // iOS8 SDK 新API 
        UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey]; 
        //UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey]; 
        [[transitionContext containerView] addSubview:toView]; 
    }else{ 
        // 添加toView到容器上 
        [[transitionContext containerView] addSubview:toViewController.view]; 
    } 
     
    // 如果是XCode5 就是用这段 
    [[transitionContext containerView] addSubview:toViewController.view]; 
    toViewController.view.alpha = 0.0; 
    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ 
        // 动画效果有很多,这里就展示个左偏移 
        fromViewController.view.transform = CGAffineTransformMakeTranslation(-320, 0); 
        toViewController.view.alpha = 1.0; 
    } completion:^(BOOL finished) { 
        fromViewController.view.transform = CGAffineTransformIdentity; 
        // 声明过渡结束-->记住,一定别忘了在过渡结束时调用 completeTransition: 这个方法 
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; 
    }]; 
}

PS:从协议中两个方法可以看出,上面两个必须实现的方法需要一个转场上下文参数,这是一个遵从UIViewControllerContextTransitioning 协议的对象。通常情况下,当我们使用系统的类时,系统框架为我们提供的转场代理(Transitioning Delegates),为我们创建了转场上下文对象,并把它传递给动画控制器。
 

复制代码 代码如下:

// MainViewController 
@interface MTHMainViewController () <UINavigationControllerDelegate,UIViewControllerTransitioningDelegate> 
 
@property (nonatomic,strong) MTHCustomAnimator *customAnimator; 
@property (nonatomic,strong) PDTransitionAnimator *minToMaxAnimator; 
@property (nonatomic,strong) MTHNextViewController *nextVC; 
// 交互控制器 (Interaction Controllers) 通过遵从 UIViewControllerInteractiveTransitioning 协议来控制可交互式的转场。 
@property (strong, nonatomic) UIPercentDrivenInteractiveTransition* interactionController; 
@end 
 
@implementation MTHMainViewController 
 
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil 

    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 
    if (self) { 
        // Custom initialization 
    } 
    return self; 

 
- (void)viewDidLoad 

    [super viewDidLoad]; 
    // Do any additional setup after loading the view. 
    self.navigationItem.title = @"Demo"; 
    self.view.backgroundColor = [UIColor yellowColor]; 
    // 设置代理 
    self.navigationController.delegate = self; 
    // 设置转场动画 
    self.customAnimator = [[MTHCustomAnimator alloc] init]; 
    self.minToMaxAnimator = [PDTransitionAnimator new]; 
 
    self.nextVC = [[MTHNextViewController alloc] init]; 
    // Present的代理和自定义设置 
    _nextVC.transitioningDelegate = self; 
    _nextVC.modalPresentationStyle = UIModalPresentationCustom; (貌似有BUG)换成modalTransitionStyle = UIModalPresentationCustom 
     
    // Push 
    UIButton *pushButton = [UIButton buttonWithType:UIButtonTypeSystem]; 
    pushButton.frame = CGRectMake(140, 200, 40, 40); 
    [pushButton setTitle:@"Push" forState:UIControlStateNormal]; 
    [pushButton addTarget:self action:@selector(push) forControlEvents:UIControlEventTouchUpInside]; 
    [self.view addSubview:pushButton]; 
     
    // Present 
    UIButton *modalButton = [UIButton buttonWithType:UIButtonTypeSystem]; 
    modalButton.frame = CGRectMake(265, 500, 50, 50); 
    [modalButton setTitle:@"Modal" forState:UIControlStateNormal]; 
    [modalButton addTarget:self action:@selector(modal) forControlEvents:UIControlEventTouchUpInside]; 
    [self.view addSubview:modalButton]; 
     
    // 实现交互操作的手势 
    UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(didClickPanGestureRecognizer:)]; 
    [self.navigationController.view addGestureRecognizer:panRecognizer]; 

 
 
- (void)push 

    [self.navigationController pushViewController:_nextVC animated:YES]; 

 
- (void)modal 

    [self presentViewController:_nextVC animated:YES completion:nil]; 

 
#pragma mark - UINavigationControllerDelegate iOS7新增的2个方法 
// 动画特效 
- (id<UIViewControllerAnimatedTransitioning>) navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC 

    /**
     *  typedef NS_ENUM(NSInteger, UINavigationControllerOperation) {
     *     UINavigationControllerOperationNone,
     *     UINavigationControllerOperationPush,
     *     UINavigationControllerOperationPop,
     *  };
     */ 
    if (operation == UINavigationControllerOperationPush) { 
        return self.customAnimator; 
    }else{ 
        return nil; 
    } 

 
// 交互 
- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController*)navigationController                           interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>)animationController 

    /**
     *  在非交互式动画效果中,该方法返回 nil
     *  交互式转场,自我理解意思是,用户能通过自己的动作来(常见:手势)控制,不同于系统缺省给定的push或者pop(非交互式)
     */ 
    return _interactionController; 
}  
 
#pragma mark - Transitioning Delegate (Modal) 
// 前2个用于动画 
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source 

    self.minToMaxAnimator.animationType = AnimationTypePresent; 
    return _minToMaxAnimator; 

 
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed 

    self.minToMaxAnimator.animationType = AnimationTypeDismiss; 
    return _minToMaxAnimator; 

 
// 后2个用于交互 
- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator 

    return _interactionController; 

 
- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator 

    return nil; 
}

以上实现的是非交互的转场,指的是完全按照系统指定的切换机制,用户无法中途取消或者控制进度切换.那怎么来实现交互转场呢:
UIPercentDrivenInteractiveTransition实现了UIViewControllerInteractiveTransitioning接口的类,,可以用一个百分比来控制交互式切换的过程。我们在手势识别中只需要告诉这个类的实例当前的状态百分比如何,系统便根据这个百分比和我们之前设定的迁移方式为我们计算当前应该的UI渲染,十分方便。具体的几个重要方法:
-(void)updateInteractiveTransition:(CGFloat)percentComplete 更新百分比,一般通过手势识别的长度之类的来计算一个值,然后进行更新。之后的例子里会看到详细的用法
-(void)cancelInteractiveTransition 报告交互取消,返回切换前的状态
–(void)finishInteractiveTransition 报告交互完成,更新到切换后的状态

复制代码 代码如下:

#pragma mark - 手势交互的主要实现--->UIPercentDrivenInteractiveTransition 
- (void)didClickPanGestureRecognizer:(UIPanGestureRecognizer*)recognizer 

    UIView* view = self.view; 
    if (recognizer.state == UIGestureRecognizerStateBegan) { 
        // 获取手势的触摸点坐标 
        CGPoint location = [recognizer locationInView:view]; 
        // 判断,用户从右半边滑动的时候,推出下一个VC(根据实际需要是推进还是推出) 
        if (location.x > CGRectGetMidX(view.bounds) && self.navigationController.viewControllers.count == 1){ 
            self.interactionController = [[UIPercentDrivenInteractiveTransition alloc] init]; 
            // 
            [self presentViewController:_nextVC animated:YES completion:nil]; 
        } 
    } else if (recognizer.state == UIGestureRecognizerStateChanged) { 
        // 获取手势在视图上偏移的坐标 
        CGPoint translation = [recognizer translationInView:view]; 
        // 根据手指拖动的距离计算一个百分比,切换的动画效果也随着这个百分比来走 
        CGFloat distance = fabs(translation.x / CGRectGetWidth(view.bounds)); 
        // 交互控制器控制动画的进度 
        [self.interactionController updateInteractiveTransition:distance]; 
    } else if (recognizer.state == UIGestureRecognizerStateEnded) { 
        CGPoint translation = [recognizer translationInView:view]; 
        // 根据手指拖动的距离计算一个百分比,切换的动画效果也随着这个百分比来走 
        CGFloat distance = fabs(translation.x / CGRectGetWidth(view.bounds)); 
        // 移动超过一半就强制完成 
        if (distance > 0.5) { 
            [self.interactionController finishInteractiveTransition]; 
        } else { 
            [self.interactionController cancelInteractiveTransition]; 
        } 
        // 结束后一定要置为nil 
        self.interactionController = nil; 
    } 
}

最后,给大家分享一个动画特效:类似于飞兔云传的发送ViewController切换

复制代码 代码如下:

@implementation PDTransitionAnimator 
 
#define Switch_Time 1.2 
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext { 
    return Switch_Time; 

 
#define Button_Width 50.f 
#define Button_Space 10.f 
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext { 
    UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; 
    UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; 
 
    UIView * toView = toViewController.view; 
    UIView * fromView = fromViewController.view; 
     
    if (self.animationType == AnimationTypeDismiss) { 
        // 这个方法能够高效的将当前显示的view截取成一个新的view.你可以用这个截取的view用来显示.例如,也许你只想用一张截图来做动画,毕竟用原始的view做动画代价太高.因为是截取了已经存在的内容,这个方法只能反应出这个被截取的view当前的状态信息,而不能反应这个被截取的view以后要显示的信息.然而,不管怎么样,调用这个方法都会比将view做成截图来加载效率更高. 
        UIView * snap = [toView snapshotViewAfterScreenUpdates:YES]; 
        [transitionContext.containerView addSubview:snap]; 
        [snap setFrame:CGRectMake([UIScreen mainScreen].bounds.size.width - Button_Width - Button_Space, [UIScreen mainScreen].bounds.size.height - Button_Width - Button_Space, Button_Width, Button_Width)]; 
         
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ 
            [snap setFrame:[UIScreen mainScreen].bounds]; 
        } completion:^(BOOL finished) { 
            [UIView animateWithDuration:0.5 animations:^{ 
                [[transitionContext containerView] addSubview:toView]; 
                snap.alpha = 0;  
            } completion:^(BOOL finished) { 
                [snap removeFromSuperview]; 
                [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; 
            }]; 
        }]; 
    } else { 
        UIView * snap2 = [toView snapshotViewAfterScreenUpdates:YES]; 
        [transitionContext.containerView addSubview:snap2]; 
        UIView * snap = [fromView snapshotViewAfterScreenUpdates:YES]; 
        [transitionContext.containerView addSubview:snap]; 
         
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ 
            [snap setFrame:CGRectMake([UIScreen mainScreen].bounds.size.width - Button_Width - Button_Space+ (Button_Width/2), [UIScreen mainScreen].bounds.size.height - Button_Width - Button_Space + (Button_Width/2), 0, 0)]; 
        } completion:^(BOOL finished) { 
            [UIView animateWithDuration:0.5 animations:^{ 
                //snap.alpha = 0; 
            } completion:^(BOOL finished) { 
                [snap removeFromSuperview]; 
                [snap2 removeFromSuperview]; 
                [[transitionContext containerView] addSubview:toView]; 
                // 切记不要忘记了噢 
                [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; 
            }]; 
        }]; 
         
    } 
}

其中,snapshotViewAfterScreenUpdates 方法的解释,我也不是很懂,反正初级来说会用就行,还可以参照下面的解析:
在iOS7 以前, 获取一个UIView的快照有以下步骤: 首先创建一个UIGraphics的图像上下文,然后将视图的layer渲染到该上下文中,从而取得一个图像,最后关闭图像上下文,并将图像显示在UIImageView中。现在我们只需要一行代码就可以完成上述步骤了:

复制代码 代码如下:

[view snapshotViewAfterScreenUpdates:NO];

这个方法制作了一个UIView的副本,如果我们希望视图在执行动画之前保存现在的外观,以备之后使用(动画中视图可能会被子视图遮盖或者发生其他一些变化),该方法就特别方便。
afterUpdates参数表示是否在所有效果应用在视图上了以后再获取快照。例如,如果该参数为NO,则立马获取该视图现在状态的快照,反之,以下代码只能得到一个空白快照:

复制代码 代码如下:

[view snapshotViewAfterScreenUpdates:YES];
[view setAlpha:0.0];

由于我们设置afterUpdates参数为YES,而视图的透明度值被设置成了0,所以方法将在该设置应用在视图上了之后才进行快照,于是乎屏幕空空如也。另外就是……你可以对快照再进行快照……继续快照……

继续前面的内容,这一章,主要介绍自定义ViewController容器上视图VC的切换.先来看看系统给我们提供的容器控制器 UINavigationController和UITabBarController 都有一个NSArray类型的属性viewControllers,很明显,存储的就是需要切换的视图VC.同理,我们定义一个ContainerViewController,是UIViewController的直接子类,用来作为容器依托,额,其他属性定义详见代码吧,这里不多说了.(PS:原先我进行多个自定义视图VC切换的方法,是放置一个UIScrollView,然后把所有childViewController的View的frame的X坐标,依此按320递增,大家可以自行想象下,这样不好的地方,我感觉就是所有的VC一经加载就全部实体化了,而且不会因为被切换变成暂不显示而释放掉)
偷懒下,用storyboard创建的5个childVC

复制代码 代码如下:

// ContainerViewController 
@interface FTContainerViewController ()  
 
@property (strong, nonatomic) FTPhotoSenderViewController       *photoSenderViewController; 
@property (strong, nonatomic) FTVideoSenderViewController       *videoSenderViewController; 
@property (strong, nonatomic) FTFileSenderViewController        *fileSenderViewController; 
@property (strong, nonatomic) FTContactSenderViewController     *contactSenderViewController; 
@property (strong, nonatomic) FTClipboardSenderViewController   *clipboardSenderViewController; 
@property (strong, nonatomic) UIViewController                  *selectedViewController;        // 当前选择的VC 
@property (strong, nonatomic) NSArray                           *viewControllers;               // childVC数组 
@property (assign, nonatomic) NSInteger                         currentControllerIndex;         // 当前选择的VC的数组下标号 
 
 
@end 
 
@implementation FTContainerViewController 
 
#pragma mark - ViewLifecycle Methods 
 
- (void)viewDidLoad 

    [super viewDidLoad]; 
    // childVC 
    self.photoSenderViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"FTPhotoSenderViewController"]; 
    self.videoSenderViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"FTVideoSenderViewController"]; 
    self.fileSenderViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"FTFileSenderViewController"]; 
    self.contactSenderViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"FTContactSenderViewController"]; 
    self.clipboardSenderViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"FTClipboardSenderViewController"]; 
    // 存储childVC的数组 
    self.viewControllers = @[_photoSenderViewController,_videoSenderViewController,_fileSenderViewController,_contactSenderViewController,_clipboardSenderViewController]; 
    // 缺省为下标为0的VC 
    self.selectedViewController = self.selectedViewController ?: self.viewControllers[0]; 
    self.currentControllerIndex = 0; 
}

依旧,实现UIViewControllerAnimatedTransitioning协议的Animator类,不过里面换个动画效果,利用iOS7新增的弹簧动画效果:

复制代码 代码如下:

#import "FTMthTransitionAnimator.h" 
 
@implementation FTMthTransitionAnimator 
 
static CGFloat const kChildViewPadding = 16; 
static CGFloat const kDamping = 0.5;    // damping参数代表弹性阻尼,随着阻尼值越来越接近0.0,动画的弹性效果会越来越明显,而如果设置阻尼值为1.0,则视图动画不会有弹性效果 
static CGFloat const kInitialSpringVelocity = 0.5;  // 初始化弹簧速率 
 
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext 

    return 1.0; 

 
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext 

    /**
     *  - viewControllerForKey:我们可以通过他访问过渡的两个 ViewController。
     *  - containerView:两个 ViewController 的 containerView。
     *  - initialFrameForViewController 和 finalFrameForViewController 是过渡开始和结束时每个 ViewController 的 frame。
     */ 
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; 
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; 
     
    [[transitionContext containerView] addSubview:toViewController.view]; 
    toViewController.view.alpha = 0; 
    BOOL goingRight = ([transitionContext initialFrameForViewController:toViewController].origin.x < [transitionContext finalFrameForViewController:toViewController].origin.x); 
    CGFloat transDistance = [transitionContext containerView].bounds.size.width + kChildViewPadding; 
    CGAffineTransform transform = CGAffineTransformMakeTranslation(goingRight ? transDistance : -transDistance, 0); 
    // CGAffineTransformInvert 反转 
    toViewController.view.transform = CGAffineTransformInvert(transform); 
//    toViewController.view.transform = CGAffineTransformTranslate(toViewController.view.transform, (goingRight ? transDistance : -transDistance), 0); 
     
    /**
     *   ----------弹簧动画.....-------
     *  使用由弹簧的运动描述的时序曲线` animations` 。当` dampingRatio`为1时,动画将平稳减速到其最终的模型值不会振荡。阻尼比小于1来完全停止前将振荡越来越多。可以使用弹簧的初始速度,以指定的速度在模拟弹簧的端部的物体被移动它附着之前。这是一个单元坐标系,其中1是指行驶总距离的动画在第二。所以,如果你改变一个物体的位置由200PT在这个动画,以及你想要的动画表现得好像物体在动,在100PT /秒的动画开始之前,你会通过0.5 。你通常会想通过0的速度。
     */ 
    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0 usingSpringWithDamping:kDamping initialSpringVelocity:kInitialSpringVelocity options:0x00 animations:^{ 
        fromViewController.view.transform = transform; 
        fromViewController.view.alpha = 0; 
        // CGAffineTransformIdentity  重置,初始化 
        toViewController.view.transform = CGAffineTransformIdentity; 
        toViewController.view.alpha = 1; 
    } completion:^(BOOL finished) { 
        fromViewController.view.transform = CGAffineTransformIdentity; 
        // 声明过渡结束-->记住,一定别忘了在过渡结束时调用 completeTransition: 这个方法。 
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; 
    }]; 

 
@end

接下来的代码,就是实现自定义容器切换的关键了.通常情况下,当我们使用系统内建的类时,系统框架为我们创建了转场上下文对象,并把它传递给动画控制器。但是在我们这种情况下,我们需要自定义转场动画,所以我们需要承担系统框架的责任,自己去创建这个转场上下文对象。

复制代码 代码如下:

@interface FTMthTransitionContext : NSObject <UIViewControllerContextTransitioning> 
 
- (instancetype)initWithFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController goingRight:(BOOL)goingRight; 
@property (nonatomic, copy) void (^completionBlock)(BOOL didComplete); 
@property (nonatomic, assign, getter=isAnimated) BOOL animated; 
@property (nonatomic, assign, getter=isInteractive) BOOL interactive; // 是否交互式 
 
@property (nonatomic, strong) NSDictionary *privateViewControllers; 
@property (nonatomic, assign) CGRect privateDisappearingFromRect; 
@property (nonatomic, assign) CGRect privateAppearingFromRect; 
@property (nonatomic, assign) CGRect privateDisappearingToRect; 
@property (nonatomic, assign) CGRect privateAppearingToRect; 
@property (nonatomic, weak) UIView *containerView; 
@property (nonatomic, assign) UIModalPresentationStyle presentationStyle; 
 
@end 
 
@implementation FTMthTransitionContext 
 
- (instancetype)initWithFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController goingRight:(BOOL)goingRight { 
     
    if ((self = [super init])) { 
        self.presentationStyle = UIModalPresentationCustom; 
        self.containerView = fromViewController.view.superview; 
        self.privateViewControllers = @{ 
                                        UITransitionContextFromViewControllerKey:fromViewController, 
                                        UITransitionContextToViewControllerKey:toViewController, 
                                        }; 
         
        // Set the view frame properties which make sense in our specialized ContainerViewController context. Views appear from and disappear to the sides, corresponding to where the icon buttons are positioned. So tapping a button to the right of the currently selected, makes the view disappear to the left and the new view appear from the right. The animator object can choose to use this to determine whether the transition should be going left to right, or right to left, for example. 
        CGFloat travelDistance = (goingRight ? -self.containerView.bounds.size.width : self.containerView.bounds.size.width); 
        self.privateDisappearingFromRect = self.privateAppearingToRect = self.containerView.bounds; 
        self.privateDisappearingToRect = CGRectOffset (self.containerView.bounds, travelDistance, 0); 
        self.privateAppearingFromRect = CGRectOffset (self.containerView.bounds, -travelDistance, 0); 
    } 
     
    return self; 

 
- (CGRect)initialFrameForViewController:(UIViewController *)viewController { 
    if (viewController == [self viewControllerForKey:UITransitionContextFromViewControllerKey]) { 
        return self.privateDisappearingFromRect; 
    } else { 
        return self.privateAppearingFromRect; 
    } 

 
- (CGRect)finalFrameForViewController:(UIViewController *)viewController { 
    if (viewController == [self viewControllerForKey:UITransitionContextFromViewControllerKey]) { 
        return self.privateDisappearingToRect; 
    } else { 
        return self.privateAppearingToRect; 
    } 

 
- (UIViewController *)viewControllerForKey:(NSString *)key { 
    return self.privateViewControllers[key]; 

 
- (void)completeTransition:(BOOL)didComplete { 
    if (self.completionBlock) { 
        self.completionBlock (didComplete); 
    } 

 
// 非交互式,直接返回NO,因为不允许交互当然也就无法操作进度取消 
- (BOOL)transitionWasCancelled { return NO; } 
 
// 非交互式,直接不进行操作,只有进行交互,下面3个协议方法才有意义,可参照系统给我们定义好的交互控制器 
// @interface UIPercentDrivenInteractiveTransition : NSObject <UIViewControllerInteractiveTransitioning> 
- (void)updateInteractiveTransition:(CGFloat)percentComplete {} 
- (void)finishInteractiveTransition {} 
- (void)cancelInteractiveTransition {} 
 
@end

OK,准备工作都做好了,为了仿照UIScrollView的滑动切换,但又因为现在展示的是非交互式,我们定义一个swip(轻扫)手势.

复制代码 代码如下:

UISwipeGestureRecognizer *leftGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swapController:)]; 
[leftGesture setDirection:UISwipeGestureRecognizerDirectionLeft]; 
[self.view addGestureRecognizer:leftGesture]; 
 
UISwipeGestureRecognizer *rightGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swapController:)]; 
[rightGesture setDirection:UISwipeGestureRecognizerDirectionRight]; 
[self.view addGestureRecognizer:rightGesture]; 
[objc] view plaincopy
// 响应手势的方法 
- (void)swapViewControllers:(UISwipeGestureRecognizer *)swipeGestureRecognizer 

    if (swipeGestureRecognizer.direction == UISwipeGestureRecognizerDirectionLeft) { 
        if (_currentControllerIndex < 4) { 
            _currentControllerIndex++; 
        } 
        NSLog(@"_currentControllerIndex = %ld",(long)_currentControllerIndex); 
        UIViewController *selectedViewController = self.viewControllers[_currentControllerIndex]; 
         NSLog(@"%s__%d__|%@",__FUNCTION__,__LINE__,@"右边"); 
         self.selectedViewController = selectedViewController; 
    } else if (swipeGestureRecognizer.direction == UISwipeGestureRecognizerDirectionRight){ 
         NSLog(@"%s__%d__|%@",__FUNCTION__,__LINE__,@"左边"); 
        if (_currentControllerIndex > 0) { 
            _currentControllerIndex--; 
        } 
        UIViewController *selectedViewController = self.viewControllers[_currentControllerIndex]; 
        self.selectedViewController = selectedViewController; 
    } 

 
// 重写selectedViewController的setter 
- (void)setSelectedViewController:(UIViewController *)selectedViewController 

    NSParameterAssert (selectedViewController); 
    [self _transitionToChildViewController:selectedViewController]; 
    _selectedViewController = selectedViewController; 

 
// 切换操作(自定义的,联想我在前面文章网易标签栏切换中,系统给的transitionFromViewController,是一个道理) 
- (void)_transitionToChildViewController:(UIViewController *)toViewController 

    UIViewController *fromViewController = self.childViewControllers.count > 0 ? self.childViewControllers[0] : nil; 
    if (toViewController == fromViewController) { 
        return; 
    } 
    UIView *toView = toViewController.view; 
    [toView setTranslatesAutoresizingMaskIntoConstraints:YES]; 
    toView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 
    toView.frame = self.view.bounds; 
     
    // 自定义容器的切换,addChildViewController是关键,它保证了你想要显示的VC能够加载到容器中 
    // 而所谓的动画和上下文,只是为了转场的动画效果 
    // 因此,就算用UIScrollView切换,也不能缺少addChildViewController,切记!切记! 
    [fromViewController willMoveToParentViewController:nil]; 
    [self addChildViewController:toViewController]; 
     
    if (!fromViewController) { 
        [self.view addSubview:toViewController.view]; 
        [toViewController didMoveToParentViewController:self]; 
        return; 
    } 
     
    // Animator 
    FTMthTransitionAnimator *transitionAnimator = [[FTMthTransitionAnimator alloc] init]; 
    NSUInteger fromIndex = [self.viewControllers indexOfObject:fromViewController]; 
    NSUInteger toIndex = [self.viewControllers indexOfObject:toViewController]; 
     
    // Context 
    FTMthTransitionContext *transitionContext = [[FTMthTransitionContext alloc] initWithFromViewController:fromViewController toViewController:toViewController goingRight:(toIndex > fromIndex)]; 
    transitionContext.animated = YES; 
    transitionContext.interactive = NO; 
    transitionContext.completionBlock = ^(BOOL didComplete) { 
        // 因为是非交互式,所以fromVC可以直接直接remove出its parent's children controllers array 
        [fromViewController.view removeFromSuperview]; 
        [fromViewController removeFromParentViewController]; 
        [toViewController didMoveToParentViewController:self]; 
        if ([transitionAnimator respondsToSelector:@selector (animationEnded:)]) { 
            [transitionAnimator animationEnded:didComplete]; 
        } 
    }; 
    // 转场动画需要以转场上下文为依托,因为我们是自定义的Context,所以要手动设置 
    [transitionAnimator animateTransition:transitionContext]; 
}

大功告成.
上面展示的就是一个基本的自定义容器的非交互式的转场切换.那交互式的呢?从上面我定义手势定义为swip而不是pan也可以看出,非交互转场,并不能完全实现UIScrollView那种分页式的效果,按照类似百分比的形式来进行fromVC和toVC的切换,因为我们缺少交互控制器.在自定义的容器中,系统是没有提供返回交互控制器的协议给我们的,查了蛮多资料,也没找到给出明确的方法,我认为,要跟实现转场上下文一样,仿照系统方法,自定义的去实现交互式的协议方法.我们就要去思考,系统是如何搭建起这个环境的.

时间: 2015-09-07

iOS实现日历翻页动画

本文我主要描述两方面: 1.日历(简单描述原理) 2.翻页动画(重点) 最终的效果如下图:     图中沿四个对角的翻页动画,代表对应方向手势的滑动 1. 日历 要实现一个日历,其实原理很简单,我们只要知道三个数据: 1.今天是哪一天 2.这个月的第一天是星期几(哪天) 3.这个月总共有多少天 根据这个三个数据,就可以把得到的日期显示在日历上了,至于日历用什么来显示,我个人比较喜欢用UICollectionView,一个cell代表一天,当然也可以用很多个label,button来显示. 1.获

iOS图片界面翻页切换效果

先看效果: 下面贴代码: #import "ViewController.h" @interface ViewController () @property (weak, nonatomic) IBOutlet UIImageView *backgroundView; @property (strong,nonatomic) NSArray *array; @end @implementation ViewController -(NSArray *)array { if (_arra

iOS自定义键盘切换效果

本文实例为大家分享了iOS自定义键盘切换的相关代码,供大家参考,具体内容如下 具体代码如下 - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. self.title = @"小飞哥键盘"; self.textField = [[UITextField alloc] initWithFrame:CGRectMa

比较IOS开发中常用视图的四种切换方式

在iOS开发中,比较常用的切换视图的方式主要有以下几种: 1. push.pop 使用举例(ViewController假设为需要跳转的控制器): [self.navigationController pushViewController:ViewController animated:YES]; //入栈,跳转到指定控制器视图 [self.navigationController popViewControllerAnimated:YES]; //弹栈,返回到前一个视图 [self.navig

浅析iOS多视图滑动点击切换的集成

前言 多视图滑动点击切换这个视图在很多App都有用到,我对这个View进行了封装,外界只需要调用一个接口,就能实现这个效果,使用方法和系统的tabbarController很相似. 外界只需要调用下面这个接口即可集成. /** * 添加一个子控制器 */ - (void)addSubItemWithViewController:(UIViewController *)viewController; HYTabbarView效果图如下 HYTabbarView可灵活配置一屏宽显示多少个标题,以及标

实例讲解iOS中的UIPageViewController翻页视图控制器

一.引言 UIPageViewController是iOS中少见的动画视图控制器之一,通过它既可以创建类似UIScrollView与UIPageControl结合的滚屏视图,也可以创建类似图书效果的炫酷翻页视图. UIPageViewController在iOS 5 SDK中首次引入,它使得开发者可以使用这个ViewController创建分页视图.在iOS 6中,这个类有了更新,支持滚动过渡效果.使用Page View,用户可以方便的通过手势在多个页面之间导航.UIPageViewContro

一步一步实现iOS主题皮肤切换效果

本文实例为大家分享了iOS主题皮肤切换代码,供大家参考,具体内容如下 1. 主题皮肤功能切换介绍 主题切换就是根据用户设置不同的主题,来动态改变用户的界面,通常会改变navigationBar背景图片.tabBar背景图片.tabBar中的按钮的图片和选中的背景图片.navigationItem.title 标题的字体颜色.UI中其他元素控件 下载源代码地址: http://xiazai.jb51.net/201609/yuanma/ThemeSkinSetup(jb51.net).rar 2.

iOS应用中使用Toolbar工具栏方式切换视图的方法详解

关于UIToolbar ToolBar工具栏是视图View的属性,可以在工具栏上添加工具栏按钮Bar Button Item(可以是自定义的Custom.也可以是系统自带的BarButtonSystemItem ),视图控制器可以通过工具栏项对视图中内容进行操作. 注意事项: 在导航栏控制器中会有一个UIToolBar实例,但默认是隐藏的,如果需要显示,需要通过这个方法将其打开: 在这里需要注意的是,与UINavigationBar类似,导航控制器拥有且只拥有一个UIToolBar实例,但UIT

Unity iOS混合开发界面切换思路解析

思路 之前一篇文章里面只谈到了Unity和iOS工程的融合,并没有谈到iOS和Unity界面的切换,这里谈谈思路,Unity导出的iOS工程里面的结构大致是这样的,有一个Window,Window上有一个UnityView,但是并没有控制器,也没有根控制器,虽然在导出的iOS工程中Classes文件夹下的UnityAppController中有rootController的属性,但是上面也标注为空~ 所以,思路就只有一种,,既然Unity导出的iOS工程有一个Window并没有控制器,那好,混合

iOS开发之视图切换

一.视图切换 UITabBarController (分页控制器) - 平行管理视图 UINavigationController (导航控制器) - 压栈出栈管理视图 模态窗口 二.UITabBarController分页控制器 UITabBarController是为了利用 页签切换视图 设计的控制器 该控制器有一个UITabBar控件,用户通过点击UITabBar进行视图切换 UITabBarController本身会不显示任何视图,它只是一个 容器控制器 为了减少视图间的耦合,所有UIT

iOS开发tips-UINavigationBar的切换效果

概述 在iOS系统中,如果控制器是以push方式进行管理的话,那么事实上多个控制器是共享的同一个导航栏.当然iOS系统的设计无可厚非,但是国内的应用经常会遇到很多个性的设计,就比如说A push到 B,A可能有导航栏,但是B控制器要求导航栏是透明的,这样一来A和B由于共用同一个导航栏就会让整个切换操作不易管理,何况从iOS 7开始不仅仅是点击返回按钮瞬间返回A那么简单,还要支持通过手势操作从B缓慢返回到A的导航栏渐变效果.本文就简单看一下如何能够更加合理的设计导航栏切换的效果以避免开发中由于导航

iOS开发中视图的下拉放大和上拉模糊的效果实现

把"秘密"的Cell效果整体视图都放到scrollView中,基本是和secret app 一模一样的效果了. 代码如下:(模糊效果的类就不写了,大家可以搜"UIImage+ImageEffects",还要导入Accelerate.framework) 1.MTSecretAppEffect.h 复制代码 代码如下: #import <Foundation/Foundation.h>    @interface MTSecretAppEffect : N

iOS开发系列--通知与消息机制详解

概述 在多数移动应用中任何时候都只能有一个应用程序处于活跃状态,如果其他应用此刻发生了一些用户感兴趣的那么通过通知机制就可以告诉用户此时发生的事情.iOS中通知机制又叫消息机制,其包括两类:一类是本地通知:另一类是推送通知,也叫远程通知.两种通知在iOS中的表现一致,可以通过横幅或者弹出提醒两种形式告诉用户,并且点击通知可以会打开应用程序,但是实现原理却完全不同.今天就和大家一块去看一下如何在iOS中实现这两种机制,并且在文章后面会补充通知中心的内容避免初学者对两种概念的混淆. 本地通知 本地通

详解IOS中Tool Bar切换视图方法

本文通过实例给大家详细讲解了IOS开发中Tool Bar切换视图方法以及原理解释,希望我们的整理对你有用,一起学习下. iOS中几种典型的多视图程序: (1)Tab Bar Application:程序的底部有一排按钮,轻触其中一个按钮,相应的视图被激活并显示出来: (2)Navigation-Based Application:其特点是使用navigation controller,而navigation controller使用navigation bar来控制多级视图: (3)Tool B

IOS开发仿微信右侧弹出视图实现

IOS开发仿微信右侧弹出视图实现 微信首页的+号,点击之后会弹出一个更多的视图,这个视图如何实现呢? 实现该效果可能需要以下技术要点: 1.图片拉伸,通过拉伸图片的中间的较小区域来保持图片的边上的形状 2.仿射变换,用到仿射变换的缩放,平移和合并,视图动画 3.navigationBar的样式设置 实现效果,如下: 本Demo图片来源微信安装包解压得到的图片 实现代码: // // ViewController.m // appXX-微信更多工具栏 // // Created by MRBean

IOS开发之为视图绘制单(多)个圆角实例代码

IOS开发之为视图绘制单(多)个圆角实例代码 前言: 为视图绘制圆角,圆角可以选左上角.左下角.右下角.右上角.全部圆角 //Core Raduias UIView *actionView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 200, 200)]; UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:actionView.bounds byRoundingCo

iOS开发之表视图详解

本文详细介绍了表视图的用法.具体如下: 概述 表视图组成 表视图是iOS开发中最重要的视图,它以列表的形式展示数据.表视图又一下部分组成: 表头视图:表视图最上边的视图 表脚视图:表视图最下边的视图 单元格(cell):表视图中每一行的视图 节(section):由多个单元格组成,应用于分组列表 节头 节脚 表视图的相关类 UITableView继承自UIScrollView,且有两个协议:UITableViewDelegate和UITableViewDataSource.此外UITableVi

IOS开发OC代码中创建Swift编写的视图控制器

IOS开发OC代码中创建Swift编写的视图控制器 背景 近日在和一群朋友做项目,我和另一位同学负责iOS客户端,我是一直使用OC的,而他只会Swift,因此在我们分工协作之后,就需要把代码合在一起,这就牵扯到如何在TabbarController中添加一个swift创建的子控制器的问题. 解决 首先在一个OC项目中新建一个Swift类,继承自UITableViewController,并且修改其view背景色,方便后续测试. import UIKit class ESSwiftTableVie