自定義轉場詳解(一)

前言

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

今天咱們來實現一個簡單的自定義轉場,咱們先來看看這篇文章將要實現的一個效果圖吧:
圖1框架

過程詳解

熱身準備

咱們先建立一個工程,首先用storyboard快速的建立兩個控制器,一個做爲主控制器,叫ViewController,另一個做爲present出來的控制器,叫PresentViewController,而且用autoLayout快速搭建好界面。就像這樣:
圖2iview

咱們先作好點擊ViewController上面的按鈕,present出 PresentViewController,點擊PresentViewController上面的按鈕,dismiss掉PresentViewController的邏輯。這裏有兩個注意點:ide

  1. 由於此處我使用了segue,因此在ViewController按鈕點擊的時候,咱們只須要這樣調用就行。學習

    #pragma mark - 點我彈出
        -(IBAction)presentBtnClick:(UIButton *)sender {
            [self performSegueWithIdentifier:@"PresentSegue" sender:nil];
        }
  2. 咱們平時寫dismiss的時候,通常都會是在第二個控制器中直接給self發送dismissViewController的相關方法。在如今的SDK中,若是當前的VC是被顯示的話,這個消息會被直接轉發到顯示它的VC去。可是這並非一個好的實現,違反了程序設計的哲學,也很容易掉到坑裏。因此咱們用標準的delegate 方式實現 dismiss動畫

首先咱們在PresentViewController控制器中申明一個代理方法。ui

#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的點擊事件中,讓代理去完成關閉當前控制器的工做。this

#pragma mark - 點擊關閉
    - (IBAction)closeBtnClick:(UIButton *)sender {
        if (self.delegate && [self.delegate respondsToSelector:@selector(dismissViewController:)]) {
            [self.delegate dismissViewController:self];
        }
    }

與此同時,在ViewController中須要設置PresentViewController的代理,而且實現代理方法:atom

- (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,到此爲止,咱們的一個自定義轉場動畫就算了完成了。

相關文章
相關標籤/搜索