自定义转场详解(一)

前言

本文是我学习了onevcat的这篇转场入门做的一点笔记。

今天我们来实现一个简单的自定义转场,我们先来看看这篇文章将要实现的一个效果图吧:
图1

过程详解

热身准备

我们先创建一个工程,首先用storyboard快速的创建两个控制器,一个作为主控制器,叫ViewController,另外一个作为present出来的控制器,叫PresentViewController,并且用autoLayout快速搭建好界面。就像这样:
图2

我们先做好点击ViewController上面的按钮,present出 PresentViewController,点击PresentViewController上面的按钮,dismiss掉PresentViewController的逻辑。这里有两个注意点:

  1. 因为此处我使用了segue,所以在ViewController按钮点击的时候,我们只需要这样调用就行。

    	#pragma mark - 点我弹出
    	-(IBAction)presentBtnClick:(UIButton *)sender {
    	    [self performSegueWithIdentifier:@"PresentSegue" sender:nil];
    	}
    
  2. 我们平时写dismiss的时候,一般都会是在第二个控制器中直接给self发送dismissViewController的相关方法。在现在的SDK中,如果当前的VC是被显示的话,这个消息会被直接转发到显示它的VC去。但是这并不是一个好的实现,违反了程序设计的哲学,也很容易掉到坑里。所以我们用标准的delegate 方式实现 dismiss

首先我们在PresentViewController控制器中申明一个代理方法。

	#import <UIKit/UIKit.h>
	@class PresentViewController;
	@protocol PresentViewControllerDelegate <NSObject>
	- (void)dismissViewController:(PresentViewController *)viewController;
	@end
	@interface PresentViewController : UIViewController
	@property (nonatomic, weak) id<PresentViewControllerDelegate> delegate;
	@end

在button的点击事件中,让代理去完成关闭当前控制器的工作。

	#pragma mark - 点击关闭
	- (IBAction)closeBtnClick:(UIButton *)sender {
	    if (self.delegate && [self.delegate respondsToSelector:@selector(dismissViewController:)]) {
	        [self.delegate dismissViewController:self];
	    }
	}

与此同时,在ViewController中需要设置PresentViewController的代理,并且实现代理方法:

	- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
	    if ([segue.identifier isEqualToString:@"PresentSegue"]) {
	        PresentViewController *presetVC = segue.destinationViewController;
	        presetVC.delegate = self;
	    }
	}
	#pragma mark - PresentViewControllerDelegate
	- (void)dismissViewController:(PresentViewController *)viewController {
	    [self dismissViewControllerAnimated:YES completion:nil];
	}

OK,到这里,我们一个基本的转场就完成了(这也是系统自带的一个效果)。like this:
图3

主要内容

接下来,要接触我们今天要讲的主要内容了,我们用iOS7中一个新的类UIViewControllerTransitioning来实现自定义转场。


UIViewControllerAnimatedTransitioning

首先我们需要一个实现了协议名为UIViewControllerAnimatedTransitioning的对象。创建一个类叫做PresentAnimation继承于NSObject并且实现了UIViewControllerAnimatedTransitioning协议。(注意:需要导入UIKit框架)

	@interface PresentAnimation : NSObject<UIViewControllerAnimatedTransitioning>

这个协议负责转场的具体内容。开发者在做自定义切换效果时大部门代码会是用来实现这个协议的,这个协议只有两个方法必须要实现的:

	// 返回动画的时间
	- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
	// 在进行切换的时候将调用该方法,我们对于切换时的UIView的设置和动画都在这个方法中完成。
	- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;

实现这两个方法

	- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
	    return 0.8f;
	}
	- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
	    // 1.我们需要得到参与切换的两个ViewController的信息,使用context的方法拿到它们的参照;
	    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];   
	    // 2.对于要呈现的VC,我们希望它从屏幕下方出现,因此将初始位置设置到屏幕下边缘;
	    CGRect finaRect = [transitionContext finalFrameForViewController:toVC];
	    toVC.view.frame = CGRectOffset(finaRect, 0, [UIScreen mainScreen].bounds.size.height);
	    // 3.将view添加到containerView中;
	    [[transitionContext containerView] addSubview:toVC.view];
	    // 4.开始动画。这里的动画时间长度和切换时间长度一致。usingSpringWithDamping的UIView动画API是iOS7新加入的,描述了一个模拟弹簧动作的动画曲线;
	    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.6 initialSpringVelocity:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
	        toVC.view.frame = finaRect;
	    } completion:^(BOOL finished) {
	        // 5.在动画结束后我们必须向context报告VC切换完成,是否成功。系统在接收到这个消息后,将对VC状态进行维护。
	        [transitionContext completeTransition:YES];
	    }];
	}

注意点

UITransitionContextToViewControllerKeyUITransitionContextFromViewControllerKey
比如从A present 出B,此时A是FromViewController,B是ToViewController
如果从B dismiss 到A,此时A是ToViewController,B是FromViewController

UIViewControllerTransitioningDelegate

这个接口的作用比较单一,在需要VC切换的时候系统会向实现了这个接口的对象询问是否需要使用自定义转场效果。
所以,一个比较好的地方是直接在主控制器ViewController中实现这个协议。

ViewController中完成如下代码:

	@interface ViewController ()<PresentViewControllerDelegate,UIViewControllerTransitioningDelegate>
	@property (nonatomic, strong) PresentAnimation *presentAnimation;
	@end
	@implementation ViewController
	#pragma mark - 懒加载
	- (PresentAnimation *)presentAnimation {
	    if (!_presentAnimation) {
	        _presentAnimation = [[PresentAnimation alloc] init];
	    }
	    return _presentAnimation;
	}
	#pragma mark - UIViewControllerTransitioningDelegate
	- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
	    return self.presentAnimation;
	}
	- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
	    if ([segue.identifier isEqualToString:@"PresentSegue"]) {
	        PresentViewController *presetVC = segue.destinationViewController;
	        presetVC.delegate = self;
	        presetVC.transitioningDelegate = self;
	    }
	}

现在看下我们的效果:
图4
相对于上面系统自带的效果来说,我们在present出第二个控制器的时候,带有弹簧效果。

手势驱动百分比切换

现在我们增加一个功能,就是用手势滑动来dismiss,通俗的说,就是让present出来的那个控制器使用手势dismiss。

  1. 创建一个类,继承自UIPercentDrivenInteractiveTransition

    	#import <UIKit/UIKit.h>
    	@interface PanInteractiveTransition : UIPercentDrivenInteractiveTransition
    	-(void)panToDismiss:(UIViewController *)viewController;
    	@end
    
    • 我们写一个方法提供给外部类调用。让外部类可以看到传入手势dismiss的VC的入口。
  2. 既然传入了这个需要手势dismiss的VC,我们就需要保存一下,方便当前类在其他地方使用,所以我们新建一个属性来保存这个传入的VC。

    	#import "PanInteractiveTransition.h"
    	@interface PanInteractiveTransition ()
    	@property (nonatomic, strong) UIViewController *presentVC;
    	@end
    	@implementation PanInteractiveTransition
    	-(void)panToDismiss:(UIViewController *)viewController {
    	    self.presentVC = viewController;
    	    UIPanGestureRecognizer *panGestR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureAction:)];
    	    [self.presentVC.view addGestureRecognizer:panGestR];
    	}
    	#pragma mark - panGestureAction
    	-(void)panGestureAction:(UIPanGestureRecognizer *)pan {
    	    CGPoint transition = [pan translationInView:self.presentVC.view];
    	    NSLog(@"%.2f",transition.y);
    	    switch (pan.state) {
    		    case UIGestureRecognizerStateBegan:{
                [self.presentVC dismissViewControllerAnimated:YES completion:nil];
            }
                break;
    	        case UIGestureRecognizerStateChanged:{  //
    	            CGFloat percent = MIN(1.0, transition.y/300);
    	            [self updateInteractiveTransition:percent];
    	        }
    	            break;
    	        case UIGestureRecognizerStateCancelled:
    	        case UIGestureRecognizerStateEnded:{
    	            if (pan.state == UIGestureRecognizerStateCancelled) {   // 手势取消
    	                [self cancelInteractiveTransition];
    	            }else{
    	                [self finishInteractiveTransition];
    	            }
    	        }
    	            break;
    	        default:
    	            break;
    	    }
    	}
    
  3. 和创建PresentAnimation一样,我们创建一个一个DismissAnimation

    	@interface DismissAnimation : NSObject<UIViewControllerAnimatedTransitioning>
    	@end
    	@implementation DismissAnimation
    	-(NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
    	    return 0.4f;
    	}
    	-(void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    	    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    	    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    	    CGRect initRect = [transitionContext initialFrameForViewController:fromVC];
    	    CGRect finalRect = CGRectOffset(initRect, 0, [UIScreen mainScreen].bounds.size.height);
    	    UIView *contrainerView = [transitionContext containerView];
    	    [contrainerView addSubview:toVC.view];
    	    [contrainerView sendSubviewToBack:toVC.view];
    	    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
    	        fromVC.view.frame = finalRect;
    	    } completion:^(BOOL finished) {
    	        [transitionContext completeTransition:YES];
    	    }];
    	}
    	@end
    
  4. 最后,我们在主控制器中添加一个手势驱动的对象,一个dismiss转场的对象,然后懒加载。

    	-(PanInteractiveTransition *)paninterTransition {
    	    if (!_paninterTransition) {
    	        _paninterTransition = [[PanInteractiveTransition alloc] init];
    	    }
    	    return _paninterTransition;
    	}
    	-(DismissAnimation *)dismissAnimation {
    	    if (!_dismissAnimation) {
    	        _dismissAnimation = [[DismissAnimation alloc] init];
    	    }
    	    return _dismissAnimation;
    	}
    	#pragma mark - UIViewControllerTransitioningDelegate
    	-(id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    	    return self.dismissAnimation;
    	}
    	-(id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
    	    return self.paninterTransition;
    	}
    	-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    	    if ([segue.identifier isEqualToString:@"PresentSegue"]) {
    	        // ...
    	        [self.paninterTransition panToDismiss:presetVC];
    	    }
    	}
    

完善

此时,我们运行程序,会发现以上代码尽管可以手势驱动了,但是点击按钮dismiss的功能无法使用了。这是因为如果只是返回self.paninterTransition,那么点击按钮dismiss的动画就会失效;如果只是返回nil,那么手势滑动的效果将会失效。综上所述,我们就得分情况考虑。
接下来我们就来完善一下。

  1. PanInteractiveTransition添加一个属性,表示是否处于切换过程中(用于判断使用的是点击按钮dismiss还是手势驱动来dismiss的)

    	// 是否处于切换过程中
    	@property (nonatomic, assign, getter=isInteracting) BOOL interacting;
    
  2. PanInteractiveTransition添加一个属性,表示是否需要dismiss(用于当手势滑动到超过指定高度之后,就会dismiss,如果没有超过,就会还原)

    	@property (nonatomic, assign, getter=isShouldComplete) BOOL shouldComplete;
    
  3. 修改PanInteractiveTransition中的panGestureAction:方法:

    	-(void)panGestureAction:(UIPanGestureRecognizer *)pan {
    	    CGPoint transition = [pan translationInView:pan.view];
    	    switch (pan.state) {
    	        case UIGestureRecognizerStateBegan:{
    	            self.interacting = YES;
    	            [self.presentVC dismissViewControllerAnimated:YES completion:nil];
    	        }
    	            break;
    	        case UIGestureRecognizerStateChanged:{  //
    	            CGFloat percent = fmin(fmax(transition.y/300.0, 0.0), 1.0);
    	            self.shouldComplete = (percent > 0.5);
    	            [self updateInteractiveTransition:percent];
    	        }
    	            break;
    	        case UIGestureRecognizerStateCancelled:
    	        case UIGestureRecognizerStateEnded:{
    	            self.interacting = NO;
    	            // 如果下移的距离小于300或者取消都当做取消
    	            if (!self.isShouldComplete || pan.state == UIGestureRecognizerStateCancelled) {   // 手势取消
    	                [self cancelInteractiveTransition];
    	            }else{
    	                [self finishInteractiveTransition];
    	            }
    	        }
    	            break;
    	        default:
    	            break;
    	    }
    	}
    
  4. 另外还有一点,就是需要修改DismissAnimation中的一处代码:

    	-(void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    	    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    	    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];  
    	    CGRect initRect = [transitionContext initialFrameForViewController:fromVC];
    	    CGRect finalRect = CGRectOffset(initRect, 0, [UIScreen mainScreen].bounds.size.height);
    	    UIView *contrainerView = [transitionContext containerView];
    	    [contrainerView addSubview:toVC.view];
    	    [contrainerView sendSubviewToBack:toVC.view];
    	    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
    	        fromVC.view.frame = finalRect;
    	    } completion:^(BOOL finished) {
    	    // 此处做了修改,由之前的[transitionContext completeTransition:YES];
    	      [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    	    }];
    	}
    

ok,到此为止,我们的一个自定义转场动画就算了完成了。

posted @ 2018-04-25 22:14  Scott_Mr  阅读(656)  评论(0编辑  收藏  举报