美文网首页iOS开发杂货铺iOS技术笔记收录iOS进阶
iOS 视图控制器转场详解:从入门到精通

iOS 视图控制器转场详解:从入门到精通

作者: seedante | 来源:发表于2016-02-19 00:23 被阅读5556次

「精通」这个词不敢瞎讲,不过本文大体能够帮助你达到副标题的水平的:熟读几遍,多写几遍,转场就掌握了。另外,大家所谓的转场动画=转场+动画,其实两者互不相关,不管是简单的转场动画还是炫酷的转场动画,你想要的效果那是动画的事情,完全是另外一个话题。

简书里不支持目录跳转以及排版页面过窄使得阅读这个长篇很不方便,文章已经搬运至我的 Github Wiki 里。

文章目录

看这目录挺吓人的,其实转场是非常简单的事情,当你照着随便哪里的入门教程写下第一个非交互式转场动画后,你就已经掌握了转场50%的内容了,剩下的50%有点坑容易掉进去,不过多写几次,基本上就全掌握了。但是,这不能保证你能写出你想要的动画,因为这得看你会不会那个动画,日常够用的动画很简单,大部分动画库里的例子绝对足够你使用了,比如 Spring,很多基础动画,我早期学习动画案例都是从这里抄。这些动画已经能够组合出绝大部分你看到的转场动画了,实际上很多动画在于你是否能够将其分解成基础的动画,这是经验的问题,多写写多看看,而一些剩下的奇技淫巧的动画则需要一些特别的知识,比如用贝塞尔曲线实现一些非规则形状的动画,这得去了解 UIBezierPath + CAShapeLayer + maskLayer,教程也有不少。另外推荐这个关于 CAlayer 的教程,很多特殊的动画得依靠它们来实现。

版权申明:我已将本文在微信公众平台的发表权「独家代理」给 iOS 开发(iOSDevTips)微信公共帐号。扫码关注「iOS 开发」:

iOSDevTips

Demo 更新了 Swift 3.0,而文章中的代码片段由于工作量挺大的,看时间更新。

相关文章

网友评论

  • 6b69124ff056:这应该是有史以来最详细的关于转场的分享了,感谢~
  • 伊织随意写:如果用OC写的话就更好了。
  • Chendy_Linda:我已讲问题发给大神邮箱,求大神帮忙...........
    seedante:@Chendy_Linda 初步验证得出,你演示的那个中途取消转场的问题在 iOS 9 上不存在,效果完美,而在 iOS 8 上则有另一种形式的瑕疵。试了用 center 和 bounds 来实现 frame 变化的动画,问题依旧。
    Chendy_Linda:@seedante 没事,哈哈,能回我我就很高兴了,你慢慢看,不急,身体要紧~
    seedante:不好意思,今天拔了牙,下午一直沉迷在游戏里缓解疼痛,刚看到你的邮件和留言以及支付宝打赏,多谢了,我现在看。
  • iYeso:xcode8.2.1 打开报了很多很多错误 :sweat: 讲的不错
  • 川少叶:CustomModalTransitionDemo,SlideAnimationController.swift文件,在navigationTransition情况下,pop出现问题,toView被移除了,界面变成黑色了。应该push可以containerView.addSubview(toView!),但是pop不应该containerView.addSubview(toView!)
    seedante:@川少叶 Modal Transition 你就老老实实用 SDETransitionType.ModalTransition,强制换成 SDETransitionType.navigationTransition,你为什么要这样用呢?
    川少叶:@seedante 修改了SDEModalTransitionDelegate.swift文件中,present方法中返回的是
    let transitionType = SDETransitionType.navigationTransition(.push)
    return SlideAnimationController(type: transitionType)
    dismiss 方法中返回的是.pop类型controller
    seedante:@川少叶 你做了什么修改?我放在 GIthub 上的 CustomModalTransitionDemo 里只做了ModalTransition ,没有添加关于 push 和 pop 的操作。
  • f52191993ee3:博主,我又遇到问题了,
    我用了PresentationController把dimmingView加到containerView上,dimmingView是一个黑色alpha为0的UIVisualEffectView,然后presentation时用动画控制它的alpha到0.5,dismissal时用动画控制它的alpha回到0。
    我在presentedViewController上加了一个拖动手势,我用这个手势更新交互控制器,但我在调试的时候发现PresentationController的动画和AnimatedTranstitioning不同步,我用NSLog打印手势更新的percent,发现percent在0.5时,presentedView就已经全部到右边消失了,但是dimming有alpha还没有到0.0,所以好像AnimatedTranstitioning只占了动画周期的一半
    下面是我的代码:
    https://github.com/xypng/TransitionAnimationDemo
    seedante:@xypng 在 PresentingViewController 里,我知道你不想以后重复添加带代码,但是现在先让代码正常跑起来再来尝试优化代码。
    f52191993ee3:@seedante 你是说直接在PresentedViewController里加吧?我这个手势是在PresentedViewController上的。我之所以在CustomPresentationController里加是想以后写别的PresentedViewController时就不用再写一遍加手势的代码了。
    seedante:@xypng 我建议你把手势移到 PresentingViewController 上试试,不要在CustomPresentationController 里去添加这个手势,直接在 PresentingViewController 里添加。
  • f52191993ee3:不是说好了present出来的ViewController的modalTransitionStyle属性是UIModalPresentationCustom,在动画完成后fromView不会被移出视图结构吗?但是我的为什么还是移除了?
    谁能帮我看看,这是我的代码:
    https://github.com/xypng/TransitionAnimationDemo
    下面我稍微解释一下代码:
    我是从PresentingViewController点击present按钮,用presentViewController:animated:completion:方法,present到PresentedViewController,
    PresentedViewController的modalTransitionStyle(设成了UIModalPresentationCustom)和transitioningDelegate(实例化了一个我自己写的代理,这个代理是全局的,解决了代理是弱引用的问题)属性是在init时设置的。
    但是我运行结束后后面就黑了,我用debug view hierarchy查看,发现viewFrom被移除了。可是文章第一部分特殊的 Modal 转场不是说 Custom 模式:presentation 结束后,presentingView(fromView) 未被主动移出视图结构
    seedante:@xypng 你的 Demo 中 PresentedViewController 类的初始化方法里使用 self.modalTransitionStyle = UIModalPresentationCustom; 来指定转场方式,但这里有个小错误导致你的转场方式还是 FullScreen 而不是 Custom 模式,正确的代码是:self.modalPresentationStyle = UIModalPresentationCustom; 由于 OC 中的 enum 实际上就是个 Int,Xcode 并没有发现这个小错误。
    f52191993ee3:@xypng 而且我也改了一下不加那个dimmingView,发现问题还在,说明不是这里的问题
    f52191993ee3:@xypng 刚才忘了说了,虽然有一个dimmingView, 但我没用UIPresentationController,我改了一下你的CustomModalTransition,发现不是非得用UIPresentationController
  • b4deb7b8ea6c:在ScrollTabBarController中progress没达到条件时,取消动画有问题,即使设置了completeSpeed也没有用。这个是怎么一回事了?
    seedante:此评论中涉及的问题已经完结: https://github.com/seedante/iOS-ViewController-Transition-Demo/issues/5#issuecomment-253497420
    b4deb7b8ea6c:@ZeroOnet 也就是没有过设定限值,但还是会切换到下一个控制器,并且选中的标签没有改变
  • 2cf952c12ec2:你好,想请教你一个问题。场景是这样的:在实现类似图片浏览器的转场动画的时候,也就是从小图到大图的过渡,实现dismiss交互式转场动画的时候,如果我想用手势使得大图随着我的手指的移动而移动,这部分动画在哪进行合适呢?我在UIPercentDrivenInteractiveTransition的subclass里实现的时候,效果总是不尽人意,动画会有卡顿,动画的时长好像也不对,我自己也看了一些文档,但还是不知道哪出的问题,希望可以指教一下,谢谢!
    seedante:@橙汽水 你说移动幅度过大的时候比较容易出错,是否需要考虑在处理手势的被取消或是失败的状态,也就是这时候 case UIGestureRecognizerStateEnded: 是否被执行了呢?考虑下.Cancelled 和 .Ended 的情况一起处理。
    2cf952c12ec2:@seedante 对,我想要实现的效果就是苹果自家的相册的转场效果,浏览大图的时候你可以单指移动(不是pinch)图片(移动的时候有动画),移动一定距离的时候松开,图片就会做一个frame变化的动画(从当前大图的frame 变换到 小图的frame)。
    我现在是在panGesture里进行的图片跟随手指移动的动画,这点应该是没错的,然后手势结束之后,需要做frame动画,代码是这样的:
    case UIGestureRecognizerStateEnded:{
    //NSLog(@"----UIGestureRecognizerStateEnded");
    [sender setTranslation:CGPointZero inView:sender.view.superview];

    self.interacting = NO;
    [self finishInteractiveTransition];

    [UIView animateWithDuration:self.duration delay:0 usingSpringWithDamping:0.7 initialSpringVelocity:0.0 options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionAllowUserInteraction animations:^{
    self.animationController.initialView.alpha = 0.0;
    self.animationController.destinationView.alpha = 1.0;
    NSLog(@"---------self.animationController.initialView.frame = %@",NSStringFromCGRect(self.animationController.initialView.frame));
    NSLog(@"---------self.animationController.destinationView.frame = %@",NSStringFromCGRect(self.animationController.destinationView.frame));
    self.animationController.initialView.frame = self.animationController.destinationFrame;
    self.animationController.destinationView.frame = self.animationController.destinationFrame;
    } completion:^(BOOL finished) {

    [self.animationController.initialView removeFromSuperview];
    [self.animationController.destinationView removeFromSuperview];
    [self.transitionContext completeTransition:YES];
    }];

    }

    现在有一个很奇怪的现象是我如果移动的距离不大的话,动画是没有问题的,但是如果移动幅度过大(图片只在屏幕上留下一个小角落)的时候,动画就会卡顿或者直接就不执行了...不知道为什么?请问楼主有没有什么建议,谢谢!
    seedante:@橙汽水 你说的这个场景和苹果自家的 Photos 里的交互转场动画很像,我姑且认为是像 Photos 里那样需要两根手指 pinch,那我现在就按照这交互来解释。这种交互动画我没有实现过,我只能说说我的思路,可能有的地方细节不对:首先,实现这个交互动画应该不需要使用UIPercentDrivenInteractiveTransition的子类;其次,在转场动画中图片的移动不属于转场动画本身,实际上在这个过程中这个移动算不上动画,它只是随着你的手指移动而实时地改变它的位置,我觉得实现的关键在于分离手指的 pinch 和 移动两个操作,在pinch时控制转场动画的进度,移动就实时修改图片的位置。
  • 栈溢出:你好,看了文章。UINavigationControllerDelegate的方法是写在哪?写在基类里不行额
    seedante:@栈溢出 写在哪里无所谓,只要确保在 push 前将 delegate 设置了就好。你这个情况可能是没有设置成功。delegate 是 weak 的,这点要注意。
    栈溢出:@seedante 话说本来是写在哪的:cry::cry:A push B 我子类了一个导航控制器然后写在里面 不行额
    seedante:@栈溢出 基类?哪个类的基类?
  • 22d4ed3d1a0a:你好,非常感谢你的分享。想请教一个问题,现在我按你的教程使用`UIPresentationController` modal 出一个`UIViewController`,presentedVC 的 view 类似一个卡片,并不占全屏。在`UIPresentationController`里加了个dimView.现在已经能实现动画完成后,点击dimView调用` presentingViewController.dismissViewControllerAnimated(true, completion: nil)
    `。然而现在的问题是,我该如何做出 present 卡片vc 的过程中,点击 dimView 取消 present 过程呢?请问是否能给我一些思路呢?谢谢。
    22d4ed3d1a0a:@seedante 已经在拜读~非常感谢分享!
    seedante:@Bigbig_Chai 在现有的转场体系下,没有现成的 API 能够实现这点,在 iOS 10 里可以做到这点了,你可以看我前几天更新关于 iOS 10 带来的变化的章节。如果你要做到这点,可以参考《自定义容器控制器转场》里如何取消转场。
    22d4ed3d1a0a:@seedante 对的。
  • ssccsci:学到了。感谢撰文。
  • 黄二瓜:博主,help;请你看一下这个怎么处理?搜了半天没找到解决方法https://github.com/seedante/iOS-ViewController-Transition-Demo/issues/2
    seedante:@黄二峰 抱歉 没注意到 我看看
  • Raykle:学习到很多!支持、 :smile:
    seedante:@Raykle 能帮助你就好 :smiley: 。唉,要多些你这样的人就好了。
    Raykle:@seedante 额。。哪里的大款哦... 学习到的远不止值这些钱了 :grin:
    seedante:@Raykle 多谢大款的打赏 :heart_eyes:
  • Neo_joke:有一个可以讨论的点,就是在构建的转场上下文中对于fromViewController 调用willMoveToParentViewController(nil)方法的时机,官方文档中是这样描述willMoveToParentViewController方法的:Called just before the view controller is added or removed from a container view controller.
    所以我觉得在转场上下文调用动画控制器animateTransition()方法之前调用fromViewCotroller的willMoveToParentViewController(nil)方法,这样能保证fromViewCotroller的正常声明周期调用,比方说你在动画控制器里调用fromViewCotroller.view移除的动画之前,可以保证在此之前fromViewCotroller 的viewWillAppear调用,而不是动画控制器完成移除之后才调用这些声明周期函数
    seedante:@Neo_joke 还记得交互章节提到的 WWDC 工程师说的转场过程中不能保证 willXX 和 didXX 方法的调用顺序么,症结就在这里。
    Neo_joke:@seedante 对于这点其实我有时候也很迷惑,所以拿出来大家讨论一下,中途取消的情况下,fromViewController的生命周期究竟是什么样的流程,其实标准的无交互的动画转场的情况下,取消转场的可能性就在于用户发起了一个转场,而在这个转场没有完成的时候,用户又进行了转场,根据你动画控制器的动画策略,是否要取消前面还没进行完的动画,来决定这些父子控制器关系的构建方法的调用顺序,官方文档写的其实应该是一个参考,实际上要看动画策略,为了能平衡转场过程中的各个问题,其实我个人觉得,构建父子视图控制器的方法调用的逻辑顺序是关键中的关键,因为这决定了转场中视图控制器的生命周期函数是否正确调用,这对于一个视图控制器是否能正常工作至关重要。
    seedante:@Neo_joke 你说的有道理,我当时好像没考虑到viewWillAppear这些方法,当时的考虑是交互转场中有取消转场的可能,那么在转场结束后又要重新添加 fromVC,索性就放到了animateTransition()结束之后。
  • 465605bdfead:有个问题想问下博主:我在modal转场下添加了交互控制,交互开始后,toView能通过交互手势控制转场进度,但是fromView直接按照非交互的方式完成转场动画了,不能控制,请问博主遇到过这种情况吗?我是刚接触转场,一边看着博主的文章一边研究,非常感谢博主的分享,写的非常详细,帮助很大!!!
    465605bdfead:我发现了一个解决方法是:把fromView add到containerView里,fromView就可以控制进度了,但是要按你说的重写presentationController的 shouldRemovePresentersView方法
    465605bdfead:@seedante animateTransition:方法里面,先通过fromVC, toVC获取fromView, toView,然后代码是:
    if (toVC.isBeingPresented) {
    toView.bounds = CGRectMake(0, 0, containerView.frame.size.width * 2 / 3, containerView.frame.size.height * 2 / 3);
    toView.center = containerView.center;
    toView.alpha = 0;

    [containerView addSubview:toView];

    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0 options:(UIViewAnimationOptionCurveEaseOut) animations:^{
    toView.bounds = CGRectMake(0, 0, containerView.frame.size.width, containerView.frame.size.height);
    toView.alpha = 1;

    fromView.transform = CGAffineTransformMakeTranslation(containerView.frame.size.width, 0);
    } completion:^(BOOL finished) {
    [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
    }


    seedante:@1269 可以贴一下你的动画控制器的代码么?
  • 黄二瓜:博主 我好像找到解决办法了,在手势end 或者 cancelled的请情况下,需要设置 completionSpedd
    if (progress > 0.5) {
    self.tabBarVCDelegate.interactionController.completionSpeed = 1 - progress;
    [self.tabBarVCDelegate.interactionController finishInteractiveTransition];
    }else {
    //取消转场后,UITabBarController会自动恢复selectedIndex的值,不需要手动恢复
    self.tabBarVCDelegate.interactionController.completionSpeed = 1 - progress;
    [self.tabBarVCDelegate.interactionController cancelInteractiveTransition];
    }
    黄二瓜:@seedante 只要保证completionSpeed < 1就没问题,原理是什么就不清楚了
    黄二瓜:@seedante OC的我确认了,completionSpeed API 中提到通过在取消或者完成转场前设置以实现非交互状态的加速或者减速,我测了下,这样不会出现重复动画的bug
    seedante:@黄二峰 你确认了吗?我在我的 Swift 版本的 Demo 里这么做没有用。
  • 黄二瓜:函数帧中找到你说的那个私有方法了,有没有办法可以解决这个?
  • 黄二瓜:ScrollTabBarController 这个Demo 为什么在滑动结束时偶尔会出现动画回弹,并且会显示部分的containerView的背景颜色(黑色)?
    seedante:@黄二峰 你的运行环境是怎样的呢?
    黄二瓜:@seedante 在取消转场和转场成功的情况下,fromView和toView的交接处都有一定几率出现黑色背景,个人猜测是由于动画:
    UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
    fromView.transform = fromViewTransform
    toView.transform = CGAffineTransformIdentity
    }, completion: { finished in
    fromView.transform = CGAffineTransformIdentity
    toView.transform = CGAffineTransformIdentity

    let isCancelled = transitionContext.transitionWasCancelled()
    transitionContext.completeTransition(!isCancelled)
    })
    引起的
    seedante:@黄二峰 我设定的是滑动结束时距离不超过屏幕宽度的30%时会返回取消转场,你是这个现象吗?第2个问题不清楚具体是个什么情况。
  • 07d93406ec39:感謝你那麼細緻的分享~
    從頭到尾都看了一遍 :)
    Dashing_Pro:@漩渦貓_Lanaya 我也在看转场动画,希望交流
    07d93406ec39:@seedante 😄😄最近一直在研究動畫 感謝你的文章
    seedante:@漩渦貓_Lanaya 希望能帮助你实现想要的转场动画。

本文标题:iOS 视图控制器转场详解:从入门到精通

本文链接:https://www.haomeiwen.com/subject/xxzmkttx.html