自定義轉場動畫(一)

江楓夜雨發表於2017-01-08

兩個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的轉場動畫進行程式碼詮釋,這樣對比著會更加清晰。

相關文章