Zach的博客

自定Modal Transition

自定义Modal Transition

iOS 7提供了一套新的自定义转场动画的API,这套API可以大大降低了我们自定过渡动画的难度。下面用Ray Wenderlich的一张流程图说明API各个部分的作用。

process.jpg

可以看到一个ViewController被呈现或者消除的时候,UIKit会想Transition Delegate请求一个Animator,Animator负责转场动画的实现。可见我们需要自定义个Animator。

一个Animator应该遵循UIViewControllerAnimatedTransitioning协议,这个包含三个方法:

1
2
3
4
5
func transitionDuration(_ transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval

func animateTransition(_ transitionContext: UIViewControllerContextTransitioning)

optional func animationEnded(_ transitionCompleted: Bool)

这里出现了一个UIViewControllerContextTransitioning,它是转场动画的上下文对象,通过它可以获得将要被显示的view、将要被消除的view、将要显示的viewController等等相关对象。需要注意一定是,可能你自己持有了destinationController,但是,但是,但是,一定要通过transitionContext对象来获得desctinationController,以避免出现一些棘手的错误。

开始

下面我们就来着手写一个Modal Transition。这个转场动画参考了Bubble Transition

先看一下效果:

preview.gif

首先创建一个BubbleTransition类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class BubbleTransition: NSObject {
var bubbleColor: UIColor = UIColor.whiteColor()
var startPoint: CGPoint = CGPointZero {
didSet {
bubble.center = startPoint
}
}

private(set) var bubble: UIView = UIView()
var duration: Double = 0.5

enum BubbleTransitionType {
case dismiss, present
}

var mode: BubbleTransitionType = .present
}

BubbleTransition现在只是定义了一个变量,其中最主要的是BubbleTransitionType,present表示显示viewController,dismiss表示消除viewController。startPoint是将要呈现的viewController.view的起始位置,后面我们将看到它的作用。

现在我们利用扩展来BubbleTransition实现UIViewControllerAnimatedTransitioning协议。

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
extension BubbleTransition: UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
// 1
return duration
}

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
guard let containerView = transitionContext.containerView() else {
return;
}
if mode == .present {
guard let presentedView = transitionContext.viewForKey(UITransitionContextToViewKey) else {
return;
}
let originalSize = presentedView.frame.size
let originalCenter = presentedView.center

// 2
let bubbleFrame = bubbleFrameFrom(originalSize: originalSize, startPoint: startPoint)
bubble.backgroundColor = bubbleColor
bubble.frame = bubbleFrame
bubble.center = startPoint
bubble.layer.cornerRadius = bubble.frame.size.height / 2.0
bubble.transform = CGAffineTransformMakeScale(0.001, 0.001)
containerView.addSubview(bubble)

// 3
presentedView.center = bubble.center
presentedView.transform = CGAffineTransformMakeScale(0.001, 0.001);
presentedView.alpha = 0
containerView.addSubview(presentedView)

UIView.animateWithDuration(duration, animations: {() in
presentedView.center = originalCenter
presentedView.alpha = 1
presentedView.transform = CGAffineTransformIdentity

self.bubble.transform = CGAffineTransformIdentity
}) {(_) in
// 4
transitionContext.completeTransition(true)
}
} else {
guard let returningView = transitionContext.viewForKey(UITransitionContextFromViewKey) else {
return;
}
let originalSize = returningView.frame.size
let originalCenter = returningView.center
let bubbleFrame = bubbleFrameFrom(originalSize: originalSize, startPoint: startPoint)
bubble.frame = bubbleFrame
bubble.layer.cornerRadius = bubble.frame.size.height / 2.0
bubble.backgroundColor = bubbleColor
bubble.center = startPoint

UIView.animateWithDuration(duration, animations: {
self.bubble.transform = CGAffineTransformMakeScale(0.001, 0.001)
returningView.center = self.startPoint
returningView.alpha = 0.0
returningView.transform = CGAffineTransformMakeScale(0.001, 0.001)
}) {(_) in
// 5
returningView.center = originalCenter
self.bubble.transform = CGAffineTransformIdentity
self.bubble.removeFromSuperview()
returningView.removeFromSuperview()
transitionContext.completeTransition(true)
}
}
}
}

// 6
extension BubbleTransition {
func bubbleFrameFrom(originalSize size: CGSize, startPoint start: CGPoint) -> CGRect {
let lengthX = fmax(start.x, size.width-start.x)
let lengthY = fmax(start.y, size.height-start.y)
let radius = sqrt(lengthX*lengthX + lengthY*lengthY)

return CGRectMake(0, 0, radius*2, radius*2)
}
}

先解释一下整体的思路:

cordinator2.png

小圆圈表示button的位置,点击button会呈现我们要显示的view,就称它是modalView吧。在动画发生之前,我们在containerView中加入一个bubbleView,然后加入modalView(顺序很重要),bubbleView就作为modalView的背景,它们一起从0.001的比例缩放到1比例,这样看起来就像是一个气泡的效果。因为气泡要充满整个屏幕,所以我们就要像上述图示所示的那样得到最大的半径,然后将bubbleView的长宽设置为半径的两倍。

对应标签位置的解释:

  1. 转场动画的时间长度
  2. 得到bubbl视图的frame,将它的缩放比列设为0.001,然后添加到containerView中
  3. 添加将要呈现的视图,将它的比例也设置为0.001,以便和bubble视图一起放到
  4. 在动画结束之后一定要调用transitionContext.completeTransition(true)以表示这次转场动画结束。
  5. dismiss基本上是present的逆过程,需要注意的是在动画完成后要把bubble视图和modalView从containerView中移除。
  6. 计算最大半径,并返回bubbleView的Frame。

最后,为了让我们的BubbleTransition成为转场动画的Animator,我们要在prepareForSegue中设置destinationController的transitioningDelegate,并实现协议对应的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let destinationController = segue.destinationViewController

destinationController.modalPresentationStyle = .Custom // 自定义modalPresentationStyle
destinationController.transitioningDelegate = self; // 设置delegate
}

// UIViewControllerTransitioningDelegate
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.bubbleColor = bubbleBtn.backgroundColor!
transition.startPoint = bubbleBtn.center
transition.mode = .present

return transition
}

func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.bubbleColor = bubbleBtn.backgroundColor!
transition.startPoint = bubbleBtn.center
transition.mode = .dismiss

return transition
}