准备:
苹果在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。现在的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]];
}];
}
至此,结束。做的可能不是太完美,如果有什么问题,随便提,共同提高。谢谢!