iOS开发——圆形过渡动画

来源:互联网 发布:droid4x for mac 编辑:程序博客网 时间:2024/05/16 10:56

前言

在一款新的app——Ping中,用户可以订阅自己感兴趣的主题,该应用会向用户推送相关的文章或段落。该应用在视图的切换时采用了一个非常炫酷的动画效果,如下图所示:
Ping app使用的视图过渡特效
现在我们就来实现这一效果。总的来说,所用到的知识点有:
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对象提供具体的动画

功能分析

当另一个控制器将要展示内容时,从上图可以看到,首先从右上角出现一个圆形,圆形不断放大,将要展示的内容就在圆形中,圆形之外依然是原来控制器的内容。动画过程中,这个圆形不断放大,随着它的增大,新内容也就慢慢出来了。换句话说,这个圆形的作用就是展示范围内的,屏蔽范围外的。
图片来自Raywenderlish
这里用遮挡层来实现是再好不过了,mask属性就决定着这一遮挡过程。当然了mask需要一个CALayer对象,这里也说过,这个过程中出现的是圆形,对于特定的形状,CAShapeLayer就能方便的解决问题了。

具体实现

有了思路,实现起来就简单了。首先要准备好两个控制器,基本的要求就是提供一个按钮可以用来视图切换即可:
准备工作
这里从左到右依次是UINavigationController、ViewController、ViewController,右上角的按钮(圆角效果)用来切换,图片的位置乱是由于使用了auto layout,加上只有一张图片,懒得调了:]

第一步,动画的实现
既然动画的具体过程是由UIViewControllerAnimatedTransitioning对象来实现的,那么我们先创建这个对象。UIViewControllerAnimatedTransitioning是一个协议,该协议中有两个必须方法和一个可选方法:

func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeIntervalfunc 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://www.raywenderlich.com/86521/how-to-make-a-view-controller-transition-animation-like-in-the-ping-app

的学习总结,部分图片也来自该文章。特此感谢~

2 0
原创粉丝点击