匿名社交应用 Secret 最近发布了一个新的应用 Ping ,它允许用户接收他们感兴趣的话题的通知。 Ping 的主界面到菜单间的圆形切换动画很出色,如下图所示。

pingimg

Every time I see a really neat animation, I do what everybody does: I think “Now how would I implement that on iOS…” — wait, normal people don’t think that?! :]

在这个教程中,我们将使用 Swift 来实现这个 Cool Animation 。 在这个过程中,将涉及到 shape layers , masking , UIViewControllerAnimatedTransitioning 协议,类 UIPercentDrivenInteractiveTransition 以及更多。

###Overall Strategy

在 Ping 中,动画发生在视图控制器的切换过程中。

在iOS中,你可以给两个由 UINaviegationController 的视图控制器间切换加上自定义的动画。实现则是通过 iOS7 以后出现的 UIViewControllerAnimatedTransitioning 协议来定义动画。

细节下面会涉及到,但是需要知道这个协议能做什么:

  • 指定动画的持续时间
  • 创建一个容器视图,包含对两个视图控制器的引用
  • 自由的设计动画

###Implementation Strategy

如果我用言语来描述这个动画时,可能会这么形容:

  • 有一个圆圈源自右上角的按钮,它作为一个视图展示的视窗。
  • 换句话说, mask 作为遮盖,显示所有内部的东西,并隐藏所有范围外的事物。

你可以实现这个效果通过在 CALayer 上使用遮盖。它的 alpha 通道来决定哪部分显示哪部分不显示。

pingimg

既然我们知道通过 mask 来实现,下一步就要决定选择什么样的 mask 。 由于它是圆形的,所以我们要选择 CAShapeLayer 。 这个动画就是给它增加半径并显示的过程。

###Getting Started

现在开始动手去做,选择 Single View Application 模板。

xcodeimg

选择 Swift 作为开发语言。

xcodeimg

打开 Main.storyboard 发现有一个单一的视图控制器,但是我们需要至少两个。我们选中视图控制器,然后通过 Editor\Embed In\Navigation Controller 来将其由导航视图控制器管理。 然后选中导航视图控制器,将其设为入口,并隐藏导航栏。

xcodeimg

现在我们再添加一个视图控制器到画布中。

xcodeimg

然后我们将新的视图控制器的 Custom Class 设为 ViewController 。

xcodeimg

接下来为两个视图控制器右上角添加按钮。

xcodeimg

背景颜色设为黑色后,来为他们添加约束。 边距为10,宽高44,然后通过 Update Frames 来更新位置。

xcodeimg

最后是来配置按钮的形状,我们不需要通过编码来设置,只需要在 IB 中设置一下 attributes 就可以。 IB 中并不会把按钮显示为圆形,但是你可以运行程序来观看效果。

现在我们需要为两个视图控制器添加一些内容,为何不先设置背景颜色呢?

xcodeimg

然后添加两个图像视图,添加约束(视图居中显示,宽高300点)。完成后如下图。

xcodeimg

添加两个图片。

xcodeimg

###Wire It Up

开始连线添加行为。将首个视图控制器的按钮 action 指定为 show 第二个视图控制器。

xcodeimg

为了将来在代码中访问到这个 Segue (场景) ,我们需要指定一个 Segue ID 为 PushSegue 。

xcodeimg

接下来我们要添加 pop 的代码以及 button 的引用在 ViewController.swift 中。

@IBOutlet weak var button: UIButton!
@IBAction func circleTapped(sender: UIButton) {
    self.navigationController?.popViewControllerAnimated(true)
}

然后链接到 IB 中。

xcodeimg xcodeimg

然后运行程序,则可以 Push 和 Pop 。

xcodeimg

###Custom Animation

编写一个自定义的 Push 或者 Pop 动画,你需要实现 UINavigationControllerDelegate 协议的 animationControllerForOperation 方法。 创建一个新的 iOS\Source\Cocoa Touch Class 模板文件。

class NavigationControllerDelegate: NSObject, UINavigationControllerDelegate {
 
}

然后打开 Main.storyboard 来设置一个 UINavigationController 的代理的实例。首先需要在 Navigation Controller Scene 中添加一个 Object 。

xcodeimg

将其 Custom Class 设置为刚才添加的 NavigationControllerDelegate

xcodeimg

然后将 NavigationController 的 Delegate 指向这个 Object

xcodeimg

回到 NavigationControllerDelegate 添加占位方法

func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return nil;
}

这个方法会得到导航视图控制器切换的两个视图控制器,然后它的工作就是负责实现一个 UIViewControllerAnimatedTransitioning

之后我们创建一个新的类来实现动画。 CircleTransitionAnimator :

xcodeimg

然后我们需要让这个类遵守协议,并添加 required 方法。

weak var transitionContext: UIViewControllerContextTransitioning?

func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
    return 0.5
}

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    
}

接下来开始编写其中的动画代码

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    //1
    self.transitionContext = transitionContext
    
    //2
    var containerView = transitionContext.containerView()
    var fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as ViewController
    var toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as ViewController
    var button = fromViewController.button
    
    //3
    containerView.addSubview(toViewController.view)
    
    //4
    var circleMaskPathInitial = UIBezierPath(ovalInRect: button.frame)
    var extremePoint = CGPoint(x: button.center.x - 0, y: button.center.y - CGRectGetMaxY(toViewController.view.bounds))
    var radius = sqrt((extremePoint.x * extremePoint.x) + (extremePoint.y * extremePoint.y))
    var circleMaskPathFinal = UIBezierPath(ovalInRect: CGRectInset(button.frame, -radius, -radius))
    
    //5
    var maskLayer = CAShapeLayer()
    maskLayer.path = circleMaskPathFinal.CGPath
    toViewController.view.layer.mask = maskLayer
    
    //6
    var maskLayerAnimation = CABasicAnimation(keyPath: "path")
    maskLayerAnimation.fromValue = circleMaskPathInitial.CGPath
    maskLayerAnimation.toValue = circleMaskPathFinal.CGPath
    maskLayerAnimation.duration = self.transitionDuration(transitionContext)
    maskLayerAnimation.delegate = self;
    maskLayer.addAnimation(maskLayerAnimation, forKey: "path")
}

分步介绍:

  1. 创建一个指向 transitionContext 的引用
  2. 获得一些引用,包括 containnerView , fromViewController , toViewController , button 。
    containerView 是动画发生的视图。在动画时, fromViewController 和 toViewController 是相同的部分。
  3. 将 toViewController 作为子视图添加到 containerView 上。
  4. 创建两个圆形的 UIBezierPath 实例;一个是 button 的 size ,另外一个则拥有足够覆盖屏幕的半径。最终的动画则是在这两个贝塞尔路径之间进行的。
  5. 创建一个 CAShapeLayer 来负责展示圆形遮盖。将它的 path 指定为最终的 path 来避免在动画完成后会回弹。
  6. 创建一个关于 path 的 CABasicAnimation 动画来从 circleMaskPathInitial.CGPath 到 circleMaskPathFinal.CGPath 。我们也指定了它的 delegate 来在完成动画时做一些清除工作。

接下来,我们实现 animationDidStop() 方法来做一点的清除。

override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
	self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled())
    self.transitionContext?.viewControllerForKey(UITransitionContextFromViewControllerKey)?.view.layer.mask = nil
}

第一行告诉 iOS 这个 transition 完成。因为完成后我们才可以清除 mask 。

最后一步我们要使用这个 CircleTransitionAnimator 。 回到 NavigationControllerDelegate.swift 然后我们来修改下面方法。

func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return CircleTransitionAnimator()
}

运行程序,你将会看到下面的酷炫效果!

xcodeimg

###An Interactive Gesture Animation

一旦你的动画运行完成,你可能会把你的注意力放到另外一个 ViewController 切换的特性上: 一个允许交互的”返回”手势。由于只是点击已经过时,请确保你实现这个深度定制的 UI .

交互手势开始会调用 navigationController:interactionControllerForAnimationController: 方法,这是导航控制器代理的一个方法,会返回一个 UIViewControllerInteractiveTransitioning 的对象。

iOSSDK 提供给你一个类名字为 UIPercentDrivenInteractiveTransition ,它已经实现了上面的协议,而且可以让我们对交互手势做很多处理。

打开 NavigationControllerDelegate.swift ,然后添加一个新属性和一个新方法。

var interactionController: UIPercentDrivenInteractiveTransition?

func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
    return self.interactionController
}

现在回来继续思考这个交互手势。显而易见的是,你需要一个 gesture recognizer 。

funimg

你将会在导航视图控制器的代理中对导航控制器添加手势,那么首先我们需要一个导航控制器的引用。 首先在 NavigationControllerDelegate.swift 添加一个属性。

@IBOutlet weak var navigationController: UINavigationController?

然后在 storyboard 中进行连线。

xcodeimg

回到 NavigationControllerDelegate.swift 实现 awakeFromNib() 方法。

override func awakeFromNib() {
    super.awakeFromNib()
    
    var panGesture = UIPanGestureRecognizer(target: self, action: Selector("panned:"))
    self.navigationController!.view.addGestureRecognizer(panGesture)
}

创建了一个 pan gesture 。 然后实现它的 action 。

@IBAction func panned(gestureRecognizer: UIPanGestureRecognizer) {
    switch gestureRecognizer.state {
    //1
    case .Began:
        self.interactionController = UIPercentDrivenInteractiveTransition()
        if self.navigationController?.viewControllers.count > 1 {
            self.navigationController?.popViewControllerAnimated(true)
        } else {
            self.navigationController?.topViewController.performSegueWithIdentifier("PushSegue", sender: nil)
        }
    
    //2
    case .Changed:
        var transition = gestureRecognizer.translationInView(self.navigationController!.view)
        var completionProgress = transition.x / CGRectGetWidth(self.navigationController!.view.bounds)
        self.interactionController?.updateInteractiveTransition(completionProgress)
        
    //3
    case .Ended:
        if (gestureRecognizer.velocityInView(self.navigationController!.view).x > 0) {
            self.interactionController?.finishInteractiveTransition()
        } else {
            self.interactionController?.cancelInteractiveTransition()
        }
        self.interactionController = nil
    
    //4
    default:
        self.interactionController?.cancelInteractiveTransition()
        self.interactionController = nil
    }
}

分步介绍:

  1. 开始的时候实例化了一个 UIPercentDrivenInteractionTransition 对象赋值给 interactionController 属性。 如果是 pop 比较容易, push 则需要执行 Segue 场景来实现。 另外角度说, push 或者 pop 会触发导航控制器的代理方法,返回 self.interactionController ,所以这个属性这时不会为空。
  2. 在变化的时候,你只需要根据进度去 update interactionController 即可,不需要做任何更多的复杂工作。
  3. 在结束时,根据当时的速率方向来确定完成这次交互或者取消。然后做清除工作。
  4. default只是做了一些清除工作。

然后运行 app . 就可以用手指来控制这个动画了哦!

xcodeimg

注:这篇博客翻译自 raywenderlich.com 中的 How To Make A View Controller Transition Animation Like in the Ping App

以上为本篇博客全部内容,欢迎提出建议,个人联系方式详见关于