首页 > 代码库 > iOS7新特性 ViewController转场切换(二) 系统视图控制器容器的切换动画---push pop present dismis

iOS7新特性 ViewController转场切换(二) 系统视图控制器容器的切换动画---push pop present dismis

          @上一章,介绍了主要的iOS7所增加的API,可以发现,它们不是一个个死的方法,苹果给我们开发者提供的是都是协议接口,所以我们能够很好的单独提出来写成一个个类,在里面实现我们各种自定义效果.

       1.先来看看实现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到容器上
    [[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;
    
    // 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,所以方法将在该设置应用在视图上了之后才进行快照,于是乎屏幕空空如也。另外就是……你可以对快照再进行快照……继续快照……

      最后,主要代码已经给出,完整的代码我已经上传为资源了,大家有意向的可以去下载下来看看(唉,主要是CSDN不支持gif动态图,好蛋疼,朋友在博客园的博客都支持,但是我又不喜欢博客园的风格,看来以后自己弄个域名博客是王道,后续我也会把我的一些代码分享到github上)
      @转载请注明,转自iOS@迷糊小书童


iOS7新特性 ViewController转场切换(二) 系统视图控制器容器的切换动画---push pop present dismis