WWDC 2013 session 218: Custom Transitions Using View Controllers
最近在朋友圈看到别人转发了一系列很帅的 iOS 交互动画。就想着自己也来玩一下,顺便把之前没写成的 Custom ViewController Transition 自定义视图控制器过渡的文章也一起搞定了。
这里只以这个动画的实现为主线,更系统的介绍请移步上面的相关链接。
视图控制器过渡,就是指图片里那种 ViewController 的过渡效果。(好废话。。。)在上面链接的视频里说到,一共有下面这四个地方可以用自定义过渡:
这个例子里,我们只涉及第二种 UITabBarController
另外还有 Interactive view controller transitions 可交互过渡,例子就是在 NavigationController 的详细页面中从屏幕左侧滑入以返回时的那个动画。可以用手势控制过渡动画的进度,还可以中途取消手势。这个也不会提到。。。
扔了这么多东西不管的好处就是,这篇文章里我们需要处理的新东西就只有两个:
// UITabBarControllerDelegate 的这个方法,用于返回一个负责管理过渡动画的 UIViewControllerAnimatedTransitioning
optional func tabBarController(_ tabBarController: UITabBarController,
animationControllerForTransitionFromViewController fromVC: UIViewController,
toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
// 另一个新东西就是 UIViewControllerAnimatedTransitioning 自己。。。有两个方法需要实现
// 这个方法负责做真正的动画,输入参数是过渡的上下文,从哪个VC过渡到哪个VC这些东西都可以从它得到。
func animateTransition(_ transitionContext: UIViewControllerContextTransitioning)
// 这个返回本过渡的持续时间
func transitionDuration(_ transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval
新建一个 TabBarController 项目,StoryBoard 里简单修饰一下,让两个页面看起来有所不同。
然后在 viewDidLoad 中设置 TabBarController 的 delegate ,这里我们设置成为 self
TabBarController 代码如下:
class MainVC: UITabBarController, UITabBarControllerDelegate { // 实现UITabBarControllerDelegate接口
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self // delegate设置为self
}
/* 如GIF中那样在切换时改变状态栏颜色,这里可选,与过渡无关
func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {
let index = find(tabBarController.viewControllers! as [UIViewController], viewController)!
switch index {
case 1:
UIApplication.sharedApplication().setStatusBarStyle(UIStatusBarStyle.LightContent, animated: true)
default:
UIApplication.sharedApplication().setStatusBarStyle(UIStatusBarStyle.Default, animated: true)
}
}
*/
// 这里我们只要返回一个UIViewControllerAnimatedTransitioning,tabbarcontroller就会根据它去执行过渡动画
func tabBarController(tabBarController: UITabBarController, animationControllerForTransitionFromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
// 下面会给出TransitioningObject的实现,这里我们可以也可以选择用本方法的fromVC、toVC、tabBarController这三个参数构造出一个TransitioningObject
let transitionObject: TransitioningObject = TransitioningObject();
transitionObject.tabBarController = self
return transitionObject
}
}
接下来是实际的动画实现,主要的想法是设定一个 CAShapeLayer
作为目标 VC 的遮罩。然后给这个 ShapeLayer 的 path 属性加动画,从半径为0变化到覆盖整个目标 VC 。
class TransitioningObject:NSObject, UIViewControllerAnimatedTransitioning {
private weak var tabBarController: MainVC!
// 动画在这里!
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
// 由context获得参与过渡的各个角色
let fromView: UIView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
let fromViewController: UIViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let toView: UIView = transitionContext.viewForKey(UITransitionContextToViewKey)!
let toViewController: UIViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
// 可以吧containerView理解成一个舞台,参与过渡动画的角色在这个舞台上表演。。。所以要让他们上台先。
transitionContext.containerView().addSubview(fromView)
transitionContext.containerView().addSubview(toView)
// 找出各个VC在tabBar上的位置。
let fromViewControllerIndex = find(self.tabBarController.viewControllers! as [UIViewController], fromViewController)
let toViewControllerIndex = find(self.tabBarController.viewControllers! as [UIViewController], toViewController)
// 计算出点击的tab的位置,作为动画的圆心
let tabBarFrame = tabBarController.tabBar.frame
let tabBarItemCount = tabBarController.tabBar.items!.count
let tabBarItemWidth = tabBarFrame.size.width / CGFloat(tabBarItemCount)
let tappedItemY = tabBarFrame.origin.y
let tappedItemX = tabBarItemWidth * CGFloat(toViewControllerIndex!) + tabBarItemWidth / 2
// 圆要放大到的半径,勾股定理算出toView的对角线长度
var finalRadius = sqrt(pow(toView.frame.height, 2) + pow(toView.frame.width, 2))
// 构造开始时和结束时的圆的贝赛尔曲线。
let start = UIBezierPath(ovalInRect: CGRect(x: tappedItemX, y: tappedItemY, width: 0, height: 0)).CGPath
let final = UIBezierPath(ovalInRect: CGRect(x: tappedItemX - finalRadius, y: tappedItemY - finalRadius, width: finalRadius * 2, height: finalRadius * 2)).CGPath
// 新建一个CAShapeLayer,用作toView的遮罩。并且开始时的path设置为上面的start——位置在点击的tab上的一个半径为0的圆。
// 下文中就要给这个path加特技,让他变化到包含整个界面那么大。
var circleMask = CAShapeLayer()
circleMask.path = start
toView.layer.mask = circleMask
// 给circleMask的path属性加动画
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = start
animation.toValue = final
animation.duration = self.transitionDuration(transitionContext)
animation.delegate = self // 设置CABasicAnimation的delegate为self,好在动画结束后通知系统过渡完成了。
animation.setValue(transitionContext, forKey: "transitionContext") // 待会需要用到transitionContext的completeTransition方法
circleMask.addAnimation(animation, forKey: "circleAnimation")
circleMask.path = final
}
// 过渡动画完成后,调用completeTransition说明过渡完成。
override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
if let context = anim.valueForKey("transitionContext") as? UIViewControllerContextTransitioning {
context.completeTransition(true)
}
}
// 持续1秒钟
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 1;
}
}