自定义转场动画(一)

两个Controller之间的交互,相比正常的Push和Present,转场动画是iOS里比较酷炫的一种效果,能够以各种效果平滑的切换两个不同的视图控制器。适当的运用转场动画,会让你的APP变得更加生动有趣。

这篇文章会以Present为例,讲述如何自定义一个转场动画。后续会有Push,以及更复杂的转场介绍。

准备:首先要两个UIViewController和一个继承与UIPercentDrivenInteractiveTransition的类。

ViewController1

ViewController2

PresentTransitionAnimator

场景:ViewController1 从右边present 到 ViewController2,并支持手势拖动返回。

PresentTransitionAnimator.h

#import <UIKit/UIKit.h>

@interface PresentTransitionAnimator : UIPercentDrivenInteractiveTransition<UIViewControllerAnimatedTransitioning,UIViewControllerTransitioningDelegate,UIGestureRecognizerDelegate>

@property (nonatomic, assign) BOOL isInteractive;//是否在拖动

@property (nonatomic, assign) BOOL isDismiss;//是present还是dismiss

@property (nonatomic, assign) float panRatio;//拖动比率

- (id)initWithModalViewController:(UIViewController *)modalViewController;//初始化

@end

PresentTransitionAnimator.m

@property (nonatomic, weak) UIViewController *modalController;//目标Controller

@property (nonatomic, strong) id<UIViewControllerContextTransitioning> transitionContext;//转场上下文,用来获取这两个交互的UIViewController。

@property (nonatomic, strong) UIPanGestureRecognizer *gesture;//拖动手势


- (instancetype)initWithModalViewController:(UIViewController *)modalViewController{
    self = [super init];
    if (self) {
        _modalController    = modalViewController;

        //创建手势
        self.gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
        self.gesture.delegate = self;
        [self.modalController.view addGestureRecognizer:self.gesture];
    }
    return self;
}

转场动画的协议

#pragma mark - UIViewControllerTransitioningDelegate Methods

//Present转场的开始
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{
    self.isDismiss = NO;
    return self;
}

//Dismiss转场的开始
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{
    self.isDismiss = YES;
    return self;
}

//手势Present转场的开始(由于这个demo不支持手势拖动Present,所以返回nil)
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id<UIViewControllerAnimatedTransitioning>)animator{
    return nil;
}

//手势Dismiss转场的开始,同理,假如不支持手势Dismiss,则返回nil。
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator{
    // Return nil if we are not interactive
    if ( self.isInteractive) {
        self.isDismiss = YES;
        return self;
    }
    return nil;
}

转场动画需要的时间

#pragma mark - UIViewControllerAnimatedTransitioning

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext{
    return 0.25;
}

转场核心代码,我们转场动画的细节处理,适用于点击按钮的自动跳转或者返回。手势拖动不会调用该方法,需要我们单独处理。

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
- 
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController   = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    UIView *containerView = [transitionContext containerView];//获取容器View

    //*这里需要注意!如果是present,fromViewController为ViewCOntroller1,toViewController为ViewController2.
    //若是dismiss,则正好相反。

    if (!self.isDismiss) {//Present
        //*把目标的view放到容器View中        
        [containerView addSubview:toViewController.view];

        toViewController.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;

        //*转场开始前,toViewController的View位于屏幕的右侧看不见的地方
       CGRect startRect = CGRectMake(CGRectGetWidth(containerView.bounds), 0, CGRectGetWidth(containerView.bounds), CGRectGetHeight(containerView.bounds));

        toViewController.view.frame = startRect;

        [fromViewController beginAppearanceTransition:NO animated:YES];

        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            fromViewController.view.alpha = self.behindViewAlpha;
            //*动画的执行过程是从右侧看不见的地方,往左移动到到屏幕的位置
            toViewController.view.frame = CGRectMake( 0, 0,
                                                     CGRectGetWidth(toViewController.view.frame),
                                                     CGRectGetHeight(toViewController.view.frame) );
        } completion:^(BOOL finished) {
            //*转场结束,把操作权给系统。
            [fromViewController endAppearanceTransition];
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        }];
    }
    //*dismiss
    else{
                [containerView bringSubviewToFront:fromViewController.view];

        toViewController.view.alpha = self.behindViewAlpha;

        CGRect endRect = CGRectMake(CGRectGetWidth(fromViewController.view.frame), 0, CGRectGetWidth(fromViewController.view.frame), CGRectGetHeight(fromViewController.view.frame));

        [toViewController beginAppearanceTransition:YES animated:YES];

        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            toViewController.view.alpha = 1.0f;
            fromViewController.view.frame = endRect;

        } completion:^(BOOL finished) {
            [toViewController endAppearanceTransition];
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        }];

    }
}

备注:可以看到转场动画的present和dismiss过程,都是在一个函数内完成的,主要区别就是处理fromViewController.view和toViewController.view的frame的方式不同罢了。


处理手势:

- (void)handlePan:(UIPanGestureRecognizer *)recognizer{

    CGPoint location = [recognizer locationInView:self.modalController.view.window];
    CGPoint point = [recognizer translationInView:self.modalController.view];
    //禁止左滑
    if (point.x<0) {
        return;
    }
    //*开始滑动
    if (recognizer.state  == UIGestureRecognizerStateBegan) {
        self.isInteractive = YES;
        self.panLocationStart = location.x;
        [self.modalController dismissViewControllerAnimated:YES completion:nil];
    }
    //*滑动中
    else if (recognizer.state == UIGestureRecognizerStateChanged){
        CGFloat animationRatio = 0;
        animationRatio       = (location.x - self.panLocationStart) / ( CGRectGetWidth([self.modalController view].bounds) );
        self.panRatio        = animationRatio;
        [self updateInteractiveTransition:animationRatio];
    }
    //*滑动
    else if (recognizer.state == UIGestureRecognizerStateEnded){
        //*控制滑动到某程度,松手后是dismiss还是恢复到滑动前的状态。
        if ( self.panRatio > 0.12 ) {
            [self finishInteractiveTransition];
        }else{
             [self cancelInteractiveTransition];
        }
        self.isInteractive = NO;
    }
}

随着拖动的位置更新view的frame以及透明度

- (void)updateInteractiveTransition:(CGFloat)percentComplete{
    if (!self.bounces && percentComplete < 0) {
         percentComplete = 0;
    }
     id<UIViewControllerContextTransitioning> transitionContext = self.transitionContext;

    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController   = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    toViewController.view.alpha = self.behindViewAlpha + ( (1 - self.behindViewAlpha) * ABS(percentComplete) );

    CGRect updateRect = CGRectMake((CGRectGetWidth(fromViewController.view.bounds) * percentComplete),
                            0,
                            CGRectGetWidth(fromViewController.view.frame),
                            CGRectGetHeight(fromViewController.view.frame));

    if ( isnan(updateRect.origin.x) || isinf(updateRect.origin.x) ) {
        updateRect.origin.x = 0;
    }
    if ( isnan(updateRect.origin.y) || isinf(updateRect.origin.y) ) {
        updateRect.origin.y = 0;
    }

    fromViewController.view.frame = updateRect;
}

完成拖动返回

- (void)finishInteractiveTransition{
    id<UIViewControllerContextTransitioning> transitionContext = self.transitionContext;
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController   = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    CGRect endRect;

    endRect = CGRectMake( CGRectGetWidth(fromViewController.view.bounds),
                         0,
                         CGRectGetWidth(fromViewController.view.frame),
                         CGRectGetHeight(fromViewController.view.frame) );

    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        toViewController.view.alpha = 1.0f;
        fromViewController.view.frame = endRect;
    }completion:^(BOOL finished) {
        [toViewController endAppearanceTransition];
        [transitionContext completeTransition:YES];
    }];
}

取消拖动

- (void)cancelInteractiveTransition{
    id<UIViewControllerContextTransitioning> transitionContext = self.transitionContext;

    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController   = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    [toViewController beginAppearanceTransition:NO animated:YES];

    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        toViewController.view.alpha = self.behindViewAlpha;

        fromViewController.view.frame = CGRectMake( 0, 0,
                                                   CGRectGetWidth(fromViewController.view.frame),
                                                   CGRectGetHeight(fromViewController.view.frame) );
    } completion:^(BOOL finished) {
        [toViewController endAppearanceTransition];
        [transitionContext completeTransition:NO];
    }];

}

完成手势拖动的协议 UIViewControllerInteractiveTransitioning

#pragma mark - UIViewControllerInteractiveTransitioning

- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
    self.transitionContext = transitionContext;

    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController   = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    [toViewController beginAppearanceTransition:YES animated:YES];
    toViewController.view.alpha = self.behindViewAlpha;

    UIView *containerView = [transitionContext containerView];

    [containerView bringSubviewToFront:fromViewController.view];

}

完成转场

#pragma mark - 完成转场
- (void)animationEnded:(BOOL)transitionCompleted{
    self.isInteractive     = NO;
    self.transitionContext = nil;
}

如何使用:
ViewController1.m

ViewController2 *vc = [[ViewController2 alloc] init];
[self presentViewController:vc animated:YES completion:nil];

ViewController2.m

- (instancetype)init
{
    self = [super init];
    if ( self ) {
        _animator                    = [[PXPresentTransitionAnimator alloc] initWithModalViewController:self];
        self.transitioningDelegate                = self.animator;
        self.modalPresentationStyle               = UIModalPresentationCustom;
    }
    return self;
}

注:Present转场和Push转场大致上是差不多的,至少在原理上基本相似。但还是有差异的,如果处理不好这些差异,会导致黑屏,甚至崩溃的问题。

Present转场是从fromView转换到toView,根视图 fromView 也参与了转场。在转场结束后,fromView 可能依然可见。

而Push转场其根视图并未参与转场,转场结束后fromView 会被主动移出视图结构。

下一节会针对Push的转场动画进行代码诠释,这样对比着会更加清晰。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值