匿名社交應用Secret的開發者開發了一款叫做Ping的應用,用戶可以他們感興趣的話題的推送。
Ping有一個很炫的東西,就是主界面和之間切換的動畫做的非常的好。每次看到一個非常炫的動畫,都不由得會想:“這個東西我要不要自己實現以下”。哈哈~~~
這個教程里,你會學到如何用Swift實現這樣的很酷的動畫。你會學到如何使用shape layer,遮罩和使用UIViewControllerAnimnatedTransitioning協議和UIPercentDrivenInteractivetransition類等實現View Controller界面切換動畫。
不過需要注意,這里假定你已經有一定的Swift開發基礎。如果只是初學的話,請自行查看我得其他Swift教程。
開篇簡介
我們主要介紹Ping里從一個View Controller跳轉到另一個的時候的動畫。
在iOS里,你可以在UINavigationController中放入兩個View Controller,並實現UIViewControllerAnimatedTransitioning協議來實現界面切換的動畫。具體的細節有:
- 動畫的時間長度
- 創建一個容器View來控制兩個View Controller的View
- 可以實現任意你能想到的動畫
這些動畫,你可以用UIView得動畫方法來作,也可以用core animation這樣的比較底層的方法來做。本教程會使用后者。
如何實現
現在你已經知道代碼大概會添加到什么地方。下面討論下如何實現那個Ping的那個圈圈動畫。這動畫嚴格的描述起來是:
- 圓圈是從右側的按鈕產生。並且從圈中可以看到下面一層試圖的內容。
- 也就是說,這個圓圈是一個遮罩。圓圈里的都可以看到,外面的全部都隱藏。
你可以用CALayer的mask可以達到這個效果。當然還需要設置alpha為0來隱藏下面一個視圖的內容。alpha值設定為1的時候顯示下面視圖的內容。

現在你就懂了遮罩了。下一步就是決定用哪一種CAShapeLayer來實現這個遮罩。只需要修改這些CAShapeLayer組成的圓圈的半徑。
現在開始
這里就不十分詳細的敘述了,都是些關於創建和配置項目的步驟。
1. 創建一個新的項目。選擇一個single view application
2. 項目名稱設置為CircleTransition。語言選擇Swift。Devices就選擇iPhone
項目到此初步創建好了。在Main.stroyboard里只有一個view controller。但是我們的動畫需要兩個至少的view controller。不過首先需要把現在的這個view controller和UINavigationController關聯起來。選中這個唯一的view controller,之后在菜單欄中選擇Editor->Embed In->Navigation Controller。之后這個navigation controller就會成為initial controller,后面連着最開始生成的那個view controller。之后,選中這個navigation controller,在右側菜單欄的第四個tab中勾去“Shows navigation bar”。因為在我們的app中不需要navigation bar。

接下來添加另外一個view controller。給這個view controller指定class為ViewController。
然后,給每一個view controller,除了navigation controller,添加一個按鈕。雙擊按鈕,刪除文字,之后把按鈕的背景色設置為黑色。另外一個按鈕也同樣處理。給這兩個按鈕設定autolayout。指定他們在右上角上。指定這兩個按鈕的寬度和高度為40。
最后讓按鈕變成圓形的。右邊菜單的第三個tab中選擇“user defined runtime attributes”。點下面的加號,添加如圖所示的內容。設置button的corner radius為15。

這樣這個按鈕在運行起來的時候就是圓形的了。設定完成之后暫時看不到這個效果。運行起來以后:

現在需要在每個view controller中添加些內容了。先把這兩個view controller的背景色修改一下。

現在這個app大致已經成型了。不同的顏色可以代表你將來要顯示出來的各種各樣的內容。所需要的就是把這個兩個view controller連起來。在橘色的controller的按鈕中放下鼠標。按下ctrl然后把光標拖動到另外一個controller上。這是會出現一個彈出的菜單。把這個菜單的action用同樣的方法和這個controller再連接一次,並選擇show。這樣,在這個按鈕選擇的時候,navigation controller就會push到下一個view controller中。這是一個segue。后面的教程會需要這個segue所以這里給這個segue一個identifer,叫做“PushSegue”。運行代碼,點擊橘色controller的按鈕就會跳轉到紫色的controller了。
因為這是一個循環的過程,所以從橘色到紫色之后還需要從紫色回到橘色。現在就完成這個功能。首先,在紫色controller綁定的ViewController類中添加一個action方法。
@IBAction func circleTapped(sender: UIButton){ self.navigationController?.popViewControllerAnimated(true) }
並添加紫色controller上的按鈕的引用,這個會在后面用到:
@IBOutlet weak var button: UIButton!
之后給紫色controller的按鈕的“touch up inside”事件添加上面的@IBAction。

綁定按鈕的屬性:

再運行起來看看。橘色到紫色,紫色到橘色循環往復!
注意:兩個view controller都需要綁定按鈕和按鈕事件!否則后面的動畫只能執行一次!
自定義動畫
這里主要處理的就是navigation controller的push和pop動畫。這需要我們實現UINavigationControllerDelegate協議的animationControllerForOperation方法。直接在ViewController中添加一個新的類:
class NavigationControllerDelegate: NSObject, UINavigationControllerDelegate{ func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { return nil } }
首先,在右側的菜單中選中Object這個item。

之后,把這個東西拖動到navigation controller secene下。

然后選中這個Object,在右側菜單的第三個tab上修改class為我們剛剛定義的NavigationControllerDelegate。
下一步,給navigation controller指定delegate。選中navigation controller,然后在右側最后的菜單中連接navigation controller的delegate選項到剛剛拖進來的Object上:

這個時候還是不會有特定的效果出現。因為方法還是空的,只能算是一個placeholder方法。
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { return nil }
這個方法接受兩個在navigation controller中得controller。從一個跳轉到另一個的兩個controller。並返回一個實現了UIViewControllerAnimatedTransitioning的對象。所以,我們需要創建一個實現了UIViewControllerAnimatedTransitioning協議的類。
class CircleTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning
首先添加一個屬性:
weak var transitionContext: UIViewControllerContextTransitioning?
這個屬性會在后面的代碼中用到。
添加一個方法:
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval { return 0.5 }
這個方法返回動畫執行的時間。
添加動畫方法:
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 = CGPointMake(button.center.x, button.center.y - CGRectGetHeight(toViewController.view.bounds)) // need more research 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(self.transitionContext!) maskLayerAnimation.delegate = self maskLayer.addAnimation(maskLayerAnimation, forKey: "CircleAnimation") }
一步步的解釋:
- transitionContext屬性保持了一個類成員的引用。這樣在后面的代碼中可以用到。
- 取出containerView以及fromViewController和toViewController和controller上面的button引用。動畫主要還是作用在container view上的。
- 把toViewController的view添加到container view上。
- 創建兩個路勁,一個就是button的大小(button在運行起來之后是圓形的),另一個要足夠大到可以cover整個screen。動畫就是在這兩個path上來來回回。
- 創建一個CAShapeLayer作為mask用。給這個layer的path賦值為circleMaskPathFinal,否則動畫執行完成以后可能又縮回來。
- 創建一個CABasicAnimation動畫,key path是“path”,這個動畫作用於layer的path屬性上。動畫從circleMaskPathInitial執行到circleMaskPathFinal。並給這個動畫添加一個delegate,在動畫執行完成以后清理現場。
實現animation代理的方法:
override func animationDidStop(anim: CAAnimation!, finished flag: Bool) { self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled()) self.transitionContext?.viewControllerForKey(UITransitionContextFromViewControllerKey)?.view.layer.mask = nil }
現在就可以用CircleTransitionAnimator來實現動畫的效果了。修改代碼NavigationControllerDelegate的代碼:
class NavigationControllerDelegate: NSObject, UINavigationControllerDelegate{ func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { return CircleTransitionAnimator() } }
運行起來吧。點擊黑色的按鈕,動畫效果就出現了。
感覺不錯吧,但是這個是不夠的!
給動畫添加手勢響應
我們還要給這個動畫添加一個可以響應手勢的transition。響應手勢需要用到一個方法:navigationController->interactionControllerForAnimationController。這是UINavigationControllerDelegate中得一個方法。這個方法返回一個實現了協議UIViewControllerInteractiveTransitioning的對象。
iOS的SDK中提供了一個UIPercentDrivenInteractiveTransition的類。這個類實現了上面的協議,並且提供了很多其他的手勢處理實現。
在NavigationControllerDelegate類中添加以下的屬性和方法:
class NavigationControllerDelegate: NSObject, UINavigationControllerDelegate{ var interactionController: UIPercentDrivenInteractiveTransition? func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { return CircleTransitionAnimator() } func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return self.interactionController } }
既然是響應手勢的,那么一個pan的手勢是必不可少的了。不過首先要添加一些輔助的東西。
1. 在NavigationControllerDelegate中添加對navigation controller的引用。
@IBOutlet weak var navigationController: UINavigationController?
給這個引用添加對navigation controller的引用,如圖:

實現awakeFromNib方法:
override func awakeFromNib() { super.awakeFromNib() var pan = UIPanGestureRecognizer(target: self, action: "panned:") self.navigationController!.view.addGestureRecognizer(pan) }
當pan這個動作在navigation controller的view上發生的時候就會觸發panned回調方法。給這個方法添加如下代碼:
func panned(gestureRecognizer: UIPanGestureRecognizer){ switch gestureRecognizer.state { case .Began: self.interactionController = UIPercentDrivenInteractiveTransition() if self.navigationController?.viewControllers.count > 1 { self.navigationController?.popViewControllerAnimated(true) } else{ self.navigationController?.topViewController.performSegueWithIdentifier("PushSegue", sender: nil) } case .Changed: var translation = gestureRecognizer.translationInView(self.navigationController!.view) var completionProgress = translation.x / CGRectGetWidth(self.navigationController!.view.bounds) self.interactionController?.updateInteractiveTransition(completionProgress) case .Ended: if gestureRecognizer.velocityInView(self.navigationController!.view).x > 0 { self.interactionController?.finishInteractiveTransition() } else{ self.interactionController?.cancelInteractiveTransition() } self.interactionController = nil default: self.interactionController?.cancelInteractiveTransition() self.interactionController = nil } }
在Begin中,pan手勢一開始執行就初始化出UIPercentDrivenInteractiveTransition對象,並作為值賦給屬性self.interactionController。
- 如果在第一個view controller就設定一個push(在早先定義的一個segue),在第二個view controller的時候就設定一個pop。
- 在navigation controller的push或者pop的時候則觸發NavigationControllerDelegate的返回self.interactionController對象的方法。
Changed,在這個方法中根據手勢移動的距離讓動畫移動不同的距離。這里apple已經替我們做了很多。
Ended,這里你會看到手勢的移動速度。如果是正則transition結束,如果是負則取消。同時,把interactionController值設置為nil。
default,如果是其他的狀態就直接取消trnasition並把interactionController值設置為nil。
運行程序,在屏幕上左右移動你的手指看看效果吧!
