在一款新的app——Ping中,用户可以订阅自己感兴趣的主题,该应用会向用户推送相关的文章或段落。该应用在视图的切换时采用了一个非常炫酷的动画效果,如下图所示:
现在我们就来实现这一效果。总的来说,所用到的知识点有:
1、使用代理UIViewControllerAnimatedTransitioning实现控制器间的自定义动画
2、使用UIShapeLayer创建一个特定形状的层
3、配合mask效果实现视图的切换
4、使用手势与UIPercentDrivenInteractiveTransition实现可控过程的交互。
首先,这里的切换效果是控制器间的过渡效果,并且是基于UINavigationContoller的,因为我们需要通过导航控制器的代理来实现。8.0以后,UINavigationController提供了一个代理UINavigationControllerDelegate可以让我们对控制器间的pop、push过程进行控制,代理中有一个方法:
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
这个方法的返回值是一个实现了UIViewControllerAnimatedTransitioning协议的对象,这是我们项目中另外一个协议。用它可以控制动画时间、编写自定义动画流程、对动画完成后的资源进行回收处理等。
总的来说,我们需要的东西有:
1、两个控制器供切换
2、一个UINavigationControllerDelegate对象控制视图控制器的切换流程
3、一个UIViewControllerAnimatedTransitioning对象提供具体的动画
当另一个控制器将要展示内容时,从上图可以看到,首先从右上角出现一个圆形,圆形不断放大,将要展示的内容就在圆形中,圆形之外依然是原来控制器的内容。动画过程中,这个圆形不断放大,随着它的增大,新内容也就慢慢出来了。换句话说,这个圆形的作用就是展示范围内的,屏蔽范围外的。
这里用遮挡层来实现是再好不过了,mask属性就决定着这一遮挡过程。当然了mask需要一个CALayer对象,这里也说过,这个过程中出现的是圆形,对于特定的形状,CAShapeLayer就能方便的解决问题了。
有了思路,实现起来就简单了。首先要准备好两个控制器,基本的要求就是提供一个按钮可以用来视图切换即可:
这里从左到右依次是UINavigationController、ViewController、ViewController,右上角的按钮(圆角效果)用来切换,图片的位置乱是由于使用了auto layout,加上只有一张图片,懒得调了:]
第一步,动画的实现
既然动画的具体过程是由UIViewControllerAnimatedTransitioning对象来实现的,那么我们先创建这个对象。UIViewControllerAnimatedTransitioning是一个协议,该协议中有两个必须方法和一个可选方法:
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval
func animateTransition(transitionContext: UIViewControllerContextTransitioning)
optional func animationEnded(transitionCompleted: Bool)
按照上面说的,我们单独建一个类,让新类实现该协议,并编写这三个方法:
class CircleTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
//context contains fromViewController、toViewController、containView etc.
//We keep a reference here to get the context during the animation
weak var transitionContext: UIViewControllerContextTransitioning?
//duration time
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 0.5
}
//animation progress
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
//get the context
self.transitionContext = transitionContext
//get other important variable
var containView = transitionContext.containerView()
var fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as ViewController
var toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as ViewController
//button here means the button that controls the changing progress in fromViewController
//we will let the circle start from button‘s center
var button = fromVC.button
containView.addSubview(toVC.view)
//calculate some values
let circlePathInitial = UIBezierPath(ovalInRect: button.frame)
//just big enough is OK, too. Because here we just need to make sure the mask can cover all view
let extremePoint = CGPoint(x: button.center.x, y: button.center.y - toVC.view.bounds.size.height)
let radius = sqrt(extremePoint.x * extremePoint.x + extremePoint.y * extremePoint.y)
let circlePathFinal = UIBezierPath(ovalInRect: CGRectInset(button.frame, -radius, -radius))
//create shape layer
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePathFinal.CGPath
toVC.view.layer.mask = shapeLayer
//create animation and add it
let maskAnimation = CABasicAnimation(keyPath: "path")
maskAnimation.fromValue = circlePathInitial.CGPath
maskAnimation.toValue = circlePathFinal.CGPath
maskAnimation.duration = self.transitionDuration(transitionContext)
maskAnimation.delegate = self
shapeLayer.addAnimation(maskAnimation, forKey: "path")
}
override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
//end the animation
self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled())
//remove the mask at fromViewController
self.transitionContext?.viewControllerForKey(UITransitionContextFromViewControllerKey)?.view.layer.mask = nil
}
}
注释中已经解释得非常详细了,这里从上下文中获取两个控制器,基于此创建CAShapeLayer,通过设置起始路径(主要是半径不同),并赋值给动画的起始属性,来完成这个流程。
第二步,动画的控制
接下来我们需要UINavigationControllerDelegate对象来控制上面完成的动画,依然新建一个类,实现该协议,并编写相关方法:
class NavigationControllerDelegate: NSObject, UINavigationControllerDelegate {
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CircleTransitionAnimator()
}
}
第三步。连接
最后,为我们跟控制器(导航控制器)的delegate赋值上面这个类的一个对象即可。这个过程可以在storyboard中完成,拖一个Object对象到UINavigationController中,将类改为NavigationControllerDelegate,然后把导航控制器的delegate拖到该对象上即可
现在已经可以看到效果了,并且跟Ping类似:
这样我们的动画就完成了。接下来,我们来把按钮给“消灭”掉。
随着手势的流行,我们可以用手势来实现很多操作,并且这些过程往往是可控的,典型的例子就是前一阵很火的抽屉菜单。
现在我们为UINavigationController添加一个pan手势,让视图切换效果受用户拖动控制。
UINavigationControllerDelegate提供了一个方法用来返回“交互过程”:
func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?
UIViewControllerInteractiveTransitioning是一个代理,UIPercentDrivenInteractiveTransition便是iOS为我们提供的一个实现了这一代理的类,该类可以按比例更新视图切换过程、直接完成切换、取消切换……因此,我们首先需要一个该类对象。另外,在UINavigationControllerDelegate中,我们也要获得UINavigationController的引用,因此声明两个变量:
var interactionController: UIPercentDrivenInteractiveTransition?
@IBOutlet weak var navigationVC: UINavigationController?
添加Pan手势:
override func awakeFromNib() {
super.awakeFromNib()
let panGR: UIPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: Selector("handlePan:"))
self.navigationVC?.view.addGestureRecognizer(panGR)
}
核心是手势处理代码,思路非常简单,根据用户滑动的偏移值决定切换的百分比:
func handlePan(recognizer: UIPanGestureRecognizer) {
var transition = recognizer.translationInView(self.navigationVC!.view)
var progress = fabs(transition.x) / self.navigationVC!.view.bounds.size.width
switch recognizer.state {
case .Began:
self.interactionController = UIPercentDrivenInteractiveTransition()
if self.navigationVC?.viewControllers.count > 1 {
self.navigationVC?.popViewControllerAnimated(true)
} else {
self.navigationVC!.topViewController.performSegueWithIdentifier("PushSegue", sender: nil)
}
case .Changed:
self.interactionController?.updateInteractiveTransition(progress)
case .Ended:
if fabs(recognizer.velocityInView(recognizer.view).x) > 0 {
self.interactionController?.finishInteractiveTransition()
} else {
self.interactionController?.cancelInteractiveTransition()
}
self.interactionController = nil
default:
self.interactionController?.cancelInteractiveTransition()
self.interactionController = nil
}
}
最后,在代理方法中返回我们创建的这个UIPercentDrivenInteractiveTransition对象:
func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return self.interactionController
}
这样手势操作就完成了,效果还是不错的
本文是对Raywenderlich的文章
的学习总结,部分图片也来自该文章。特此感谢~
原文地址:http://blog.csdn.net/u013604612/article/details/43883029