iOS--轉場動畫,你學會了嗎?

語言: CN / TW / HK

前言

不積跬步無以至千里,不積小流無以成江海。學如逆水行舟,不進則退。我是平平無奇遊蕩於各平臺的搬運工。廢話不多說,直接給大家上乾貨,希望能對各位看官有小小幫助,優秀的人已經點讚了。

這裡直接上圖,有圖有真相。

webp2.jpg

webp3.jpg

這裡需要用到我上一篇文章,抖音的上下滑實現

學習這篇文章之前推薦看下喵神的iOS7中的ViewController轉場切換

如果對轉場不是很瞭解的話可能學習會有一些難度和疑問.

轉場呼叫程式碼

``` - (void)collectionView:(UICollectionView )collectionView didSelectItemAtIndexPath:(NSIndexPath )indexPath { AwemeListViewController *awemeVC = [[AwemeListViewController alloc] init]; awemeVC.transitioningDelegate = self; //0

// 1
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
// 2
CGRect cellFrame = cell.frame;
// 3
CGRect cellConvertedFrame = [collectionView convertRect:cellFrame toView:collectionView.superview];

//彈窗轉場
self.presentScaleAnimation.cellConvertFrame = cellConvertedFrame; //4

//消失轉場
self.dismissScaleAnimation.selectCell = cell; // 5
self.dismissScaleAnimation.originCellFrame  = cellFrame; //6
self.dismissScaleAnimation.finalCellFrame = cellConvertedFrame; //7

awemeVC.modalPresentationStyle = UIModalPresentationOverCurrentContext; //8
self.modalPresentationStyle = UIModalPresentationCurrentContext; //9

[self.leftDragInteractiveTransition wireToViewController:awemeVC];
[self presentViewController:awemeVC animated:YES completion:nil];

} ``0處程式碼使我們需要把當前的類做為轉場的代理\1這裡我們要拿出cell這個view\2拿出當前Cell的frame座標\3cell的座標轉成螢幕座標\4設定彈出時候需要cell在螢幕的位置座標\5設定消失轉場需要的選中cell檢視\6設定消失轉場原始cell座標位置\7設定消失轉場最終得cell螢幕座標位置 用於消失完成回到原來位置的動畫\8設定彈出得vc彈出樣式 這個用於顯示彈出VC得時候 預設底部使blua的高斯模糊\9` 設定當前VC的模態彈出樣式為當前的彈出上下文

5~7 步設定的消失轉場動畫 下面會講解

這裡我們用的是前面講上下滑的VC物件 大家不必擔心 當它是一個普通的UIViewController即可

實現轉場所需要的代理

首先在需要實現UIViewControllerTransitioningDelegate這個代理

**

``` #pragma mark -

pragma mark - UIViewControllerAnimatedTransitioning Delegate

  • (nullable id )animationControllerForPresentedController:(UIViewController )presented presentingController:(UIViewController )presenting sourceController:(UIViewController *)source {

    return self.presentScaleAnimation; //present VC }

  • (nullable id )animationControllerForDismissedController:(UIViewController *)dismissed { return self.dismissScaleAnimation; //dismiss VC }

  • (nullable id )interactionControllerForDismissal:(id )animator { return self.leftDragInteractiveTransition.isInteracting? self.leftDragInteractiveTransition: nil; } ```

這裡面我們看到我們分別返回了

  • 彈出動畫例項self.presentScaleAnimation

  • dismiss動畫例項self.dismissScaleAnimation

  • 以及self.leftDragInteractiveTransition例項用於負責轉場切換的具體實現

    所以我們需要在 當前的VC中宣告3個成員變數 並初始化

**

@property (nonatomic, strong) PresentScaleAnimation *presentScaleAnimation; @property (nonatomic, strong) DismissScaleAnimation *dismissScaleAnimation; @property (nonatomic, strong) DragLeftInteractiveTransition *leftDragInteractiveTransition;

並在viewDidLoad:方法中初始化一下

**

//轉場的兩個動畫 self.presentScaleAnimation = [[PresentScaleAnimation alloc] init]; self.dismissScaleAnimation = [[DismissScaleAnimation alloc] init]; self.leftDragInteractiveTransition = [DragLeftInteractiveTransition new];

這裡我說一下這三個成員都負責啥事

首先DragLeftInteractiveTransition類負責轉場的 手勢 過程,就是pan手勢在這個類裡面實現,並繼承自UIPercentDrivenInteractiveTransition類,這是iOS7以後系統提供的轉場基類必須在interactionControllerForDismissal:代理協議中返回這個類或者子類的例項物件,所以我們生成一個成員變數self.leftDragInteractiveTransition

其次是彈出present和消失dismiss的動畫類,這倆類其實是負責簡單的手勢完成之後的動畫.

這兩個類都是繼承自NSObject並實現UIViewControllerAnimatedTransitioning協議的類,這個協議裡面有 需要你複寫某些方法返回具體的動畫執行時間,和中間過程中我們需要的相關的容器檢視以及控制器的檢視例項,當我們自己執行完成之後呼叫相關的block回答告知轉場是否完成就行了.

**

``` @implementation PresentScaleAnimation - (NSTimeInterval)transitionDuration:(id )transitionContext{ return 0.3f; }

  • (void)animateTransition:(id )transitionContext{ UIViewController toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    if (CGRectEqualToRect(self.cellConvertFrame, CGRectZero)) { [transitionContext completeTransition:YES]; return; } CGRect initialFrame = self.cellConvertFrame; UIView
    containerView = [transitionContext containerView]; [containerView addSubview:toVC.view]; CGRect finalFrame = [transitionContext finalFrameForViewController:toVC]; NSTimeInterval duration = [self transitionDuration:transitionContext]; toVC.view.center = CGPointMake(initialFrame.origin.x + initialFrame.size.width/2, initialFrame.origin.y + initialFrame.size.height/2); toVC.view.transform = CGAffineTransformMakeScale(initialFrame.size.width/finalFrame.size.width, initialFrame.size.height/finalFrame.size.height); [UIView animateWithDuration:duration delay:0 usingSpringWithDamping:0.8 initialSpringVelocity:1 options:UIViewAnimationOptionLayoutSubviews animations:^{ toVC.view.center = CGPointMake(finalFrame.origin.x + finalFrame.size.width/2, finalFrame.origin.y + finalFrame.size.height/2); toVC.view.transform = CGAffineTransformMakeScale(1, 1); } completion:^(BOOL finished) { [transitionContext completeTransition:YES]; }]; } @end ```

很簡單.

消失的動畫 同上邊差不多

**

```

@interface DismissScaleAnimation () @end @implementation DismissScaleAnimation - (instancetype)init { self = [super init]; if (self) { _centerFrame = CGRectMake((ScreenWidth - 5)/2, (ScreenHeight - 5)/2, 5, 5); } return self; } - (NSTimeInterval)transitionDuration:(id )transitionContext{ return 0.25f; } - (void)animateTransition:(id )transitionContext{ UIViewController fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; // UINavigationController toNavigation = (UINavigationController )[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; // UIViewController toVC = [toNavigation viewControllers].firstObject;

UIView *snapshotView;
CGFloat scaleRatio;
CGRect finalFrame = self.finalCellFrame;
if(self.selectCell && !CGRectEqualToRect(finalFrame, CGRectZero)) {
    snapshotView = [self.selectCell snapshotViewAfterScreenUpdates:NO];
    scaleRatio = fromVC.view.frame.size.width/self.selectCell.frame.size.width;
    snapshotView.layer.zPosition = 20;
}else {
    snapshotView = [fromVC.view snapshotViewAfterScreenUpdates:NO];
    scaleRatio = fromVC.view.frame.size.width/ScreenWidth;
    finalFrame = _centerFrame;
}

UIView *containerView = [transitionContext containerView];
[containerView addSubview:snapshotView];

NSTimeInterval duration = [self transitionDuration:transitionContext];

fromVC.view.alpha = 0.0f;
snapshotView.center = fromVC.view.center;
snapshotView.transform = CGAffineTransformMakeScale(scaleRatio, scaleRatio);
[UIView animateWithDuration:duration
                      delay:0
     usingSpringWithDamping:0.8
      initialSpringVelocity:0.2
                   options:UIViewAnimationOptionCurveEaseInOut
                 animations:^{
                     snapshotView.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
                     snapshotView.frame = finalFrame;
                 } completion:^(BOOL finished) {
                     [transitionContext finishInteractiveTransition];
                     [transitionContext completeTransition:YES];
                     [snapshotView removeFromSuperview];
                 }];

} @end ```

我們重點需要說一下 轉場過渡的類DragLeftInteractiveTransition繼承自UIPercentDrivenInteractiveTransition負責轉場過程,

標頭檔案的宣告

**

``` @interface DragLeftInteractiveTransition : UIPercentDrivenInteractiveTransition / 是否正在拖動返回 標識是否正在使用轉場的互動中 */ @property (nonatomic, assign) BOOL isInteracting; / 設定需要返回的VC

@param viewController 控制器例項 / -(void)wireToViewController:(UIViewController )viewController; @end ```

實現

**

``` @interface DragLeftInteractiveTransition () @property (nonatomic, strong) UIViewController presentingVC; @property (nonatomic, assign) CGPoint viewControllerCenter; @property (nonatomic, strong) CALayer transitionMaskLayer; @end @implementation DragLeftInteractiveTransition

pragma mark -

pragma mark - override methods 複寫方法

-(CGFloat)completionSpeed{ return 1 - self.percentComplete; } - (void)updateInteractiveTransition:(CGFloat)percentComplete { NSLog(@"%.2f",percentComplete);

} - (void)cancelInteractiveTransition { NSLog(@"轉場取消"); } - (void)finishInteractiveTransition { NSLog(@"轉場完成"); } - (CALayer *)transitionMaskLayer { if (_transitionMaskLayer == nil) { _transitionMaskLayer = [CALayer layer]; } return _transitionMaskLayer; }

pragma mark -

pragma mark - private methods 私有方法

  • (void)prepareGestureRecognizerInView:(UIView)view { UIPanGestureRecognizer gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)]; [view addGestureRecognizer:gesture]; }

pragma mark -

pragma mark - event response 所有觸發的事件響應 按鈕、通知、分段控制元件等

  • (void)handleGesture:(UIPanGestureRecognizer )gestureRecognizer { UIView vcView = gestureRecognizer.view; CGPoint translation = [gestureRecognizer translationInView:vcView.superview]; if(!self.isInteracting && (translation.x < 0 || translation.y < 0 || translation.x < translation.y)) { return; } switch (gestureRecognizer.state) { case UIGestureRecognizerStateBegan:{ //修復當從右側向左滑動的時候的bug 避免開始的時候從又向左滑動 當未開始的時候 CGPoint vel = [gestureRecognizer velocityInView:gestureRecognizer.view]; if (!self.isInteracting && vel.x < 0) { self.isInteracting = NO; return; } self.transitionMaskLayer.frame = vcView.frame; self.transitionMaskLayer.opaque = NO; self.transitionMaskLayer.opacity = 1; self.transitionMaskLayer.backgroundColor = [UIColor whiteColor].CGColor; //必須有顏色不能透明 [self.transitionMaskLayer setNeedsDisplay]; [self.transitionMaskLayer displayIfNeeded]; self.transitionMaskLayer.anchorPoint = CGPointMake(0.5, 0.5); self.transitionMaskLayer.position = CGPointMake(vcView.frame.size.width/2.0f, vcView.frame.size.height/2.0f); vcView.layer.mask = self.transitionMaskLayer; vcView.layer.masksToBounds = YES;
        self.isInteracting = YES;
    }
    
        break;
    case UIGestureRecognizerStateChanged: {
        CGFloat progress = translation.x / [UIScreen mainScreen].bounds.size.width;
        progress = fminf(fmaxf(progress, 0.0), 1.0);
    
        CGFloat ratio = 1.0f - progress*0.5f;
        [_presentingVC.view setCenter:CGPointMake(_viewControllerCenter.x + translation.x * ratio, _viewControllerCenter.y + translation.y * ratio)];
        _presentingVC.view.transform = CGAffineTransformMakeScale(ratio, ratio);
        [self updateInteractiveTransition:progress];
        break;
    }
    case UIGestureRecognizerStateCancelled:
    case UIGestureRecognizerStateEnded:{
        CGFloat progress = translation.x / [UIScreen mainScreen].bounds.size.width;
        progress = fminf(fmaxf(progress, 0.0), 1.0);
        if (progress < 0.2){
            [UIView animateWithDuration:progress
                                  delay:0
                      options:UIViewAnimationOptionCurveEaseOut
                             animations:^{
                                 CGFloat w = [UIScreen mainScreen].bounds.size.width;
                                 CGFloat h = [UIScreen mainScreen].bounds.size.height;
                                 [self.presentingVC.view setCenter:CGPointMake(w/2, h/2)];
                                 self.presentingVC.view.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
                             } completion:^(BOOL finished) {
                                 self.isInteracting = NO;
                                 [self cancelInteractiveTransition];
                             }];
        }else {
            _isInteracting = NO;
            [self finishInteractiveTransition];
            [_presentingVC dismissViewControllerAnimated:YES completion:nil];
        }
        //移除 遮罩
        [self.transitionMaskLayer removeFromSuperlayer];
        self.transitionMaskLayer = nil;
    }
        break;
    default:
        break;
    

    } }

    pragma mark -

    pragma mark - public methods 公有方法

    -(void)wireToViewController:(UIViewController *)viewController { self.presentingVC = viewController; self.viewControllerCenter = viewController.view.center; [self prepareGestureRecognizerInView:viewController.view]; } @end ```

我們對外提供了一個wireToViewController:方法用於外部需要建立轉場使用.

前面的程式碼我們發現有一處

**

[self.leftDragInteractiveTransition wireToViewController:awemeVC]; [self presentViewController:awemeVC animated:YES completion:nil];

這裡就是需要把我們要彈出的上下滑VC例項傳進來,進來之後為VC的self.view加個pan手勢,

複寫方法中我們可以看到相關開始結束 完成過程的百分比相關方法複寫

**

```

pragma mark -

pragma mark - override methods 複寫方法

-(CGFloat)completionSpeed{ return 1 - self.percentComplete; } - (void)updateInteractiveTransition:(CGFloat)percentComplete { NSLog(@"%.2f",percentComplete); } - (void)cancelInteractiveTransition { NSLog(@"轉場取消"); } - (void)finishInteractiveTransition { NSLog(@"轉場完成"); } ```

看是手勢 出發前 先檢查一下是否如下條件

**

UIView *vcView = gestureRecognizer.view; CGPoint translation = [gestureRecognizer translationInView:vcView.superview]; if(!self.isInteracting && (translation.x < 0 || translation.y < 0 || translation.x < translation.y)) { return; }

拿出手勢作用的檢視,然後座標轉換,判斷當前是否已經開始了動畫,如果沒開始 或者x座標 < y座標是判斷當前是否是超過邊界範圍等等異常case處理.

開始的時候需要注意下

**

//修復當從右側向左滑動的時候的bug 避免開始的時候從又向左滑動 當未開始的時候 CGPoint vel = [gestureRecognizer velocityInView:gestureRecognizer.view]; if (!self.isInteracting && vel.x < 0) { self.isInteracting = NO; return; }

然後 開始的時候加個蒙版做為view.mask 這樣是為了解決tableView 超出contentSize的範圍要隱藏

剩下的就是中間過程

關鍵的核心程式碼

這個程式碼注意了 重中之重 [self updateInteractiveTransition:progress];

更新轉場的進度 這是這個類的自帶方法,呼叫就行了

最後 手勢結束

**

CGFloat progress = translation.x / [UIScreen mainScreen].bounds.size.width; progress = fminf(fmaxf(progress, 0.0), 1.0); if (progress < 0.2){ [UIView animateWithDuration:progress delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ CGFloat w = [UIScreen mainScreen].bounds.size.width; CGFloat h = [UIScreen mainScreen].bounds.size.height; [self.presentingVC.view setCenter:CGPointMake(w/2, h/2)]; self.presentingVC.view.transform = CGAffineTransformMakeScale(1.0f, 1.0f); } completion:^(BOOL finished) { self.isInteracting = NO; [self cancelInteractiveTransition]; }]; }else { _isInteracting = NO; [self finishInteractiveTransition]; [_presentingVC dismissViewControllerAnimated:YES completion:nil]; } //移除 遮罩 [self.transitionMaskLayer removeFromSuperlayer]; self.transitionMaskLayer = nil;

這裡設定0.2的容差 如果你覺得這個應該開放介面設定可自行封裝.

當用戶取消的話記得呼叫cancelInteractiveTransition方法取消

完成的話呼叫finishInteractiveTransition完成轉場

總結

整個過程還是比較簡單的 如果看過喵神的文章將會更加清晰的瞭解轉場的三個過程\ 就是 彈出和消失動畫 以及一箇中間轉場過程需要我們熟悉.

優化點: 在原開源工程中的demo轉場右滑是有bug的,我做了一下如下判斷

**

//修復當從右側向左滑動的時候的bug 避免開始的時候從又向左滑動 當未開始的時候 CGPoint vel = [gestureRecognizer velocityInView:gestureRecognizer.view]; if (!self.isInteracting && vel.x < 0) { self.isInteracting = NO; return; }

vel這個變數 其實是判斷當我們從右側劃入返回.修復了原來開源的一個bug

還有 原來開源中tableViewcontentSize以外 區域露在外部,我用了一個mask的蒙版遮住了顯示在外的區域.

唯一有些許遺憾的地方是抖音的左滑返回時候,有背景遮蓋透明的漸變.

如果需要抖音的轉場動畫Demo,可以加入iOS高階技術交流群,獲取Demo,以及更多iOS學習資料

轉載:原文地址