我们在使用淘宝App购物的时候,经常用到的一个操作就是加入购物车,好奇的朋友一定会发现,当我们在点击加入购物车按钮的时候出现了一个酷炫的动画。原本的视图出现了一定的折叠之后,仿佛嵌入了屏幕内部,随后弹出商品详细参数的页面。实际上,这是一种自定义的模态视图转场动画。下面我们一步一步分析实现类似的模态视图转场动画。

准备工作

  • 首先我们创建GoodsViewControllerGoodsDetailViewController分别作为商品页面和商品详细页面。创建Shop.storyboard,以及两个ViewController对应的视图如下。
    Shop.storyboard

  • 创建CustomTransitioningDelegate代理,用于dismiss方法回调。

    1
    2
    3
    4
    protocol CustomTransitioningDelegate {
    // dismiss
    func dismissPresentViewController()
    }
  • 创建CustomTransitioningAnimator,作为我们所有转场动画的总代理。没错!转场导读需要花多少时间、转场该以怎样的动画进行都有它来控制。

present前的关键步奏

“点击购物车”按钮的事件处理如下,在这里比较重要的是我们需要设置modalPresentationStyleCustom类型,并把transitioningDelegate设置为CustomTransitioningAnimator类的实例animator.

1
2
3
4
5
6
7
8
9
@IBAction func addGoodsToShopCart(sender: UIBarButtonItem) {
let shopCartVC = UIStoryboard(name: "Shop", bundle: nil).instantiateViewControllerWithIdentifier("GoodsDetailViewController") as! GoodsDetailViewController
animator = CustomTransitioningAnimator(presentViewController: shopCartVC)
shopCartVC.delegate = self
shopCartVC.modalPresentationStyle = UIModalPresentationStyle.Custom
shopCartVC.transitioningDelegate = animator
shopCartVC.panGesture = UIPanGestureRecognizer(target: animator, action: #selector(animator.handleGesture(_:)))
self.presentViewController(shopCartVC, animated: true, completion: nil)
}

CustomTransitioningAnimator解析

UIViewControllerTransitioningDelegate协议实现

由于我们上一步中提到的transitioningDelegateUIViewControllerTransitioningDelegate类型的,所以我们的Animator(CustomTransitioningAnimator以下都简称Animator)首先需要实现UIViewControllerTransitioningDelegate协议。

  • 这四个方法用于设定present或dismiss的动画的代理对象,设置成self则由本类的方法实现,设置成nil则不处理。
  • 如果是手势驱动的情况下,需要代理对象实现UIViewControllerInteractiveTransitioning协议,幸运的是系统为我们提供了UIPercentDrivenInteractiveTransition类,此类本生就实现了UIViewControllerInteractiveTransitioning协议,支持百分比变换,并在此基础上扩展updateInteractiveTransitionfinishInteractiveTransitioncancelInteractiveTransition等方法,大大简化了手势驱动。
  • 如果是非手势驱动我们的类需要实现UIViewControllerAnimatedTransitioning协议
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
extension CustomTransitioningAnimator: UIViewControllerTransitioningDelegate {
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
isDismiss = false
return self
}

func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
isDismiss = true
return self
}

func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
isDismiss = true
return self.interacting ? self : nil
}

func interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
isDismiss = false
return nil
}
}

非手势情况处理

在非手势的情况下,上面提到了我们需要实现UIViewControllerAnimatedTransitioning协议。UIViewControllerAnimatedTransitioning的方法介绍:

1
2
3
public func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval // 用于设置转场动画的时间
public func animateTransition(transitionContext: UIViewControllerContextTransitioning) // 动画的具体实现细节
optional public func animationEnded(transitionCompleted: Bool) // 动画完成时调用

非手势情况实现具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// 非手势情况
extension CustomTransitioningAnimator: UIViewControllerAnimatedTransitioning {

/**
动画持续时间

- parameter transitionContext:

- returns:
*/
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.25
}

/**
动画执行效果

- parameter transitionContext:
*/
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
/**
* 如果是手势驱动的直接返回
*/
guard !interacting else {
return
}

let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!

/**
* 判断是否是dismiss的动画还是present的动画
*/
if isDismiss! {
let finalFrame = CGRect(x: 0, y: 0, width: UIScreen.mainScreen().bounds.width, height: UIScreen.mainScreen().bounds.height)
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0, options: UIViewAnimationOptions.CurveEaseIn, animations: {
fromViewController.view.frame = CGRect(x: 0, y: UIScreen.mainScreen().bounds.height, width: UIScreen.mainScreen().bounds.width, height: UIScreen.mainScreen().bounds.height)
var transform = CATransform3DIdentity
transform.m24 = -1/5000
transform = CATransform3DScale(transform, 0.95, 0.95, 1)
toViewController.view.layer.transform = transform
toViewController.view.alpha = 1
}) { (finished) in
if finished {
UIView.animateWithDuration(self.transitionDuration(transitionContext), delay: 0, options: UIViewAnimationOptions.CurveEaseIn, animations: {
toViewController.view.layer.transform = CATransform3DIdentity
toViewController.view.frame = finalFrame
}) { (finished) in
transitionContext.completeTransition(true)
}
}
}
} else {
transitionContext.containerView()?.addSubview(toViewController.view)
let finalFrame = CGRect(x: 0, y: 0, width: UIScreen.mainScreen().bounds.width, height: UIScreen.mainScreen().bounds.height)
toViewController.view.frame = CGRectOffset(finalFrame, 0, UIScreen.mainScreen().bounds.height)
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: {
var transform = CATransform3DIdentity
transform.m24 = -1/2000
fromViewController.view.alpha = 0.5
fromViewController.view.layer.transform = transform
toViewController.view.frame = finalFrame
}) { (finished) in
if finished {
UIView.animateWithDuration(self.transitionDuration(transitionContext), delay: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: {
var transform = CATransform3DIdentity
transform = CATransform3DTranslate(transform, 0, -15, 0)
transform = CATransform3DScale(transform, 0.8,0.9, 1)
fromViewController.view.layer.transform = transform
}) { (finished) in
if finished {
transitionContext.completeTransition(true)
}
}
}
}
}
}
}

手势情况处理

手势操作我们上面提到了,只要继承UIPercentDrivenInteractiveTransition这个类,就可以很方面的实现手势操作以及百分比动画。UIPercentDrivenInteractiveTransition有几个主要的方法介绍如下:

1
2
3
4
public func startInteractiveTransition(transitionContext: UIViewControllerContextTransitioning) // 实现自`UIViewControllerInteractiveTransitioning`协议,手势刚触发时调用
public func updateInteractiveTransition(percentComplete: CGFloat) // 手势触发过程中不断调用,参数为百分比0~1。
public func cancelInteractiveTransition() // 手势取消时调用
public func finishInteractiveTransition() // 手势达到一定程度成功时调用

手势操作百分比动画的具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
class CustomTransitioningAnimator: UIPercentDrivenInteractiveTransition {

var isDismiss: Bool!
weak var presentViewController: UIViewController!
var transitionContext: UIViewControllerContextTransitioning!
// 是否处于交互视图切换过程
var interacting = false
// 是否手势完成
var shouleComplete = true

init(presentViewController: UIViewController) {
super.init()
self.presentViewController = presentViewController
}
}

// MARK: - 手势操作
extension CustomTransitioningAnimator {
/**
处理手势操作

- parameter gestureRecognizer:
*/
func handleGesture(gestureRecognizer: UIPanGestureRecognizer) {
let translation = gestureRecognizer.translationInView(gestureRecognizer.view)
switch gestureRecognizer.state {
case UIGestureRecognizerState.Began:
interacting = true
if presentViewController is GoodsDetailViewController {
(presentViewController as! GoodsDetailViewController).delegate?.dismissPresentViewController()
}
case .Changed:
var fraction = translation.y / 200.0
fraction = fmin(fmax(fraction, 0.0), 1.0)
shouleComplete = fraction > 0.5
self.updateInteractiveTransition(fraction)
case .Ended, .Cancelled:
interacting = false
if !shouleComplete || gestureRecognizer.state == .Cancelled || gestureRecognizer.velocityInView(gestureRecognizer.view).y < 0 {
self.cancelInteractiveTransition()
} else {
self.finishInteractiveTransition()
}
default:
break
}
}

/**
开始手势拖拽

- parameter transitionContext:
*/
override func startInteractiveTransition(transitionContext: UIViewControllerContextTransitioning) {
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
self.transitionContext = transitionContext
UIView.animateWithDuration(transitionDuration(transitionContext)) {
var transform = CATransform3DIdentity
transform = CATransform3DTranslate(transform, 0, 15, 0)
transform.m24 = -1/5000
transform = CATransform3DScale(transform, 0.85, 1 , 1)
toViewController.view.layer.transform = transform
}
}

/**
手势拖动过程中不断更新

- parameter percentComplete: 更新百分比0~1
*/
override func updateInteractiveTransition(percentComplete: CGFloat) {
guard let _ = transitionContext else {
return
}
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
var transform = CATransform3DIdentity
transform.m24 = -1/3500 + 1/3500 * percentComplete
transform = CATransform3DScale(transform, 0.85 + 0.15 * percentComplete, 0.9 + 0.1 * percentComplete , 1)
toViewController.view.layer.transform = transform
toViewController.view.alpha = 0.5 + 0.5 * percentComplete
}

/**
完成手势dismiss
*/
override func finishInteractiveTransition() {
let finalFrame = CGRect(x: 0, y: 0, width: UIScreen.mainScreen().bounds.width, height: UIScreen.mainScreen().bounds.height)
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
UIView.animateWithDuration(2 * transitionDuration(transitionContext), animations: {
fromViewController.view.frame = CGRect(x: 0, y: UIScreen.mainScreen().bounds.height, width: UIScreen.mainScreen().bounds.width, height: UIScreen.mainScreen().bounds.height)
toViewController.view.layer.transform = CATransform3DIdentity
toViewController.view.alpha = 1
toViewController.view.frame = finalFrame
}) { (finished) in
if finished {
self.transitionContext.completeTransition(true)
self.transitionContext = nil
}
}
}

/**
手势取消操作
*/
override func cancelInteractiveTransition() {
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: { () -> Void in
fromViewController.view.userInteractionEnabled = false
fromViewController.view.frame = CGRect(x: 0, y: 0, width: fromViewController.view.frame.width, height: fromViewController.view.frame.height)
}) { (finished) -> Void in
if finished {
UIView.animateWithDuration(self.transitionDuration(self.transitionContext), delay: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: {
var transform = CATransform3DIdentity
transform = CATransform3DTranslate(transform, 0, -15, 0)
transform = CATransform3DScale(transform, 0.8,0.9, 1)
toViewController.view.layer.transform = transform
toViewController.view.alpha = 0.5
}) { (finished) in
if finished {
fromViewController.view.userInteractionEnabled = true
self.transitionContext.completeTransition(false)
}
}
}

}
}
}

源码分享

Github地址:HCustomTransition