之前看过王巍写的ViewController切换来实现自定义转场,之后又看了唐巧的一篇控制器转场详解的文章,篇幅很长,讲的特别详细。转场有不少小细节,在这自己整理下,巩固下知识。
为了表示方便,转场前的控制器用FromVC表示,视图用FromView表示;转场后的控制器用ToVC表示,视图用ToView表示。
转场的基本步骤:
1.实现转场代理
// 1.UIViewController 的 transitioningDelegate 遵循的协议。
UIViewControllerTransitioningDelegate
//2. UINavigationController 的 delegate 遵循的协议
UINavigationControllerDelegate。
//3. UITabBarController 的 delegate 遵循的协议。
UITabBarControllerDelegate
2.转场的动画控制器
//转场动画遵循的协议
UIViewControllerAnimatedTransitioning
该协议必须要实现的两个方法:
- -(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext;转场动画时间
- -(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext;转场动画的具体实现(各种炫酷的动画都是在这里实现的)
3.转场上下文环境
//转场上下文环境遵循的协议
UIViewControllerContextTransitioning
- 不需要开发者自己实现,该协议提供了转场前后控制器的一写信息。
- 其中几个重要的方法:
- -(UIView *)containerView; 动画发生的view容器。
- -(UIViewController *) viewControllerForKey:(NSString *)key; 通过key值,获取转场前后的控制器。
- key:
1 . UITransitionContextFromViewControllerKey 转场前
2 . UITransitionContextToViewControllerKey 转场后
- key:
- -(CGRect)initialFrameForViewController:(UIViewController *)vc; 获取控制器初始frame。
- -(CGRect)finalFrameForViewController:(UIViewController *)vc;获取控制器结束时的frame。
- -(void)completeTransition:(BOOL)didComplete; 转场结束时必须要调用的方法。
4.手势驱动视图
系统提供了一个实现UIViewControllerInteractiveTransitioning协议的UIPercentDrivenInteractiveTransition类,所以我们只要继承这个类,添加手势并在手势实现的方法中告知当前视图的百分比,通过此逻辑来驱动视图,在调用类中定义的一些方法就很容易实现视图的交互。
其中几个重要的方法:
- -(void)updateInteractiveTransition:(CGFloat)percentComplete;更新视图的百分比。
- -(void)cancelInteractiveTransition;取消视图的交互,返回交互前的状态。
- -(void)finishInteractiveTransition;完成视图交互,更新到交互后的状态。
通过UINavigationController 实现转场
通过UICollectionView简单的实现神奇移动的效果。
设置好代理,并调用push方法
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
PushedViewController *pushVC=[[PushedViewController alloc]init];
self.indexPath = indexPath;
self.navigationController.delegate = pushVC;
[self.navigationController pushViewController:pushVC animated:YES];
}
实现UINavigationControllerDelegate代理方法,此处返回实现UIViewControllerAnimatedTransitioning协议的动画控制器对象,若返回nil,则调用系统默认方式。
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
// return nil;
return [PushPopTransfromAnimation transformWithType:(operation == UINavigationControllerOperationPush ? PushPopTransformPush :PushPopTransformPop)];
}
实现UIViewControllerAnimatedTransitioning协议的类:
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext
{
return 0.8;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
if (_type == PushPopTransformPush) {
[self pushAnimation:transitionContext];
}
else if (_type == PushPopTransformPop)
{
[self popAnimation:transitionContext];
}
}
#pragma mark -
-(void)pushAnimation:(id <UIViewControllerContextTransitioning>)transitionContext
{
//1.获取控制器
CollectionViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
PushedViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
//获取FromVC中点击的cell
MyCollectionViewCell *cell = (MyCollectionViewCell *)[fromVC.collectionView cellForItemAtIndexPath:fromVC.indexPath];
UIView *containerView = [transitionContext containerView];
UIView *tempView = [cell.myImgView snapshotViewAfterScreenUpdates:NO];
tempView.frame =[cell.myImgView convertRect:cell.myImgView.frame toView:containerView];
toVC.view.alpha = 0;
//2.添加ToView
[containerView addSubview:toVC.view];
[containerView addSubview:tempView];
toVC.imgView.hidden = YES;
[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.5 initialSpringVelocity:0 options:0 animations:^{
tempView.frame = [toVC.imgView convertRect:toVC.imgView.bounds toView:containerView] ;
toVC.view.alpha = 1;
} completion:^(BOOL finished) {
toVC.imgView.hidden = NO;
[tempView removeFromSuperview];
//3.转场完成
[transitionContext completeTransition:YES];
}];
}
-(void)popAnimation:(id <UIViewControllerContextTransitioning>)transitionContext
{
//1.
PushedViewController *fromVC = (PushedViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
CollectionViewController *toVC = (CollectionViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
//2.
UIView *containerView = [transitionContext containerView];
MyCollectionViewCell *cell = (MyCollectionViewCell *)[toVC.collectionView cellForItemAtIndexPath:toVC.indexPath];
CGRect cellInitRect = [cell.myImgView convertRect:cell.myImgView.frame toView:containerView];
UIView *temp = fromVC.imgView;
[containerView addSubview:toVC.view];
[containerView sendSubviewToBack:toVC.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:0 animations:^{
temp.frame = cellInitRect;
fromVC.view.alpha = 0;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES]];
}];
注意:1. 在转场结束时,FromView会从视图结构中主动移除 。2.push和pop动画时都需要将ToView添加到ContainerView视图中。
Model转场
Model转场与UINavigationController 和 UITabBarController转场有些不同,需要注意。Model转场属性modalPresentationStyle可设置不同的模式,模式设置的不同,结果也不同,如:
UIModalPresentationFullScreen模式下:presentation后,FromView会被主动从视图结构中移除,dismissal时,ToView可以自己手动添加到containerView中,也可以不用手动添加,系统会自己添加。
UIModalPresentationCustom模式下:presentation后,FromView不会被主动移除,这与FullScreen模式是不同的,dismissal时,切记也不用添加ToView视图到containerView中,否则dismiss方法之后,presenting(将要显示)的视图不见了。
添加手势驱动
Model方式实现转场并添加手势,在实现UIViewControllerTransitioningDelegate协议 和 UIViewControllerAnimatedTransitioning协议基础上还要实现一个继承UIPercentDrivenInteractiveTransition类的子类。
- UIViewControllerAnimatedTransitioning协议接口中有- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator方法,这个方法中就是用来返回UIPercentDrivenInteractiveTransition类的对象的。
- 该方法中还要判断是否正在手势驱动,手动驱动返回UIPercentDrivenInteractiveTransition类的对象,没有手动驱动则返回nil,如果没有判断返回nil,则返回时没有任何响应。
示例代码:
PresentToViewController *presentToVC=[[PresentToViewController alloc]init];
presentToVC.transitioningDelegate=self; //转场代理
presentToVC.delegate=self; //自定义代理,用于dismiss
[self.swipeVC handleDismissViewController:presentToVC];//用于手势处理
//presentToVC.modalPresentationStyle = UIModalPresentationCustom;
[self presentViewController:presentToVC animated:YES completion:nil];
实现UIViewControllerTransitioningDelegate协议方法:
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
return self.presentAnimation;
}
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
return self.dismissAnimation;
}
//返回手势对象
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator
{
return self.swipeVC.interacting ? self.swipeVC : nil;
}
实现UIViewControllerAnimatedTransitioning的类:
-(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 0.5;
}
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
if (_type == YJPPresentAnimationPresent) {
[self presentVC:transitionContext];
}
else if (_type == YJPPresentAnimationDismiss)
{
[self dismissVC:transitionContext];
}
}
-(void)presentVC:(id<UIViewControllerContextTransitioning>)transitionContext
{
PresentToViewController *toVC=(PresentToViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *containerView = [transitionContext containerView];
toVC.view.frame = CGRectMake(0, 0, 1, containerView.frame.size.height);
toVC.view.center = containerView.center;
[containerView addSubview:toVC.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toVC.view.frame = CGRectMake(0, 0, containerView.frame.size.width, containerView.frame.size.height);
toVC.view.center = containerView.center;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
-(void)dismissVC:(id<UIViewControllerContextTransitioning>)transitionContext
{
PresentToViewController *fromVC = (PresentToViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
PresentFromViewController *toVC = (PresentFromViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
//添加视图
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toVC.view];
[containerView sendSubviewToBack:toVC.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromVC.view.frame = CGRectMake(0, 0, 1, containerView.frame.size.height);
fromVC.view.center = containerView.center;
} completion:^(BOOL finished) {
//因添加了手势驱动,这里要判断是否取消
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
集成UIPercentDrivenInteractiveTransition类,实现子类:
- (void)handleDismissViewController:(PresentToViewController *)controller
{
self.dissmissVC = controller;
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panAction:)];
[controller.view addGestureRecognizer:pan];
}
-(CGFloat )completionSpeed
{
return 1 - self.persentCompleted;
}
-(void)panAction:(UIPanGestureRecognizer *)gesture
{
CGPoint point = [gesture translationInView:self.dissmissVC.view];
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
{
self.interacting = YES;
[self.dissmissVC dismissViewControllerAnimated:YES completion:nil];
break;
}
case UIGestureRecognizerStateChanged:
{
CGFloat persent = (point.y/500) <=1 ?(point.y/500):1;//百分比的程度
self.persentCompleted = persent;
[self updateInteractiveTransition:persent];
break;
}
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateEnded:
{
self.interacting = NO;
if (gesture.state == UIGestureRecognizerStateCancelled) {
[self cancelInteractiveTransition];
}else{
[self finishInteractiveTransition];
}
break;
}
default:
break;
}
}
扩散效果
通过自定义了一个UINavigationController实现扩散效果的转场。转场的方法基本就那几个方法,大同小异,不同的是动画的实现方式。扩散本质其实就是设置了视图的mask属性,并添加了动画,通过UIBezierPath、CAShapeLayer、CABasicAnimation就可以实现。
转场动画的核心代码:
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext
{
return 0.5;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
if (_type == MaskTransitionPush) {
[self pushAnimaiton:transitionContext];
}
else if (_type == MaskTransitionPop)
{
[self popAnimaiton:transitionContext];
}
}
-(void)pushAnimaiton:(id <UIViewControllerContextTransitioning>)transitionContext
{
//用于动画结束时使用
self.transitionContext = transitionContext;
MaskedViewController *toVC = (MaskedViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toVC.view];
//计算圆的半径
UIButton *button = toVC.button;
CGFloat x =toVC.view.frame.size.width - button.center.x;
CGFloat y =toVC.view.frame.size.height - button.center.y;
CGFloat radius =sqrt(x*x+y*y);
UIBezierPath *initPath = [UIBezierPath bezierPathWithOvalInRect:button.frame];
UIBezierPath *finalPath = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(button.frame, -radius, -radius)];
//
CAShapeLayer *shaperLayer = [CAShapeLayer layer];
shaperLayer.path = finalPath.CGPath;
toVC.view.layer.mask = shaperLayer;
//添加动画
CABasicAnimation *baseAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
baseAnimation.fromValue = (id)initPath.CGPath;
baseAnimation.toValue = (id)finalPath.CGPath;
baseAnimation.duration = [self transitionDuration:transitionContext];
baseAnimation.delegate =self; //设置代理
[shaperLayer addAnimation:baseAnimation forKey:nil];
}
-(void)popAnimaiton:(id <UIViewControllerContextTransitioning>)transitionContext
{
self.transitionContext = transitionContext;
MaskedViewController *fromVC= (MaskedViewController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
SystemViewController *toVC = (SystemViewController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
//添加视图
UIView *containerView = [transitionContext containerView];
[containerView addSubview:toVC.view];
[containerView sendSubviewToBack:toVC.view];
//计算半径
UIButton *button = fromVC.button;
CGFloat x =fromVC.view.frame.size.width - button.center.x;
CGFloat y =fromVC.view.frame.size.height - button.center.y;
CGFloat radius =sqrt(x*x+y*y);
UIBezierPath *initPath = [UIBezierPath bezierPathWithOvalInRect:button.frame];
UIBezierPath *finalPath = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(button.frame, -radius, -radius)];
//
CAShapeLayer *shaperLayer = [CAShapeLayer layer];
shaperLayer.path = initPath.CGPath;
fromVC.view.layer.mask = shaperLayer;
//
CABasicAnimation *baseAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
baseAnimation.fromValue = (id)finalPath.CGPath;
baseAnimation.toValue =(id)initPath.CGPath;
baseAnimation.duration = [self transitionDuration:transitionContext];
baseAnimation.delegate =self;
[shaperLayer addAnimation:baseAnimation forKey:nil];
}
//CABasicAnimation动画结束
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
[self.transitionContext completeTransition:YES];
if (_type == MaskTransitionPush) {
[self.transitionContext viewControllerForKey:UITransitionContextToViewControllerKey].view.layer.mask = nil;
}
else if (_type == MaskTransitionPop)
{
[self.transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey].view.layer.mask = nil;
}
}
参考文章:
https://onevcat.com/2013/10/vc-transition-in-ios7/
http://blog.devtang.com/2016/03/13/iOS-transition-guide/
http://www.jianshu.com/p/45434f73019e