準備:
蘋果在iOS7之后,提供了自定義轉場API。使得我們可以對模態(present、dismiss)、導航控制器(push、pop)、標簽控制器的切換進行自定義轉場。前些天在項目空檔期,仿做了小紅書,用到了這個效果,所以今天,就以實戰為基礎做講解。具體詳細的介紹請參考:唐巧-iOS 視圖控制器轉場詳解、喵神-WWDC 2013 Session筆記 - iOS7中的ViewController切換。
效果:
是不是感覺仿的很逼真啊!哈哈哈。請允許我嘚瑟一下。好了下面進入正題。
下面先介紹幾個重要的協議:
UIViewControllerContextTransitioning
這個接口用來提供切換上下文給開發者使用,包含了從哪個VC到哪個VC等各類信息,一般不需要開發者自己實現。具體來說,iOS7的自定義切換目的之一就是切換相關代碼解耦,在進行VC切換時,做切換效果實現的時候必須要需要切換前后VC的一些信息,系統在新加入的API的比較的地方都會提供一個實現了該接口的對象,以供我們使用。
對于切換的動畫實現來說(這里先介紹簡單的動畫,在后面我會再引入手勢驅動的動畫),這個接口中最重要的方法有:
- -(UIView *)containerView; VC切換所發生的view容器,開發者應該將切出的view移除,將切入的view加入到該view容器中。
- -(UIViewController *)viewControllerForKey:(NSString *)key; 提供一個key,返回對應的VC?,F在的SDK中key的選擇只有UITransitionContextFromViewControllerKey和UITransitionContextToViewControllerKey兩種,分別表示將要切出和切入的VC。
- -(CGRect)initialFrameForViewController:(UIViewController *)vc; 某個VC的初始位置,可以用來做動畫的計算。
- -(CGRect)finalFrameForViewController:(UIViewController *)vc; 與上面的方法對應,得到切換結束時某個VC應在的frame。
- -(void)completeTransition:(BOOL)didComplete; 向這個context報告切換已經完成。
UIViewControllerAnimatedTransitioning
這個接口負責切換的具體內容,也即“切換中應該發生什么”。開發者在做自定義切換效果時大部分代碼會是用來實現這個接口。它只有兩個方法需要我們實現:
- -(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning >)transitionContext; 系統給出一個切換上下文,我們根據上下文環境返回這個切換所需要的花費時間(一般就返回動畫的時間就好了,SDK會用這個時間來在百分比驅動的切換中進行幀的計算,后面再詳細展開)。
- -(void)animateTransition:(id<UIViewControllerContextTransitioning >)transitionContext; 在進行切換的時候將調用該方法,我們對于切換時的UIView的設置和動畫都在這個方法中完成。
UIViewControllerTransitioningDelegate
這個接口的作用比較簡單單一,在需要VC切換的時候系統會像實現了這個接口的對象詢問是否需要使用自定義的切換效果。這個接口共有四個類似的方法:
- -(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;
前兩個方法是針對動畫切換的,我們需要分別在呈現VC和解散VC時,給出一個實現了UIViewControllerAnimatedTransitioning接口的對象(其中包含切換時長和如何切換)。后兩個方法涉及交互式切換,之后再說。
了解了上面的協議代理之后,咱們正式開始:
- 首先我們要自定義一個遵循
<UIViewControllerAnimatedTransitioning>
協議的動畫過渡管理對象,實現兩個必要方法:
//返回動畫事件
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
//所有的過渡動畫事務都在這個方法里面完成
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
- 另外根據需求我們也可以自定義一個繼承
UIPercentDrivenInteractiveTransition
的手勢過渡管理對象。使我們可以通過手勢觸發轉場動畫。如滑動屏幕左側,pop到上一頁。 - 我們今天要做的是導航控制器動畫,所以主要實現下面兩個代理方法
//返回轉場動畫過渡管理對象
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController NS_AVAILABLE_IOS(7_0);
//返回手勢過渡管理對象
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC NS_AVAILABLE_IOS(7_0);
直接上關鍵代碼了:
- 首先創建兩個需要跳轉的容器。(HomeViewController)VC1,(NotesDetailViewController)VC2.
- HomeViewController*的關鍵代碼
首先要遵循:UINavigationControllerDelegate
-(NavTransitioning *)pushTransition
{
if (!_pushTransition) {
_pushTransition = [[NavTransitioning alloc] init];
}
return _pushTransition;
}
#pragma mark UINavigationControllerDelegate
-(id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
{
if (operation == UINavigationControllerOperationPush && [toVC isKindOfClass:[NotesDetailViewController class]]) {
return self.pushTransition;
}else{
return nil;
}
}
-(id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
{
return self.interactionController;
} - (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.navigationController.delegate = self;
} - (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.navigationController.delegate == self) {
self.navigationController.delegate = nil;
}
}
- NotesDetailViewController*的關鍵代碼
首先要遵循:UINavigationControllerDelegate
#pragma mark - <UINavigationControllerDelegate>
-(id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController
{
if ([animationController isKindOfClass:[NavTransitioningBack class]]) {
return _interactivePopTransition;
}else{
return nil;
}
}
-(id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {
if ([toVC isKindOfClass:[HomeViewController class]])
{
return self.backTransition;
}else{
return nil;
}
}-(NavTransitioningBack *)backTransition
{
if (!_backTransition) {
_backTransition = [[NavTransitioningBack alloc]init];
}
return _backTransition;
}
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
}
-(void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.navigationController.delegate == self) {
self.navigationController.delegate = nil;
}
}
為該頁面添加手勢:
UIScreenEdgePanGestureRecognizer *popRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePopRecognizer:)];
popRecognizer.edges = UIRectEdgeLeft;
[self.view addGestureRecognizer:popRecognizer];
-(void)handlePopRecognizer:(UIScreenEdgePanGestureRecognizer *)recognizer
{
CGFloat progress = [recognizer translationInView:self.view].x / self.view.bounds.size.width;
progress = MIN(1.0, MAX(0.0, progress));
if (recognizer.state == UIGestureRecognizerStateBegan) {
self.interactivePopTransition = [[UIPercentDrivenInteractiveTransition alloc] init];
[self.navigationController popViewControllerAnimated:YES];
}else if (recognizer.state == UIGestureRecognizerStateChanged){
[self.interactivePopTransition updateInteractiveTransition:progress];
}else if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled){
if (progress > 0.5) {
[self.interactivePopTransition finishInteractiveTransition];
}else{
[self.interactivePopTransition cancelInteractiveTransition];
}
self.interactivePopTransition = nil;
}
}
- 下面開始創建遵循NavTransitioning
UIViewControllerAnimatedTransitioning
的動畫過渡管理類。
/**
* 這個接口負責切換的具體內容,也即“切換中應該發生什么”。開發者在做自定義切換效果時大部分代碼會是用來實現這個接口
*/
-(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 0.6f;
}
/**
* UIViewControllerAnimatedTransitioning 的協議都包含一個對象:transitionContext,通過這個對象能獲取到切換時的上下文信息,比如從哪個VC切換到哪個VC等。我們從 transitionContext 獲取 containerView,這是一個特殊的容器,切換時的動畫將在這個容器中進行;UITransitionContextFromViewControllerKey和UITransitionContextToViewControllerKey 就是從哪個VC切換到哪個VC
*/-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
//通過viewControllerForKey取出轉場前后的兩個控制器
HomeViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
NotesDetailViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
//這里有個重要的概念containerView,如果要對視圖做轉場動畫,視圖就必須要加入containerView中才能進行,可以理解containerView管理著所有做轉場動畫的視圖
UIView *containerView = [transitionContext containerView];
fromVC.currentIndexPath = [[fromVC.collectionView indexPathsForSelectedItems] firstObject];
NotesCollectionCell *cell = (NotesCollectionCell *)[fromVC.collectionView cellForItemAtIndexPath:fromVC.currentIndexPath];
//snapshotViewAfterScreenUpdates 獲取快照 對cell的imageView截圖保存成另一個視圖用于過渡,并將視圖轉換到當前控制器的坐標
UIView *snapShotView = [cell.itemImage snapshotViewAfterScreenUpdates:NO];
//坐標轉換
snapShotView.frame = fromVC.finalCellRect = [containerView convertRect:cell.itemImage.frame fromView:cell.itemImage.superview];
cell.itemImage.hidden = YES;
//設置toVC的frame
toVC.view.frame = [transitionContext finalFrameForViewController:toVC];
toVC.view.alpha = 0;
toVC.imageScrollView.hidden = YES;
fromVC.view.alpha = 0;
[containerView addSubview:toVC.view];
[containerView addSubview:snapShotView];
//轉場過程中要執行的動畫
[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.0f initialSpringVelocity:0.0f options:UIViewAnimationOptionCurveLinear animations:^{
[containerView layoutIfNeeded];
toVC.view.alpha = 1.0;
snapShotView.frame = [containerView convertRect:toVC.imageScrollView.frame fromView:toVC.imageScrollView.superview];
} completion:^(BOOL finished) {
toVC.imageScrollView.hidden = NO;
fromVC.view.alpha = 1;
cell.itemImage.hidden = NO;
[snapShotView removeFromSuperview];
//使用如下代碼標記整個轉場過程是否正常完成[transitionContext transitionWasCancelled]代表手勢是否取消了,如果取消了就傳NO表示轉場失敗,反之亦然,如果不用手勢的話直接傳YES也是可以的,但是無論如何我們都必須標記轉場的狀態,系統才知道處理轉場后的操作,否者認為你一直還在轉場中,會出現無法交互的情況,切記!
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
}];
}
- 下面開始創建遵循NavTransitioningBack
UIViewControllerAnimatedTransitioning
的動畫過渡管理類。
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 0.6f;
}
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
NotesDetailViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
HomeViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *containerView = [transitionContext containerView];
UIView *snapShotView = [fromVC.imageScrollView snapshotViewAfterScreenUpdates:NO];
snapShotView.frame = [containerView convertRect:fromVC.imageScrollView.frame fromView:fromVC.imageScrollView.superview];
fromVC.imageScrollView.hidden = YES;
NSLog(@"********1 %@",NSStringFromCGRect(toVC.view.frame));
toVC.view.frame = [transitionContext finalFrameForViewController:toVC];
toVC.view.alpha = 0;
NotesCollectionCell *cell = (NotesCollectionCell *)[toVC.collectionView cellForItemAtIndexPath:toVC.currentIndexPath];
cell.itemImage.hidden = YES;
[containerView addSubview:toVC.view];
[containerView addSubview:snapShotView];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toVC.view.alpha = 1.0;
snapShotView.frame = toVC.finalCellRect;
}completion:^(BOOL finished) {
[snapShotView removeFromSuperview];
fromVC.imageScrollView.hidden = NO;
cell.itemImage.hidden = NO;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
至此,結束。做的可能不是太完美,如果有什么問題,隨便提,共同提高。謝謝!