iOS7新特性 ViewController轉場切換(二) 系統視圖控制器容器的切換動畫—push pop present dismis – iPhone手機開發技術文章 iPhone軟體開發教學課程

@上一章,介紹瞭主要的iOS7所增加的API,可以發現,它們不是一個個死的方法,蘋果給我們開發者提供的是都是協議接口,所以我們能夠很好的單獨提出來寫成一個個類,在裡面實現我們各種自定義效果.

1.先來看看實現UIViewControllerAnimatedTransitioning的自定義動畫類

/**
 *  自定義的動畫類
 *  實現協議------>@protocol UIViewControllerAnimatedTransitioning
 *  這個接口負責切換的具體內容,也即“切換中應該發生什麼”
 */
@interface MTHCustomAnimator : NSObject 

@end

@implementation MTHCustomAnimator

// 系統給出一個切換上下文,我們根據上下文環境返回這個切換所需要的花費時間
- (NSTimeInterval)transitionDuration:(id)transitionContext
{
    return 1.0;
}

// 完成容器轉場動畫的主要方法,我們對於切換時的UIView的設置和動畫都在這個方法中完成
- (void)animateTransition:(id)transitionContext
{
    // 可以看做為destination ViewController
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    // 可以看做為source ViewController
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    // 添加toView到容器上
    [[transitionContext containerView] addSubview:toViewController.view];
    toViewController.view.alpha = 0.0;
    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        // 動畫效果有很多,這裡就展示個左偏移
        fromViewController.view.transform = CGAffineTransformMakeTranslation(-320, 0);
        toViewController.view.alpha = 1.0;
    } completion:^(BOOL finished) {
        fromViewController.view.transform = CGAffineTransformIdentity;
        // 聲明過渡結束-->記住,一定別忘瞭在過渡結束時調用 completeTransition: 這個方法
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
}

PS:從協議中兩個方法可以看出,上面兩個必須實現的方法需要一個轉場上下文參數,這是一個遵從UIViewControllerContextTransitioning 協議的對象。通常情況下,當我們使用系統的類時,系統框架為我們提供的轉場代理(Transitioning Delegates),為我們創建瞭轉場上下文對象,並把它傳遞給動畫控制器。

// MainViewController
@interface MTHMainViewController () 

@property (nonatomic,strong) MTHCustomAnimator *customAnimator;
@property (nonatomic,strong) PDTransitionAnimator *minToMaxAnimator;
@property (nonatomic,strong) MTHNextViewController *nextVC;
// 交互控制器 (Interaction Controllers) 通過遵從 UIViewControllerInteractiveTransitioning 協議來控制可交互式的轉場。
@property (strong, nonatomic) UIPercentDrivenInteractiveTransition* interactionController;
@end

@implementation MTHMainViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.navigationItem.title = @"Demo";
    self.view.backgroundColor = [UIColor yellowColor];
    // 設置代理
    self.navigationController.delegate = self;
    // 設置轉場動畫
    self.customAnimator = [[MTHCustomAnimator alloc] init];
    self.minToMaxAnimator = [PDTransitionAnimator new];

    self.nextVC = [[MTHNextViewController alloc] init];
    // Present的代理和自定義設置
    _nextVC.transitioningDelegate = self;
    _nextVC.modalPresentationStyle = UIModalPresentationCustom;
    
    // Push
    UIButton *pushButton = [UIButton buttonWithType:UIButtonTypeSystem];
    pushButton.frame = CGRectMake(140, 200, 40, 40);
    [pushButton setTitle:@"Push" forState:UIControlStateNormal];
    [pushButton addTarget:self action:@selector(push) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:pushButton];
    
    // Present
    UIButton *modalButton = [UIButton buttonWithType:UIButtonTypeSystem];
    modalButton.frame = CGRectMake(265, 500, 50, 50);
    [modalButton setTitle:@"Modal" forState:UIControlStateNormal];
    [modalButton addTarget:self action:@selector(modal) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:modalButton];
    
    // 實現交互操作的手勢
    UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(didClickPanGestureRecognizer:)];
    [self.navigationController.view addGestureRecognizer:panRecognizer];
}


- (void)push
{
    [self.navigationController pushViewController:_nextVC animated:YES];
}

- (void)modal
{
    [self presentViewController:_nextVC animated:YES completion:nil];
}

#pragma mark - UINavigationControllerDelegate iOS7新增的2個方法
// 動畫特效
- (id) navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
{
    /**
     *  typedef NS_ENUM(NSInteger, UINavigationControllerOperation) {
     *     UINavigationControllerOperationNone,
     *     UINavigationControllerOperationPush,
     *     UINavigationControllerOperationPop,
     *  };
     */
    if (operation == UINavigationControllerOperationPush) {
        return self.customAnimator;
    }else{
        return nil;
    }
}

// 交互
- (id )navigationController:(UINavigationController*)navigationController                           interactionControllerForAnimationController:(id )animationController
{
    /**
     *  在非交互式動畫效果中,該方法返回 nil
     *  交互式轉場,自我理解意思是,用戶能通過自己的動作來(常見:手勢)控制,不同於系統缺省給定的push或者pop(非交互式)
     */
    return _interactionController;
}

#pragma mark - Transitioning Delegate (Modal)
// 前2個用於動畫
-(id)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
    self.minToMaxAnimator.animationType = AnimationTypePresent;
    return _minToMaxAnimator;
}

-(id)animationControllerForDismissedController:(UIViewController *)dismissed
{
    self.minToMaxAnimator.animationType = AnimationTypeDismiss;
    return _minToMaxAnimator;
}

// 後2個用於交互
- (id )interactionControllerForPresentation:(id )animator
{
    return _interactionController;
}

- (id )interactionControllerForDismissal:(id )animator
{
    return nil;
}

@以上實現的是非交互的轉場,指的是完全按照系統指定的切換機制,用戶無法中途取消或者控制進度切換.那怎麼來實現交互轉場呢:

UIPercentDrivenInteractiveTransition實現瞭UIViewControllerInteractiveTransitioning接口的類,,可以用一個百分比來控制交互式切換的過程。我們在手勢識別中隻需要告訴這個類的實例當前的狀態百分比如何,系統便根據這個百分比和我們之前設定的遷移方式為我們計算當前應該的UI渲染,十分方便。具體的幾個重要方法:
-(void)updateInteractiveTransition:(CGFloat)percentComplete 更新百分比,一般通過手勢識別的長度之類的來計算一個值,然後進行更新。之後的例子裡會看到詳細的用法
-(void)cancelInteractiveTransition 報告交互取消,返回切換前的狀態
–(void)finishInteractiveTransition 報告交互完成,更新到切換後的狀態

#pragma mark - 手勢交互的主要實現--->UIPercentDrivenInteractiveTransition
- (void)didClickPanGestureRecognizer:(UIPanGestureRecognizer*)recognizer
{
    UIView* view = self.view;
    if (recognizer.state == UIGestureRecognizerStateBegan) {
        // 獲取手勢的觸摸點坐標
        CGPoint location = [recognizer locationInView:view];
        // 判斷,用戶從右半邊滑動的時候,推出下一個VC(根據實際需要是推進還是推出)
        if (location.x > CGRectGetMidX(view.bounds) && self.navigationController.viewControllers.count == 1){
            self.interactionController = [[UIPercentDrivenInteractiveTransition alloc] init];
            //
            [self presentViewController:_nextVC animated:YES completion:nil];
        }
    } else if (recognizer.state == UIGestureRecognizerStateChanged) {
        // 獲取手勢在視圖上偏移的坐標
        CGPoint translation = [recognizer translationInView:view];
        // 根據手指拖動的距離計算一個百分比,切換的動畫效果也隨著這個百分比來走
        CGFloat distance = fabs(translation.x / CGRectGetWidth(view.bounds));
        // 交互控制器控制動畫的進度
        [self.interactionController updateInteractiveTransition:distance];
    } else if (recognizer.state == UIGestureRecognizerStateEnded) {
        CGPoint translation = [recognizer translationInView:view];
        // 根據手指拖動的距離計算一個百分比,切換的動畫效果也隨著這個百分比來走
        CGFloat distance = fabs(translation.x / CGRectGetWidth(view.bounds));
        // 移動超過一半就強制完成
        if (distance > 0.5) {
            [self.interactionController finishInteractiveTransition];
        } else {
            [self.interactionController cancelInteractiveTransition];
        }
        // 結束後一定要置為nil
        self.interactionController = nil;
    }
}

@最後,給大傢分享一個動畫特效:類似於飛兔雲傳的發送ViewController切換

@implementation PDTransitionAnimator

#define Switch_Time 1.2
- (NSTimeInterval)transitionDuration:(id )transitionContext {
    return Switch_Time;
}

#define Button_Width 50.f
#define Button_Space 10.f
- (void)animateTransition:(id )transitionContext {
    UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];

    UIView * toView = toViewController.view;
    UIView * fromView = fromViewController.view;
    
    if (self.animationType == AnimationTypeDismiss) {
        // 這個方法能夠高效的將當前顯示的view截取成一個新的view.你可以用這個截取的view用來顯示.例如,也許你隻想用一張截圖來做動畫,畢竟用原始的view做動畫代價太高.因為是截取瞭已經存在的內容,這個方法隻能反應出這個被截取的view當前的狀態信息,而不能反應這個被截取的view以後要顯示的信息.然而,不管怎麼樣,調用這個方法都會比將view做成截圖來加載效率更高.
        UIView * snap = [toView snapshotViewAfterScreenUpdates:YES];
        [transitionContext.containerView addSubview:snap];
        [snap setFrame:CGRectMake([UIScreen mainScreen].bounds.size.width - Button_Width - Button_Space, [UIScreen mainScreen].bounds.size.height - Button_Width - Button_Space, Button_Width, Button_Width)];
        
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            [snap setFrame:[UIScreen mainScreen].bounds];
        } completion:^(BOOL finished) {
            [UIView animateWithDuration:0.5 animations:^{
                [[transitionContext containerView] addSubview:toView];
                snap.alpha = 0;
            } completion:^(BOOL finished) {
                [snap removeFromSuperview];
                [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
            }];
        }];
    } else {
        UIView * snap2 = [toView snapshotViewAfterScreenUpdates:YES];
        [transitionContext.containerView addSubview:snap2];
        UIView * snap = [fromView snapshotViewAfterScreenUpdates:YES];
        [transitionContext.containerView addSubview:snap];
        
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            [snap setFrame:CGRectMake([UIScreen mainScreen].bounds.size.width - Button_Width - Button_Space+ (Button_Width/2), [UIScreen mainScreen].bounds.size.height - Button_Width - Button_Space + (Button_Width/2), 0, 0)];
        } completion:^(BOOL finished) {
            [UIView animateWithDuration:0.5 animations:^{
                //snap.alpha = 0;
            } completion:^(BOOL finished) {
                [snap removeFromSuperview];
                [snap2 removeFromSuperview];
                [[transitionContext containerView] addSubview:toView];
                // 切記不要忘記瞭噢
                [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
            }];
        }];
        
    }
}

@其中,snapshotViewAfterScreenUpdates 方法的解釋,我也不是很懂,反正初級來說會用就行,還可以參照下面的解析:

在iOS7 以前, 獲取一個UIView的快照有以下步驟: 首先創建一個UIGraphics的圖像上下文,然後將視圖的layer渲染到該上下文中,從而取得一個圖像,最後關閉圖像上下文,並將圖像顯示在UIImageView中。現在我們隻需要一行代碼就可以完成上述步驟瞭:

[view snapshotViewAfterScreenUpdates:NO];
這個方法制作瞭一個UIView的副本,如果我們希望視圖在執行動畫之前保存現在的外觀,以備之後使用(動畫中視圖可能會被子視圖遮蓋或者發生其他一些變化),該方法就特別方便。
afterUpdates參數表示是否在所有效果應用在視圖上瞭以後再獲取快照。例如,如果該參數為NO,則立馬獲取該視圖現在狀態的快照,反之,以下代碼隻能得到一個空白快照:
[view snapshotViewAfterScreenUpdates:YES];
[view setAlpha:0.0];
由於我們設置afterUpdates參數為YES,而視圖的透明度值被設置成瞭0,所以方法將在該設置應用在視圖上瞭之後才進行快照,於是乎屏幕空空如也。另外就是……你可以對快照再進行快照……繼續快照……


最後,主要代碼已經給出,我已經上傳完整代碼瞭,大傢有意向的可以去下載下來看看(唉,主要是CSDN不支持gif動態圖,好蛋疼,朋友在博客園的博客都支持,但是我又不喜歡博客園的風格,看來以後自己弄個域名博客是王道,後續我也會把我的一些代碼分享到github上)
@轉載請註明,轉自iOS@迷糊小書童
@待續…..下一章給出自定義ViewController容器的轉場.

發佈留言