1. 程式人生 > >自定義轉場詳解(一)

自定義轉場詳解(一)

assign pda 好的 led hint ext hid case delegate

前言

本文是我學習了onevcat的這篇轉場入門做的一點筆記。

今天我們來實現一個簡單的自定義轉場,我們先來看看這篇文章將要實現的一個效果圖吧:
技術分享圖片

過程詳解

熱身準備

我們先創建一個工程,首先用storyboard快速的創建兩個控制器,一個作為主控制器,叫ViewController,另外一個作為present出來的控制器,叫PresentViewController,並且用autoLayout快速搭建好界面。就像這樣:
技術分享圖片

我們先做好點擊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:
技術分享圖片

主要內容

接下來,要接觸我們今天要講的主要內容了,我們用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;
        }
    }

現在看下我們的效果:
技術分享圖片
相對於上面系統自帶的效果來說,我們在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,到此為止,我們的一個自定義轉場動畫就算了完成了。

自定義轉場詳解(一)