iOS--轉場動畫,你學會了嗎?
前言
不積跬步無以至千里,不積小流無以成江海。學如逆水行舟,不進則退。我是平平無奇遊蕩於各平臺的搬運工。廢話不多說,直接給大家上乾貨,希望能對各位看官有小小幫助,優秀的人已經點讚了。
圖
這裡直接上圖,有圖有真相。
這裡需要用到我上一篇文章,抖音的上下滑實現
學習這篇文章之前推薦看下喵神的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
- (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
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
還有 原來開源中tableView
的contentSize
以外 區域露在外部,我用了一個mask的蒙版遮住了顯示在外的區域.
唯一有些許遺憾的地方是抖音的左滑返回時候,有背景遮蓋透明的漸變.
如果需要抖音的轉場動畫Demo,可以加入iOS高階技術交流群,獲取Demo,以及更多iOS學習資料
轉載:原文地址