前言
今天帶來的是抖音的轉(zhuǎn)場動畫實現(xiàn) 廢話不多說上圖
這里需要用到前一篇文章的上下滑實現(xiàn)
學(xué)習(xí)這篇文章之前推薦看下喵神的iOS7中的ViewController轉(zhuǎn)場切換
如果對轉(zhuǎn)場不是很了解的話可能學(xué)習(xí)會有一些難度和疑問.
轉(zhuǎn)場調(diào)用代碼
- (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];
//彈窗轉(zhuǎn)場
self.presentScaleAnimation.cellConvertFrame = cellConvertedFrame; //4
//消失轉(zhuǎn)場
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
處代碼使我們需要把當(dāng)前的類做為轉(zhuǎn)場的代理
1
這里我們要拿出cell這個view
2
拿出當(dāng)前Cell的frame坐標(biāo)
3
cell的坐標(biāo)轉(zhuǎn)成屏幕坐標(biāo)
4
設(shè)置彈出時候需要cell在屏幕的位置坐標(biāo)
5
設(shè)置消失轉(zhuǎn)場需要的選中cell視圖
6
設(shè)置消失轉(zhuǎn)場原始cell坐標(biāo)位置
7
設(shè)置消失轉(zhuǎn)場最終得cell屏幕坐標(biāo)位置 用于消失完成回到原來位置的動畫
8
設(shè)置彈出得vc彈出樣式 這個用于顯示彈出VC得時候 默認(rèn)底部使blua的高斯模糊
9
設(shè)置當(dāng)前VC的模態(tài)彈出樣式為當(dāng)前的彈出上下文
5~7 步設(shè)置的消失轉(zhuǎn)場動畫 下面會講解
這里我們用的是前面講上下滑的VC對象 大家不必?fù)?dān)心 當(dāng)它是一個普通的UIViewController即可
實現(xiàn)轉(zhuǎn)場所需要的代理
首先在需要實現(xiàn)UIViewControllerTransitioningDelegate
這個代理
#pragma mark -
#pragma mark - UIViewControllerAnimatedTransitioning Delegate
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
return self.presentScaleAnimation; //present VC
}
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
return self.dismissScaleAnimation; //dismiss VC
}
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
return self.leftDragInteractiveTransition.isInteracting? self.leftDragInteractiveTransition: nil;
}
這里面我們看到我們分別返回了
彈出動畫實例
self.presentScaleAnimation
dismiss動畫實例
self.dismissScaleAnimation
-
以及
self.leftDragInteractiveTransition
實例用于負(fù)責(zé)轉(zhuǎn)場切換的具體實現(xiàn)所以我們需要在 當(dāng)前的VC中聲明3個成員變量 并初始化
@property (nonatomic, strong) PresentScaleAnimation *presentScaleAnimation;
@property (nonatomic, strong) DismissScaleAnimation *dismissScaleAnimation;
@property (nonatomic, strong) DragLeftInteractiveTransition *leftDragInteractiveTransition;
并在viewDidLoad:
方法中初始化一下
//轉(zhuǎn)場的兩個動畫
self.presentScaleAnimation = [[PresentScaleAnimation alloc] init];
self.dismissScaleAnimation = [[DismissScaleAnimation alloc] init];
self.leftDragInteractiveTransition = [DragLeftInteractiveTransition new];
這里我說一下這三個成員都負(fù)責(zé)啥事
首先DragLeftInteractiveTransition
類負(fù)責(zé)轉(zhuǎn)場的 手勢 過程,就是pan手勢在這個類里面實現(xiàn),并繼承自UIPercentDrivenInteractiveTransition
類,這是iOS7以后系統(tǒng)提供的轉(zhuǎn)場基類必須在interactionControllerForDismissal:
代理協(xié)議中返回這個類或者子類的實例對象,所以我們生成一個成員變量self.leftDragInteractiveTransition
其次是彈出present和消失dismiss的動畫類,這倆類其實是負(fù)責(zé)簡單的手勢完成之后的動畫.
這兩個類都是繼承自NSObject并實現(xiàn)UIViewControllerAnimatedTransitioning
協(xié)議的類,這個協(xié)議里面有 需要你復(fù)寫某些方法返回具體的動畫執(zhí)行時間,和中間過程中我們需要的相關(guān)的容器視圖以及控制器的視圖實例,當(dāng)我們自己執(zhí)行完成之后調(diào)用相關(guān)的block回答告知轉(zhuǎn)場是否完成就行了.
@implementation PresentScaleAnimation
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext{
return 0.3f;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)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 <UIViewControllerContextTransitioning>)transitionContext{
return 0.25f;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)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
我們重點需要說一下 轉(zhuǎn)場過渡的類DragLeftInteractiveTransition
繼承自UIPercentDrivenInteractiveTransition
負(fù)責(zé)轉(zhuǎn)場過程,
頭文件的聲明
@interface DragLeftInteractiveTransition : UIPercentDrivenInteractiveTransition
/** 是否正在拖動返回 標(biāo)識是否正在使用轉(zhuǎn)場的交互中 */
@property (nonatomic, assign) BOOL isInteracting;
/**
設(shè)置需要返回的VC
@param viewController 控制器實例
*/
-(void)wireToViewController:(UIViewController *)viewController;
@end
實現(xiàn)
@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 復(fù)寫方法
-(CGFloat)completionSpeed{
return 1 - self.percentComplete;
}
- (void)updateInteractiveTransition:(CGFloat)percentComplete {
NSLog(@"%.2f",percentComplete);
}
- (void)cancelInteractiveTransition {
NSLog(@"轉(zhuǎn)場取消");
}
- (void)finishInteractiveTransition {
NSLog(@"轉(zhuǎn)場完成");
}
- (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 所有觸發(fā)的事件響應(yīng) 按鈕、通知、分段控件等
- (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:{
//修復(fù)當(dāng)從右側(cè)向左滑動的時候的bug 避免開始的時候從又向左滑動 當(dāng)未開始的時候
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:
方法用于外部需要創(chuàng)建轉(zhuǎn)場使用.
前面的代碼我們發(fā)現(xiàn)有一處
[self.leftDragInteractiveTransition wireToViewController:awemeVC];
[self presentViewController:awemeVC animated:YES completion:nil];
這里就是需要把我們要彈出的上下滑VC實例傳進來,進來之后為VC的self.view
加個pan
手勢,
復(fù)寫方法中我們可以看到相關(guān)開始結(jié)束 完成過程的百分比相關(guān)方法復(fù)寫
#pragma mark -
#pragma mark - override methods 復(fù)寫方法
-(CGFloat)completionSpeed{
return 1 - self.percentComplete;
}
- (void)updateInteractiveTransition:(CGFloat)percentComplete {
NSLog(@"%.2f",percentComplete);
}
- (void)cancelInteractiveTransition {
NSLog(@"轉(zhuǎn)場取消");
}
- (void)finishInteractiveTransition {
NSLog(@"轉(zhuǎn)場完成");
}
看是手勢 出發(fā)前 先檢查一下是否如下條件
UIView *vcView = gestureRecognizer.view;
CGPoint translation = [gestureRecognizer translationInView:vcView.superview];
if(!self.isInteracting &&
(translation.x < 0 ||
translation.y < 0 ||
translation.x < translation.y)) {
return;
}
拿出手勢作用的視圖,然后坐標(biāo)轉(zhuǎn)換,判斷當(dāng)前是否已經(jīng)開始了動畫,如果沒開始 或者x坐標(biāo) < y坐標(biāo)是判斷當(dāng)前是否是超過邊界范圍等等異常case處理.
開始的時候需要注意下
//修復(fù)當(dāng)從右側(cè)向左滑動的時候的bug 避免開始的時候從又向左滑動 當(dāng)未開始的時候
CGPoint vel = [gestureRecognizer velocityInView:gestureRecognizer.view];
if (!self.isInteracting && vel.x < 0) {
self.isInteracting = NO;
return;
}
然后 開始的時候加個蒙版做為view.mask 這樣是為了解決tableView 超出contentSize的范圍要隱藏
剩下的就是中間過程
關(guān)鍵的核心代碼
[self updateInteractiveTransition:progress];
更新轉(zhuǎn)場的進度 這是這個類的自帶方法,調(diào)用就行了
最后 手勢結(jié)束
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;
這里設(shè)置0.2的容差 如果你覺得這個應(yīng)該開放接口設(shè)置可自行封裝.
當(dāng)用戶取消的話記得調(diào)用cancelInteractiveTransition
方法取消
完成的話調(diào)用finishInteractiveTransition
完成轉(zhuǎn)場
總結(jié)
整個過程還是比較簡單的 如果看過喵神的文章將會更加清晰的了解轉(zhuǎn)場的三個過程
就是 彈出和消失動畫 以及一個中間轉(zhuǎn)場過程需要我們熟悉.
優(yōu)化點: 在原開源工程中的demo轉(zhuǎn)場右滑是有bug的,我做了一下如下判斷
//修復(fù)當(dāng)從右側(cè)向左滑動的時候的bug 避免開始的時候從又向左滑動 當(dāng)未開始的時候
CGPoint vel = [gestureRecognizer velocityInView:gestureRecognizer.view];
if (!self.isInteracting && vel.x < 0) {
self.isInteracting = NO;
return;
}
vel
這個變量 其實是判斷當(dāng)我們從右側(cè)劃入返回.修復(fù)了原來開源的一個bug
還有 原來開源中tableView
的contentSize
以外 區(qū)域露在外部,我用了一個mask的蒙版遮住了顯示在外的區(qū)域.
唯一有些許遺憾的地方是抖音的左滑返回時候,有背景遮蓋透明的漸變.
如果需要抖音的轉(zhuǎn)場動畫Demo,可以加iOS高級技術(shù)交流群:937194184,獲取Demo,以及更多iOS學(xué)習(xí)資料
轉(zhuǎn)載:原文地址