如何實現Ping App的轉場動畫

原文:How To Make A View Controller Transition Animation Like in the Ping App

作者:Rounak Jain

譯者:遠的風景,あ夂寒ツ

匿名社交網絡App Secret製造商最近發佈了一個新App叫做Ping,用戶可以收到他們感興趣內容的消息。

Ping突出的是主導主屏幕和菜單之間轉場的動畫,如圖所示。

1.gif

每次見到一個完美的動畫,我都會思考要是我講怎麼樣去在iOS上實現這個動畫。

在這個教程中,你將會學習用Swift實現這個很酷的動畫。在這個過程中,你將會用到圖形圖層、遮罩、theUIViewControllerAnimatedTransitioning協議,以及UIPercentDrivenInteractiveTransition類等等。

注意:本教程假定你瞭解基本的iOS開發和Swift語言。如果你是新手,可以參看我們網站上的教程

總體策略

在Ping中,動畫發生在從一個控制器過渡到另一個控制器。

在iOS中,你可以通過將兩個視圖控制器放在一個UINavigationController中來自定義視圖控制器之間的動畫,並使用iOS7中的UIViewControllerAnimatedTransitioning協議來動畫轉場。

在以下內容中,你可以瞭解更多詳細細節,不過本質上該協議允許你:

  • 指定動畫的時間

  • 創建一個可以引用兩個控制器的容器視圖

  • 實現任何你想到的動畫 

你將會用UIView動畫或Core Animatio動畫實現這些。

實現策略

下面具體討論怎麼實現圓形的過度效果。

描述這個動畫效果如下:

  • 屏幕右上角有個圓形按鈕可控制視圖的出現。

  • 換句話說,圓作爲一個Mask顯示邊框內的東西,隱藏邊框外的東西。

你可以在CAlayer上使用遮罩,並用其alpha channel決定要展示圖層的哪個屬性。

1的alpha值展示下面的圖層內容,0的alpha值則隱藏下面的內容,中間部分局部地展示圖層的內容,以下用圖示解釋這個意思:

blob.png

現在你已經瞭解了遮罩,下一步要決定使用哪種遮罩。由於動畫帶有圓形遮罩,所以最自然的選擇是CAShapeLayer。想要動畫這個圓形,你需要簡單增加圓形遮罩的半徑。

開始

注意:這個章節是爲想從頭構建項目準備的,如果你是一個資深的iOS開發者,你可以越過此章節直接從Custom Animation section開始。

在Xcode中選擇File\New\Project新建工程,接選擇 iOS\Application\Single View Application.

blob.png

給工程命名爲CircleTransition,選擇開發語言爲Swift,設備爲iPhone。

blob.png

打開Main.storyboard,你會發現有一個single view controller,但是過度效果需要幾個控制器之間切換。

首先你要把控制器嵌入到導航控制器中,選定好視圖控制器,選擇 Editor\Embed In\Navigation Controller。

接下來需要隱藏導航欄,打開Xcode右邊的工具欄選擇第四個tab(Attributes Inspector )取消Shows Navigation Bar的選定框。

blob.png

現在添加視圖控制器到Storyboard(故事板)中,使用導航控制器水平地鏈接各個視圖控制器。

blob.png

選擇一個新的視圖控制器,在右邊的工具面板中選擇第三個tab(Identity Inspector),更改類類型爲ViewController,這樣就與Xcode創建的ViewController類文件一致了。

blob.png

接下來,在每個視圖控制器右上角添加一個按鈕。雙擊每個按鈕,按下退格鍵,將按鈕的標題設置爲空字符串。

blob.png

設置每個按鈕的背景爲黑色。

現在用AutoLayout設置按鈕的位置設置。選擇第一個視圖控制器的按鈕,設置如下:

  • 點擊右邊和頂部紅色括號,設置爲10

  • 設置寬度和高度爲44

  • 設置Update Frames爲Items of New Constraints

blob.png

點擊Add 4 Constraints,按鈕的大小和位置就設置好了,重複爲每個視圖控制器設置按鈕。

最後設置按鈕的形狀,通過設置corner radius把按鈕形狀設置爲圓形。在右邊的工具面板中選擇第三個tab(Identity Inspector)中用 User Defined Runtime Attributes設置按鈕的layer的cornerRadius參數。

blob.png

運行的時候將會見到按鈕形狀爲圓形,而在IB中按鈕形狀不是圓形。

blob.png

現在給每個視圖控制器設置不同的背景色。給第一個視圖控制器設置綠色背景色,第二個設置黃色背景色。

blob.png

給兩個視圖控制器都添加上image views,設置高寬都爲300 points, 選擇右下角的Align選項,讓它們居中展示在父視圖中,並選擇Horizontal Center in Container和Vertical Center in Container.

blob.png

通過Resolve Auto Layout Issues將它們的Frame調整到正確值,然後Update Frames,這是canvas看起來是這樣的:

blob.png

下載圖片iPhonesiPads版,接着把它們賦給image view,並設置image view的content model爲Aspect Fit。

blob.png

設置完後canvas如下所示:

blob.png

鏈接起來

恭喜!你已經完成了App的框架。現在可以開始鏈接按鈕的響應事件了。

右擊第一個視圖控制器的按鈕,然後拖動action outlet到第二個視圖控制器。

blob.png

接着顯示一個彈出的菜單:選擇show。

blob.png

當按鈕被按下時,第二個視圖控制器被push進來。

打開Xocde的右邊the Attributes Inspector命名segue的identifier爲PushSegue.

blob.png

編譯並運行該應用程序以確保推出第二個視圖控制器。

現在你已經將按鈕連接在第二個視圖控制器上,試試彈出視圖控制器,所以你需要些一個在ViewController類中寫一個方法:

1
2
3
@IBAction func circleTapped(sender:UIButton) {
  self.navigationController?.popViewControllerAnimated(true)
}

同時添加一個弱引用的屬性:

1
@IBOutlet weak var button: UIButton!

回到故事版中,對兩個視圖控制器如下操作:

  • 右擊按鈕

  • 拖曳Touch Up Inside圓形到視圖控制器的頂部

blob.png

右擊按鈕,同時拖住引用的outlet到每個view controller中,並鏈接到視圖控制器的button屬性。

blob.png

blob.png

編譯並再次運行,現在你已經有了一個功能完善的push和pop動畫。

2.gif

自定義動畫

如果你跳過了前面的章節,可以直接下載starter project,這樣你可以直接使用完成配置的視圖控制器和按鈕。

自定義push或pop動畫需要實現UINavigationControllerDelegate協議的animationControllerForOperation方法。

新建一個文件,命名NavigationControllerDelegate,實現UINavigationControllerDelegate協議。

1
2
class NavigationControllerDelegate: NSObject,UINavigationControllerDelegate {
    }

接着打開Main.storyboard , 把UINavigationControllerDelegate賦給storyboard的UINavigationController的委託。

要實現這一步,可在右邊庫中搜索object,並拖拽到左側Navigation Controller Source的下面。

blob.png

現在點擊object,在右邊Identity Inspector中,將其類更改爲NavigationControllerDelegate.

blob.png

接下來,右擊左面板中的UINavigationController,將object賦給UINavigationController的委託,並將其委託屬性拖拽到NavigationControllerDelegate 對象上:

blob.png

返回NavigationControllerDelegate,並添加如下佔位符方法: 

1
2
3
4
5
func navigationController(navigationController: UINavigationController, animationControllerForOperation 
operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController 
toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
  return nil
}

注意方法的主體是空的,某個時候你將會用到它。

該方法接受兩個需要轉場的視圖控制器,這將返回一個實現UIViewControllerAnimatedTransitioning的對象。

所以你需要創建其中一個。想要完成這一步,你需要通過File\New\File創建一個新的Cocoa Touch類,並將其命名爲CircleTransitionAnimator.

blob.png

聲明實現the UIViewControllerAnimatedTransitioning協議:

1
class CircleTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {

接下來你要爲協議添加所需方法:

添加第一個方法:

1
2
3
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
    return 0.5
}

在該方法中,你需要返回動畫的持續時間。如果你希望動畫持續0.5s,那可以返回0.5:

下一步,將該屬性添加到類:

1
weak var transitionContext: UIViewControllerContextTransitioning?

你會需要它來儲存轉場上下文環境。

下一步添加第二個所需方法:

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
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 = CGPoint(x: button.center.x - 0, y: button.center.y - CGRectGetHeight(toViewController.view.bounds))
  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(transitionContext)
  maskLayerAnimation.delegate = self
  maskLayer.addAnimation(maskLayerAnimation, forKey: "path")
}

逐步解讀代碼:

1.在超出該方法範圍外保持對transitionContext的引用,以便將來訪問。

2.創建從容器視圖到視圖控制器的引用。容器視圖是動畫發生的地方,切換的視圖控制器是動畫的一部分。

3.添加toViewController作爲containerView的子視圖。

4.創建兩個圓形UIBezierPath實例:一個是按鈕的尺寸,一個實例的半徑範圍可覆蓋整個屏幕。最終的動畫將位於這兩個Bezier路徑間。

5.創建一個新的CAShapeLayer來展示圓形遮罩。你可以在動畫之後使用最終的循環路徑指定其路徑值,以避免圖層在動畫完成後回彈。

6.在關鍵路徑上創建一個CABasicAnimation,從circleMaskPathInitial到circleMaskPathFinal.你也要註冊一個委託,因爲你要在動畫完成後做一些清理工作。

接着在同一個類中執行animationDidStop()進行清理:

1
2
3
4
override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
  self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled())
  self.transitionContext?.viewControllerForKey(UITransitionContextFromViewControllerKey)?.view.layer.mask = nil
}

第一行是告知iOS動畫的完成。由於動畫已經完成了,所以你可以移除遮罩。最後一步是實際使用CircleTransitionAnimator.

回到NavigationControllerDelegate.swift,並調整此前你添加的stub方法:

1
2
3
4
5
6
func navigationController(navigationController: UINavigationController,
 animationControllerForOperation operation: UINavigationControllerOperation,
 fromViewController fromVC: UIViewController,
 toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return CircleTransitionAnimator()
}

簡單調整後會返回一個新的CircleTransitionAnimator實例。編譯並運行app,最終動畫效果如下:

3.gif

恭喜,現在你已經重製了Ping app中的動畫。如果這是你想要的效果,那可以在此打住了,但是如果你想了解如何實現動畫的交互,請繼續閱讀!

交互式手勢動畫

動畫運行正常後,你可以將關注自定義視圖控制器轉場的另一個特性:交互手勢。由於點擊操作已經是很老套的了,所以你可以通過實現這個特性來增加UI的深度。

交互式手勢從調用navigationController:interactionControllerForAnimationController:開始。這是一個UINavigationControllerDelegate方法,有望返回一個符合UIViewControllerInteractiveTransitioning的對象。

iOS SDK提供了UIPercentDrivenInteractiveTransition類,該類已經在這個協議中實現,並且爲你做了不少交互式手勢處理。

打開NavigationControllerDelegate.swift,並添加該屬性和新方法:

1
2
3
4
5
6
var interactionController: UIPercentDrivenInteractiveTransition?
  
func navigationController(navigationController: UINavigationController, 
interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
  return self.interactionController
}

回頭思考下這個輕掃返回手勢,很明顯,你需要一個手勢識別器。

blob.png

你將要爲導航控制器添加手勢識別器。你需要一個導航控制器的引用,打開NavigationControllerDelegate.swift並添加以下屬性:

1
@IBOutlet weak var navigationController: UINavigationController?

打開Main.storyboard,右擊左側Navigation Controller Delegate object,將屬性和導航控制器連接起來,然後從navigationController屬性中 拖到storyboard中的導航控制器上。

blob.png

返回NavigationControllerDelegate.swift並實現awakeFromNib():

1
2
3
4
5
override func awakeFromNib() {
  super.awakeFromNib()
  var panGesture = UIPanGestureRecognizer(target: self, action: Selector("panned:"))
  self.navigationController!.view.addGestureRecognizer(panGesture)
}

這一步會創建UIPanGestureRecognizer,並將該對象添加到導航控制器的視圖上,並得到panned:方法中的手勢回調函數。

下一步,實現該方法:

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
//1
@IBAction 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)
    }
  
    //2
  case .Changed:
    var translation = gestureRecognizer.translationInView(self.navigationController!.view)
    var completionProgress = translation.x/CGRectGetWidth(self.navigationController!.view.bounds)
    self.interactionController?.updateInteractiveTransition(completionProgress)
  
    //3
  case .Ended:
    if (gestureRecognizer.velocityInView(self.navigationController!.view).x > 0) {
      self.interactionController?.finishInteractiveTransition()
    else {
      self.interactionController?.cancelInteractiveTransition()
    }
    self.interactionController = nil
  
    //4
  default:
    self.interactionController?.cancelInteractiveTransition()
    self.interactionController = nil
  }
}

代碼分解如下:

  1. .Began: 只要識別了手勢,那麼它會初始化一個UIPercentDrivenInteractiveTransition對象並將其賦給interactionController屬性。

    1. 如果你切換到第一個視圖控制器,它初始化了一個push,如果是在第二個視圖控制器,那麼初始化的是pop。Pop非常簡單,但是對於push,你需要從此前創建的按鈕底部手動完成segue.

    2. 反過來,push/pop調用觸發了NavigationControllerDelegate方法調用返回self.interactionController.這樣屬性就有了non-nil值。

  2. .Changed: 這種狀態下,你完成了手勢的進程並更新了interactionController.插入動畫是項艱苦的工作,不過蘋果已經做了這部分的工作,你無需做什麼事情。

  3. .Ended: 你已經看到了pan手勢的速度。如果是正數,轉場就完成了;如果不是,就是被取消了。你也可以將interactionController設置爲nil,這樣她就承擔了清理的任務。

  4. default: 如果是其他任何狀態,你可以簡單取消轉場並將interactionController設置爲nil.

構建並運行app,從左向右輕掃,你會看到相同的動畫,但是是在你的手指控制之下。

4.gif

下一步

這是該教程的完整項目

希望你能喜歡這篇主要爲了實現一個簡單但非常酷的轉場動畫的文章。你可以在自己的app中實現類似Ping中的效果,也可以通過改變背景色和動畫速度來更改其外觀和整體感覺。

這篇文章中有不少東西,包括使用圖形圖層、遮罩、UIViewControllerAnimatedTransitioning協議、UIPercentDrivenInteractiveTransition類以及其他等。

如果有任何問題,歡迎交流。

若需轉載,請寫明來源和譯者!

發佈了53 篇原創文章 · 獲贊 2 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章