iOS 自定义 ViewController 过渡动画

动画预览

相关链接

WWDC 2013 session 218: Custom Transitions Using View Controllers

开扯

最近在朋友圈看到别人转发了一系列很帅的 iOS 交互动画。就想着自己也来玩一下,顺便把之前没写成的 Custom ViewController Transition 自定义视图控制器过渡的文章也一起搞定了。

这里只以这个动画的实现为主线,更系统的介绍请移步上面的相关链接。

视图控制器过渡,就是指图片里那种 ViewController 的过渡效果。(好废话。。。)在上面链接的视频里说到,一共有下面这四个地方可以用自定义过渡:

  • Presentations and dismissals
  • UITabBarController
  • UINavigationController
  • UICollectionViewController layout-to-layout transitions

这个例子里,我们只涉及第二种 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 中设置 TabBarControllerdelegate ,这里我们设置成为 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;
    }
}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏非著名程序员

Android自定义 View 实战之 StickerView

本篇文章为利用Matrix自定义View三部曲的第一部曲。 虽然Android内置了许多View供开发者组合和使用,但其多样性还是不足,在很多场景或功能需求下...

2349
来自专栏向治洪

ios动画

在iOS开发中,动画是提高用户体验重要的环节之一。一个设计严谨、精细的动画效果能给用户耳目一新的效果,这对于app而言是非常重要的。 简介 iOS动画主要是指C...

2335
来自专栏Youngxj

Canvas画板

3764
来自专栏用户2442861的专栏

pyqt4制作透明无边框窗体

http://blog.chinaunix.net/uid-25979788-id-3081886.html

1892
来自专栏一“技”之长

iOS开发CoreAnimation解读之二——对CALayer的分析

        每一个UIView的对象中都有一个layer这样的属性,并且layer会负责view中有关图形绘制的相关操作,例如我们设置view的背景颜色和设...

802
来自专栏james大数据架构

AndroidManifest.xml配置文件 android.theme大全权限设置Android Permission中英对照

AndroidManifest.xml启动文件 主activity: <activity android:name="com.examp...

2215
来自专栏iOS开发攻城狮的集散地

iOS 瀑布流封装

初始化仅三行代码,只需设置代理和样式,item的大小、头脚视图的大小、行列数以及间距都可以在对应样式的代理方法中自定义,然后设置为UICollectionVi...

2998
来自专栏非典型技术宅

iOS动画系列之七:实现类似Twitter的启动动画1. CAKeyframeAnimation2. CAAnimationGroup3. 实现类似Twitter的启动动画

2253
来自专栏Java呓语

View·InputEvent事件投递源码分析(一)

这里的事件是指来源于硬件的事件,诸如:屏幕的按压、触摸(屏幕解锁),实体按键的按压(调整音量),甚至于实体按键的组合使用(截屏)。

3054
来自专栏BY的专栏

iOS将单个控制器设为横屏、页面横屏

3445

扫码关注云+社区

领取腾讯云代金券