屏幕左边缘右滑返回,TabBar 滑动切换,你是否喜欢并十分依赖这两个操做,甚至以为 App 不支持这类操做的话简直反人类?这两个操做在大屏时代极大提高了操做效率,其背后的技术即是今天的主题:视图控制器转换(View Controller Transition)。ios
经过学习seedante的iOS 视图控制器转场详解:从入门到精通的这篇文章,对视图转场有了新的认识,写这篇文章的目的,主要是记录一下本身对视图转场动画的理解并作一个总结方便之后查阅。git
目前为止,官方支持如下几种方式的自定义转场:github
UINavigationController
中 push 和 popUITabBarController
中切换 TabUIModalPresentationCustom
这两种模式UICollectionViewController
的布局转场:仅限于 UICollectionViewController 与 UINavigationController 结合的转场方式转场动画的本质: 下一场景(子 VC)的视图替换当前场景(子 VC)的视图以及相应的控制器(子 VC)的替换,表现为当前视图消失和下一视图出现。 iOS 7 以协议的方式开放了自定义转场的 API,协议的好处是再也不拘泥于具体的某个类,只要是遵照该协议的对象都能参与转场,很是灵活。主要有一下几个协议:bash
对于非交互式动画咱们只须要实现转场代理
和动画控制器
协议便可,对于交互式动画咱们还须要实现交互控制器
协议。👇下面对每一个协议进行详细介绍。ide
/*返回已经实现的`动画控制器`,若是返回nil则使用系统默认的动画效果*/
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC NS_AVAILABLE_IOS(7_0);
/*返回已经实现的`交互控制器`,若是返回nil则不支持手势交互*/
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController NS_AVAILABLE_IOS(7_0);
复制代码
一样做为容器控制器,UITabBarController 的转场代理和 UINavigationController 相似,经过相似的方法提供动画控制器,不过UINavigationControllerDelegate
的代理方法里提供了操做类型,但UITabBarControllerDelegate
的代理方法没有提供滑动的方向信息,须要咱们来获取滑动的方向。布局
/*同理返回已经实现的`动画控制器`,返回nil是默认效果*/
- (nullable id <UIViewControllerAnimatedTransitioning>)tabBarController:(UITabBarController *)tabBarController animationControllerForTransitionFromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC;
/*返回已经实现的`交互控制器`,返回nil则不支持用户交互*/
- (nullable id <UIViewControllerInteractiveTransitioning>)tabBarController:(UITabBarController *)tabBarController interactionControllerForAnimationController: (id <UIViewControllerAnimatedTransitioning>)animationController NS_AVAILABLE_IOS(7_0);
复制代码
Modal 转场的代理协议UIViewControllerTransitioningDelegate
是 iOS 7 新增的,其为 presentation 和 dismissal 转场分别提供了动画控制器。 UIPresentationController
只在 iOS 8中可用,经过available关键字能够解决 API 的版本差别。学习
/*present时调用,返回已经实现的`动画控制器`*/
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
/*dissmis时调用,返回已经实现的`动画控制器`*/
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
/*交互动画present时调用,返回已经实现的`交互控制器`*/
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
/*交互动画dissmiss时调用,返回已经实现的`交互控制器`*/
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;
/*ios8新增的协议*/
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0);
复制代码
Modal 转场的代理由 presentedVC 的transitioningDelegate
属性来提供,这与前两种容器控制器的转场不同,另外,须要将 presentedVC 的modalPresentationStyle
属性设置为.Custom或.FullScreen,只有这两种模式下才支持自定义转场,该属性默认值为.FullScreen。当与 UIPresentationController
配合时该属性必须为.Custom。动画
动画控制器负责添加视图以及执行动画,遵照UIViewControllerAnimatedTransitioning
协议,该协议要求实现如下方法:ui
/*返回动画执行时间,通常0.5s就足够了*/
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
/*核心方法,作一些动画相关的操做*/
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
复制代码
UIKit 在转场开始前生成遵照转场环境协议<UIViewControllerContextTransitioning>
的对象 transitionContext,它有如下几个方法来提供动画控制器须要的信息:spa
/*获取容器视图,转场发生的地方*/
UIView *containerView = [transitionContext containerView];
/*获取参与转场的视图控制器*/
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
/*获取参与参与转场的视图View*/
UIView *fromView;
UIView *toView;
if ([transitionContext respondsToSelector:@selector(viewForKey:)]) {
//iOS8新增的方法
fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
toView = [transitionContext viewForKey:UITransitionContextToViewKey];
}else{
//iOS8以前的方法
fromView = fromVC.view;
toView = toVC.view;
}
复制代码
经过viewForKey
:获取的视图是viewControllerForKey
:返回的控制器的根视图,或者 nil。viewForKey
:方法返回 nil 只有一种状况: UIModalPresentationCustom
模式下的 Modal 转场 ,经过此方法获取 presentingView
时获得的将是 nil,所以在 Modal 转场中,较稳妥的方法是从 fromVC 和 toVC 中获取 fromView
和 toView
。
须要注意的地方:
addSubview:
,某些场合你可能须要调整 fromView 和 toView 的显示顺序,总之将之加入到containerView
里就好了。transitionWasCancelled()
方法来获取转场的结果,而后使用completeTransition:
来通知系统转场过程结束。fromView
会从视图结构中移除,UIKit 自动替咱们作了这事,你也能够手动处理提早将 fromView 移除,这彻底取决于你。dismissal
转场中不要像其余的转场那样将 toView(presentingView) 加入 containerView
,不然presentingView
将消失不见,而应用则也极可能假死。而 FullScreen 模式下可使用与前面的容器类 VC 转场一样的代码,(Modal 转场在 Custom 模式下必须区分 presentation 和 dismissal 转场,而在 FullScreen 模式下能够不用这么作)。iOS8引入了UIPresentationController
类,该类接管了 UIViewController 的显示过程,为其提供转场和视图管理支持,model模式必须是Custom
。 UIPresentationController
类主要给 Modal 转场带来了如下几点变化:
👇介绍相关的方法:
/**在呈现过渡即将开始的时候被调用的*/
- (void)presentationTransitionWillBegin;
/**在呈现过渡结束时被调用的*/
- (void)presentationTransitionDidEnd:(BOOL)completed;
/**在退出过渡即将开始的时候被调用的*/
- (void)dismissalTransitionWillBegin;
/**在退出的过渡结束时被调用的*/
- (void)dismissalTransitionDidEnd:(BOOL)completed;
/*提供给动画控制器使用的视图,默认返回 presentedVC.view,经过重写该方法返回其余视图,但必定要是 presentedVC.view 的上层视图。对 presentedView 的外观进行定制。*/
- (UIView *)presentedView;
/*返回动画结束后的`presented view`的frame*/
- (CGRect)frameOfPresentedViewInContainerView;
复制代码
有个问题,没法直接访问动画控制器,不知道转场的持续时间,怎么与转场过程同步?这时候前面提到的用处甚少的转场协调器(Transition Coordinator)将在这里派上用场。该对象可经过UIViewController
的transitionCoordinator()
方法获取,这是 iOS 7 为自定义转场新增的 API,该方法只在控制器处于转场过程当中才返回一个与当前转场有关的有效对象,其余时候返回 nil。 转场协调器遵照<UIViewControllerTransitionCoordinator>
协议,它含有如下几个方法:
/*与动画控制器中的转场动画同步,执行其余动画*/
- (BOOL)animateAlongsideTransition:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))animation completion:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))completion;
/*与动画控制器中的转场动画同步,在指定的视图内执行动画*/
- (BOOL)animateAlongsideTransitionInView:(nullable UIView *)view animation:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))animation completion:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))completion;
复制代码
在 iOS 7 中,Custom 模式的 Modal 转场里,presentingView
不会被移除,若是咱们要移除它并妥善恢复会破坏动画控制器的独立性使得第三方动画控制器没法直接使用;在 iOS 8 中,UIPresentationController
解决了这点,给予了咱们选择的权力,经过重写下面的方法来决定 presentingView
是否在 presentation 转场结束后被移除:
- (BOOL)shouldRemovePresentersView
复制代码
返回 true 时,presentation 结束后presentingView
被移除,在 dimissal 结束后 UIKit 会自动将 presentingView 恢复到原来的视图结构中。此时,Custom 模式与 FullScreen
模式下无异,彻底没必要理会前面 dismissal 转场部分的差别了。
实现交互效果须要在非交互转场的基础上实现下面两个方法:
<UIViewControllerInteractiveTransitioning>
协议的对象,不过系统已经打包好了现成的类UIPercentDrivenInteractiveTransition
供咱们使用。咱们不须要作任何配置,仅仅在转场代理的相应方法中提供一个该类实例便能工做。另外交互控制器必须有动画控制器才能工做。/*更新转场进度,进度数值范围为0.0~1.0。*/
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
/*取消转场,转场动画从当前状态返回至转场发生前的状态。*/
- (void)cancelInteractiveTransition;
/*完成转场,转场动画从当前状态继续直至结束。*/
- (void)finishInteractiveTransition;
复制代码
交互控制协议<UIViewControllerInteractiveTransitioning>
只有一个必须实现的方法:
/*交互转场,获取转场上下文*/
- (void)startInteractiveTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
复制代码
须要注意的地方: 若是在转场代理中提供了交互控制器,而转场发生时并无方法来驱动转场进程(好比手势),转场过程将一直处于开始阶段没法结束,应用界面也会失去响应:在 NavigationController 中点击 NavigationBar 也能实现 pop 返回操做,但此时没有了交互手段的支持,转场过程卡壳;在 TabBarController 的代理里提供交互控制器存在一样的问题,点击 TabBar 切换页面时也没有实现交互控制。所以仅在确实处于交互状态时才提供交互控制器,可使用一个变量来标记
交互状态,该变量由交互手势来更新状态。
- (void)leftPan:(UIScreenEdgePanGestureRecognizer *)recognizer{
CGPoint currentPoint = [recognizer translationInView:recognizer.view];
CGFloat progress = currentPoint.x/CGRectGetWidth(recognizer.view.frame);
progress = MIN(1, MAX(0, progress));
if (recognizer.state == UIGestureRecognizerStateBegan){
//使用变量来标记交互状态
_isStart = YES;
[self.controller.navigationController popViewControllerAnimated:YES];
}else if (recognizer.state == UIGestureRecognizerStateChanged){
[self updateInteractiveTransition:progress];
}else if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled){
_isStart = NO;
if (progress > 0.4) {
[self finishInteractiveTransition];
}else{
[self cancelInteractiveTransition];
}
}
}
复制代码
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController{
return _percentModel.isStart ? _percentModel : nil;
}
复制代码
实现思路: 获取UICollectionView
当前选中的Cell上的ImageView
,而且对ImageView进行截图,将ToView和截图ImageView添加到ContainerView
,以动画的方式将截图imageView的frame转换为toView的ImageView的Frame。下面请看Push详细代码,Pop代码同理:
- (void)PushAnimation:(id <UIViewControllerContextTransitioning>)transitionContext{
/*切出和切入的VC*/
FistViewController *fromVC = (FistViewController*)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
DetailController *toVC = (DetailController*)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
/*VC切换所发生的view容器,开发者应该将切出的view移除,将切入的view加入到该view容器中。*/
UIView *containerView = [transitionContext containerView];
/*对选中cell的imageView截图*/
NSIndexPath *indexPath = [[fromVC.myCollection indexPathsForSelectedItems] firstObject];
fromVC.selectIndexPath = indexPath;
FirstCollectionViewCell *selectCell = (FirstCollectionViewCell*)[fromVC.myCollection cellForItemAtIndexPath:indexPath];
UIView *snapShotView = [selectCell.avatarimageView snapshotViewAfterScreenUpdates:NO];
// 将rect从view中转换到当前视图中,返回在当前视图中的rect
snapShotView.frame = fromVC.finalCellRect = [containerView convertRect:selectCell.avatarimageView.frame fromView:selectCell.avatarimageView.superview];
selectCell.avatarimageView.hidden = YES;
/*设置第二个控制器的位置,透明度*/
toVC.view.frame = [transitionContext finalFrameForViewController:toVC];
toVC.view.alpha = 0;
toVC.avatarImageView.hidden = YES;
CGPoint currentCenter = toVC.textView.center;
toVC.textView.center = CGPointMake(currentCenter.x + 30, currentCenter.y);
/*将动画先后的两个View添加到containerView,注意添加顺序,snapShotView在上面*/
[containerView addSubview:toVC.view];
[containerView addSubview:snapShotView];
/*开始动画*/
[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.6 initialSpringVelocity:1.0 options:UIViewAnimationOptionCurveLinear animations:^{
//textView中心点
toVC.textView.center = currentCenter;
//透明度,frame变换
toVC.view.alpha = 1.0;
snapShotView.frame = [containerView convertRect:toVC.avatarImageView.frame toView:toVC.avatarImageView.superview];
} completion:^(BOOL finished) {
toVC.avatarImageView.hidden = NO;
selectCell.avatarimageView.hidden = NO;
[snapShotView removeFromSuperview];
/*告诉系统动画结束*/
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
}];
}
复制代码
- (void)pushAnimation:(id <UIViewControllerContextTransitioning>)transitionContext{
//获取fromVC和toVC,以及containerView
FirstViewController *fromVC = (FirstViewController*)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
SecondViewController *toVC = (SecondViewController*)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *containerView = [transitionContext containerView];
//设置遮罩
CGPoint buttonCenter = fromVC.targetButton.center;
CGRect buttonFrame = fromVC.targetButton.frame;
CGFloat paddingX = MAX(buttonCenter.x, CGRectGetWidth(fromVC.view.frame) - buttonCenter.x);
CGFloat paddingY = MAX(buttonCenter.y, CGRectGetHeight(fromVC.view.frame) - buttonCenter.y);
CGFloat distance = sqrtf((paddingX * paddingX) + (paddingY * paddingY));
UIBezierPath *startPath = [UIBezierPath bezierPathWithOvalInRect:buttonFrame];
UIBezierPath *endPath = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(buttonFrame, -(distance - CGRectGetWidth(buttonFrame)/2.0), -(distance - CGRectGetHeight(buttonFrame)/2.0))];
CAShapeLayer *maskLayer = [CAShapeLayer layer];
//将参与变换的视图添加到contaier上
[containerView addSubview:toVC.view];
toVC.view.layer.mask = maskLayer;
//防止最后闪屏一下
maskLayer.path = endPath.CGPath;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"path"];
animation.duration = 0.6;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
animation.delegate = self;
animation.fromValue = (__bridge id)startPath.CGPath;
animation.toValue = (__bridge id)endPath.CGPath;
[animation setValue:@"maskAnimation" forKey:AnimationKey];
[animation setValue:transitionContext forKey:TransitionContextKey];
[maskLayer addAnimation:animation forKey:nil];
}
复制代码
动画结束后要将toView或者FromView的遮罩设置为nil。
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
if ([[anim valueForKey:AnimationKey] isEqualToString:@"maskAnimation"]){
id <UIViewControllerContextTransitioning> transitionContext = [anim valueForKey:TransitionContextKey];
SecondViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
toVC.view.layer.mask = nil;
//完成动画
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
}else if ([[anim valueForKey:AnimationKey]isEqualToString:@"maskAnimationPop"]){
id <UIViewControllerContextTransitioning> transitionContext = [anim valueForKey:TransitionContextKey];
SecondViewController *FromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
FromVC.view.layer.mask = nil;
//完成动画
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
}
}
复制代码
UIPresentationController
类。原理上面已经解释的很清楚了,直接上代码:
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
/*获取controller,ContainerView*/
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *containerView = [transitionContext containerView];
UIView *fromView;
UIView *toView;
if ([transitionContext respondsToSelector:@selector(viewForKey:)]) {
fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
toView = [transitionContext viewForKey:UITransitionContextToViewKey];
}else{
fromView = fromVC.view;
toView = toVC.view;
}
CGRect fromViewFrame = [transitionContext initialFrameForViewController:fromVC];
CGRect toViewFrame = [transitionContext finalFrameForViewController:toVC];
/*进行动画*/
if (_type == AnimationTypePresent) {
CGRect orginalFrame = CGRectZero;
orginalFrame.origin = CGPointMake(CGRectGetMinX(containerView.bounds), CGRectGetMaxY(containerView.bounds));
orginalFrame.size = toViewFrame.size;
toView.frame = orginalFrame;
[containerView addSubview:toView];
}else if (_type == AnimationTypeDissmiss){
/**
处理 Dismissal 转场,按照上一小节的结论,.Custom模式下不要将 toView添加到 containerView
*/
fromViewFrame = CGRectOffset(fromViewFrame, 0, CGRectGetHeight(containerView.bounds));
}
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
if (_type == AnimationTypePresent) {
toView.frame = toViewFrame;
}else if (_type == AnimationTypeDissmiss){
fromView.frame = fromViewFrame;
}
} completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
复制代码
动画控制协调器中执行背景透明度变化,与动画控制器中的转场动画同步。
self.dimmingView.alpha = 0.0;
[self.presentingViewController.transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
self.dimmingView.alpha = 0.7;
self.presentingViewController.view.transform = CGAffineTransformScale(CGAffineTransformIdentity, 0.92, 0.92);
} completion:nil];
复制代码