iOS之Custom UIViewController Transition


本文學習下自定義ViewController的切換,從無交互的到交互式切換。

(本文已同步到我的小站:icocoa,歡迎訪問。)

iOS7中定義了3個協議:

UIViewControllerTransitioningDelegate:
用於支持自定義切換或切換交互,定義了一組供animator對象實現的協議,來自定義切換。
可以為動畫的三個階段單獨提供animator對象:presenting,dismissing,interacting。

UIViewControllerAnimatedTransitioning:
主要用於定義切換時的動畫。這個動畫的運行時間是固定的,而且無法進行交互。

UIViewControllerInteractiveTransitioning:
負責交互動畫的對象。
該對象是通過加快/減慢動畫切換的過程,來響應觸發事件或者隨時間變化的程序輸入。對象也可以提高切換的逆過程來響應變化。
比如iOS7上NavController響應手指滑動來切換viewController
如果要提供交互,那么也需要提供實現UIViewControllerAnimatedTransitioning的對象,這個對象可以就是之前實現UIViewControllerInteractiveTransitioning的對象,也可以不是。
如果不需要(動畫按預先設置的進行),則可以自己實現。如果要提供交互,那么也需要實現UIViewControllerAnimatedTransitioning。

 

上述是API文檔中的說明,我們按圖索驥,根據說明一步一步來實現一個無交互的切換動畫。

為了方便,我在一個viewController A里添加按鈕,點擊后以present modal的方式跳轉到viewController B。B中也放置一個按鈕,用來回到A。
為了支持自定義transition,iOS7中UIViewController多了transitioningDelegate的屬性。這個delegate需要實現相關的protocol,可以是viewcontroller本身。不過,這樣的話,很顯然不利於自定義部分的重用。因此我們新建一個類:

@interface ZJTransitionDelegateObj : NSObject<UIViewControllerTransitioningDelegate>

@end

然后實現delegate,UIViewControllerTransitioningDelegate定義了4個protocol,后2個是用於交互時用的,這里我們只需實現前2個。

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;

- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;

前2個返回的是實現 UIViewControllerAnimatedTransitioning 協議的對象,這里我們返回self,這樣意味着我們的ZJTransitionDelegateObj類還需要實現相應的協議:

// This is used for percent driven interactive transitions, as well as for container controllers that have companion animations that might need to
// synchronize with the main animation. 
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext;
// This method can only  be a nop if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;


@optional

// This is a convenience and if implemented will be invoked by the system when the transition context's completeTransition: method is invoked.
- (void)animationEnded:(BOOL) transitionCompleted;

根據說明,我們可以看到主要是實現第2個協議。transitionContext是一個實現UIViewControllerContextTransitioning協議的對象,再進一步查看該協議,可以看到一系列方法,具體的就不詳細展開,看一下代碼:

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
{
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *containView = [transitionContext containerView];
    [containView addSubview:toViewController.view];
        
    CGRect rect = toViewController.view.frame;
    rect.origin.x = -320;
    rect.origin.y = -rect.size.height;
    toViewController.view.frame = rect;
    [UIView animateKeyframesWithDuration:1.5 delay:0 options:UIViewKeyframeAnimationOptionLayoutSubviews animations:^{
        CGRect frame = rect;
        frame.origin.x = 0;
        frame.origin.y = 0;
        toViewController.view.frame = frame;
    } completion:^(BOOL finished) {
        
        
        
        [transitionContext completeTransition:YES];
    }];

}

內容很簡單,這里需要注意的是 [transitionContext completeTransition:YES] 很重要。如果沒有使用,系統會不知道當前的transition是否已經結束,這樣造成的后果:使app進入某種未知狀態,比如presentingViewController能看到新view但是無法和用戶交互。關於這一點,Apple把它放置在頭文件里說明了,所以我推薦大家遇到問題的時候,不妨先直接查看頭文件中的注釋說明(xCode中按住command后鼠標點擊類名)。

接下來,看一下app,發現present的方式是以對角的方式出現了。如果你不小心點擊了ViewCOntroller B的dismiss按鈕,發現之前的view也以同樣的方式出現了。這是因為我們尚未做present和dismiss的區分。接下來給ZJTransitionDelegateObj增加增加Bool屬性

@interface ZJTransitionDelegateObj ()
@property (nonatomic) BOOL isPresent;
@end

並在協議中賦值:

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
{
    self.isPresent = YES;
    return self;
}

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
{
    self.isPresent = NO;
    return self;
}

然后修改動畫:

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
{
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIViewController *fromViewController = [transitionContext viewControllerForKey: UITransitionContextFromViewControllerKey];
    UIView *containView = [transitionContext containerView];
    CGRect rect = toViewController.view.frame;
    if (self.isPresent)
    {
        [containView addSubview:toViewController.view];
        
        
        rect.origin.x = - rect.size.width;
        rect.origin.y = - rect.size.height;
        toViewController.view.frame = rect;
        [UIView animateKeyframesWithDuration:1.5 delay:0 options:UIViewKeyframeAnimationOptionLayoutSubviews animations:^{
            CGRect frame = rect;
            
            frame.origin.x = 0;
            frame.origin.y = 0;
            toViewController.view.frame = frame;
        } completion:^(BOOL finished) {
            
            
            
            [transitionContext completeTransition:YES];
        }];

    }
    else
    {
        
        [containView insertSubview:toViewController.view atIndex:0];
        rect = fromViewController.view.frame;
   
        [UIView animateKeyframesWithDuration:1.5 delay:0 options:UIViewKeyframeAnimationOptionLayoutSubviews animations:^{
            CGRect frame = rect;
            
            frame.origin.x = - rect.size.width;
            frame.origin.y = - rect.size.height;
            fromViewController.view.frame = frame;
        } completion:^(BOOL finished)
        {
            [fromViewController.view removeFromSuperview];
            [transitionContext completeTransition:YES];
        }];

    }
    
}

假設A present B,那么fromViewController和toViewController在present和dismiss是正好相反的,如圖:

而且present時,container view中沒有subview,需要自己添加B的view。而dismiss的時候,container view中已經添加了B的view,所以要先把A的view添加到最底層,然后對B的view做動畫,最后還要把它移除。

這樣,一個簡單的custom transition 就已經完成了。

下面,我們趁熱打鐵,來實現一個交互式的custom transion。何謂交互式的custom transion呢?舉個簡單的例子,有個navController,push了viewController A,在A頁面可以通過手指從左向右的滑動的方式pop到上一級ViewController。在滑動的過程中,你也可以取消當前的pop。這種交互的方式,是Apple在iOS7中推薦的。

我們看一下WWDC中的講義,來領會一下這樣的一個過程:

 上圖就是交互式動畫過程中的狀態變化,其中更新,結束和取消的幾個狀態,是需要客戶端調用來通知系統的。

 

根據WWDC的說明,最簡單的實現交互式動畫的方法就是通過繼承 UIPercentDrivenInteractiveTransition。

下面我們嘗試實現一個交互式動畫,我選擇的是對nav的pop添加交互式動畫,通過兩個手指向內滑動pop當前的viewcontroller。與此同時,點擊返回鍵能正常的pop當前的viewcontroller。

首先根據WWDC的例子,添加一個新類:

#import <UIKit/UIKit.h>

@interface ZJSliderTransition : UIPercentDrivenInteractiveTransition
- (instancetype)initWithNavigationController:(UINavigationController *)nc;

@property(nonatomic,assign) UINavigationController *parent;
@property(nonatomic,assign,getter = isInteractive) BOOL interactive;

@end

注意源文件中需要添加一些變量,並且在初始化的時候添加gesture:

#import "ZJSliderTransition.h"

@interface ZJSliderTransition ()
{
    CGFloat _startScale;
}
@end



@implementation ZJSliderTransition
- (instancetype)initWithNavigationController:(UINavigationController *)nc;
{
    if (self = [super init])
    {
        self.parent = nc;
        
        UIPinchGestureRecognizer *pintchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
        [self.parent.topViewController.view addGestureRecognizer:pintchGesture];
    }
    return self;
}


- (void)handlePinch:(UIPinchGestureRecognizer *)gr {
    CGFloat scale = [gr scale];
    switch ([gr state]) {
        case UIGestureRecognizerStateBegan:
            self.interactive = YES; _startScale = scale;
       self.parent.delegate = self.parent.topViewController;

[self.parent popViewControllerAnimated:YES];
            break;
        case UIGestureRecognizerStateChanged: {
            CGFloat percent = (1.0 - scale/_startScale);
            [self updateInteractiveTransition: (percent <= 0.0) ? 0.0 : percent];
            break;
        }
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled:
            if([gr velocity] >= 0.0 || [gr state] == UIGestureRecognizerStateCancelled)
                [self cancelInteractiveTransition];
            else
                [self finishInteractiveTransition];
            self.interactive = NO;
        break;
        default:
            break;
    }
}
@end

由此可見,gesture的狀態和交互式的狀態,是一一對應的。因為我們希望添加的動畫不影響正常的返回pop,我們在pinch操作開始的時候,再設置navController的delegate。當然,這樣的設置有點怪。

接下來,就是添加我們的sliderTransition。為了和其他transition區分,我們給ZJToViewController添加一個BOOL屬性:isPopInterActive。

當isPopInterActive為YES的時候,我們才去准備navController的delegate需要實現的相關對象。

ZJViewController類添加的部分:

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    if (self.isPopInterActive)
    {
        _sliderTransition = [[ZJSliderTransition alloc] initWithNavigationController:self.navigationController];

    }
}
- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
    if (self.isPopInterActive)
    {
        self.navigationController.delegate = nil;
    }
}



#pragma mark - UINavigationController
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
{
    if (self.isPopInterActive)
    {
        return [[ZJSliderTransitionDelegateObj alloc] init];
    }
    
    else
    {
        return nil;
    }
}

- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                          interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController
{
    if (self.isPopInterActive)
    {
        return self.sliderTransition;
    }
    return nil;
    
}

然后,在masterViewController部分,push一個新的ZJViewController即可。具體的效果請自行編譯運行文后的源碼。

從構建一個交互式的transition可以看到,交互式本身就被設計為一個單獨的“模塊”,方便開發的時候集成。這也再次體現出蘋果對開發者的“體貼”。

最后附上本篇的代碼下載地址

由於最近轉戰C,iOS的內容拖了又拖,如果有疏漏的地方,歡迎大家指正,謝謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM