通過自動布局來實現 iOS 動畫

jopen 8年前發布 | 14K 次閱讀 IOS iOS開發 移動開發

About the Speaker: Marin Todorov

Marin Todorov 是一位獨立 iOS 開發者,也是一位作家。他是《iOS Animation by Tutorials》一書的作者,并且維護著“iOS 動畫郵件訂閱”的服務。他在 Apple 上開發已經有20多年的經驗了,并且直到今天仍然未下火線。與此同時,他還曾經在諸如 Monster Technologies、Native Instruments 之類的大公司就職過,待過 4 個不同的國家,是 raywenderlich.com 教程團隊的組織者之一。在編寫代碼之余,Marin 同樣還喜歡撰寫博客、著書、網絡教學以及分享。他有時還會開源他的代碼,目前住在圣地亞哥。

@icanzilb Website

概述(0:00)

我是 Marin Todorov,是一名獨立開發者和作家。我的工作時間基本都花費在我的 iPhone 應用外包工作室以及名為 raywenderlich.com 的網站上面。2015 年初,我出版了一本名為《iOS Animations by Tutorials》的書,講述 iOS 上的精妙動畫效果。我同樣還維護著每月更新的動畫訂閱號。今天在這里我想和大家談論一下自動布局(Auto Layout)和堆棧視圖(Stack View)。

在 iOS 7、8、9中動畫效果變得更簡單了,因為我們不再使用諸如樹林之類的背景圖或者線框架構來豐富我們的用戶界面。現在,iOS 界面大多由文本和色塊構成。我們必須找到一種新的方式,能夠讓我們的創意、想法、思路能夠很好地展現給用戶,并且還能夠從其他應用中脫穎而出。比如說,這個截圖可以是 Apple 內置應用中的某個界面,也可能是某人花費了數百小時所設計研發出來的效果。

動畫是一種讓應用脫穎而出的絕佳方式。上面的例子中,在 iOS 的聯系人內置應用中就已存在同樣的界面,如果不使用一些獨特元素的話,是很難將其氣氛出來的。如果你寫過 iOS 應用的話,那么其實你至少已經使用過了一種動畫。

絕大多數動畫教程都以一個紅色方塊開始。這個方塊通過 animateWithDuration 中的一行代碼就可以實現移動。你只需指明動畫持續的時間,以及目標動畫的終值即可。比如說,UIKit API 會取出當前紅色方塊的位置和相關屬性,然后創建一個持續 1 秒的動畫,將紅色方塊在 X 軸上移動 200 px 的單位。

 UIView.animateWithDuration(1.0, animations: {
  redSquare.center.x = 200.0
})

這個做法在 iOS 3、4、5 中也是有效的,但是當我們引入新的屏幕尺寸的時候,就大有不同了。在 X 軸 上移動紅色方塊 200 個單位并不意味著能夠達到既有目的。我們都知道屏幕尺寸都是從 0 起始的,接著我們向右移動 200 個單位之后。比如說,在 iPhone 6、6+ 或者 5S 上面,我們就不能夠知道移動 200 個單位的實際視覺效果。它很有可能靠近屏幕邊緣,也可能位于屏幕中間,或者其他地方。

在我們繼續之前,我想要強調的一點是,在出現相同問題的時候,請一定要搞明白出現問題的原因。千萬不要前去 Stack Overflow 然后點開第一條回答,想也不想直接用上。

自動布局與預自動布局大有不同(6:17)

自動布局與預自動布局(pre-Auto Layout)大有不同。我會為大家快速展示一個例子。我期望您在本次演講之后就會知曉如何借助自動布局來創建動畫,至少要了解大概。

這里是一個既有應用中的一個故事板文件。我使用自動布局。這個模擬的應用有一個登錄界面,在界面頂部有用戶名和密碼輸入框。當用戶打開用戶的時候,我希望它們能夠自動移到屏幕中央。在界面構造器中我已經搭建好了所有東西,將文本框居中以及建立好了約束。

接著,讓我們想象一下我們到 Stack Overflow 中詢問如何創建一個動畫。Super Awesome Ninja Dev 回答了我的問題,“通過Duration來調用你的動畫,在其中改變center屬性,這么做應該有效。”我會到我的代碼當中,然后在UIView.animationWithDuration當中進行了操作,設置其持續時間為0.5秒。我此時有用兩個輸出口(outlet):用戶名和密碼。我試圖對它們的center屬性進行改變。由于它們的透明度為0,因此我們還需要改變透明度。

 override func viewDidAppear(animated: Bool) {
  super.viewDidAppear(animated)

  UIView.animateWithDuration(0.5, animations: {
    self.fldPassword.center.y += 200
    self.fldUsername.alpha = 1.0
    self.fldPassword.alpha = 1.0
  })

  UIView.animateWithDuration(0.5, delay: 0.1, options: [], animations: {
    self.fldUsername.center.y += 200
    }, completion: nil)
}

當我輸入用戶名的時候就出現了問題。當我點擊文本框的時候,所有的元素都會往上方跳動。這里發生了兩件事情。首先,文本框回到了它們的初始位置。然后,當它們回到原始位置之后并沒有淡出。自動布局為我們自行生成約束值,但是這里我們卻自行修改了這些視圖的位置。這不是一個好主意。本次演講的第一個要點就是不是所有的動畫都能夠通過自動布局進行實現。對于透明度、背景顏色之類的動畫來說,使用舊有方式是沒有問題的,但是對于手動改變center之類的動畫來說就不起作用了。

問題是為什么我點擊文本框的時候它就會往上這樣跳呢?當你在界面構造器中搭建按鈕、標簽、圖像之類的元素,并且設置約束的時候,同時還會給這些視圖上面添加解釋視圖之間關系的約束。最后,所有這些元素都將取決于屏幕尺寸。比如說,當鍵盤彈出或者當電話撥打進來的時候,視圖控制器的尺寸就會發生變化。

NSLayoutConstraint繼承自NSObject。這不是一種虛類。它當中并沒有可怕的邏輯,這只是一個模型而已。它其中包含了決定等式的一些屬性,可以用來設置等式規則。比如說,按鈕應該始終居中。有一點很重要的是,你在界面構造器中所設計的約束都是模型-數據對象,包含了許多信息。它們并不會通過UIKit 繪制任何東西,也不會移動視圖,只有自動布局才會這樣做。當元素需要在屏幕上顯示或者定位的時候,自動布局將會獲取約束列表。包括寬度、高度、X 軸中值,Y軸邊緣,寬高比例,垂直距離以及垂直邊緣等等。然而,當自動布局配置完你所設置的這些規則之后,就會改變視圖的位置和尺寸。這就是它所做的工作。

假設當你加載應用的時候壓入了一個視圖控制器。首先,應用從故事板加載了視圖。自動布局需要解析布局以便知曉屏幕上所應該展示元素的位置。它會解析你所有的約束,并且找到所有視圖的位置。然而,當你點擊文本框的時候,比如說,鍵盤會彈出,改變了視圖控制器邊緣的尺寸。這樣會觸發布局改變,自動布局就會改變位置。也就是說,自動布局會重新加載約束列表,基于容器尺寸重新計算它們的位置和大小。

那么我們如何正確的使用動畫呢?任何在其中發生的改變都會產生動畫:

 UIView.animateWithDuration(1.0, animations: {
  view.center.x -= 10
})

這里可以使用諸如透明度、背景顏色、位置、尺寸之類可添加動畫的屬性,在這個閉包中 UIKit 會自行進行動畫效果。現在,我們需要獲取到自動布局的所有約束值,重新進行計算并且改變可添加動畫屬性的中心位置和邊緣。我們所做的和之前做的基本相似,不同的是我們需要改變約束值,并且在動畫閉包中讓布局進行改變。讓我快速給大家展示如何用代碼實現這個功能。

這個是我展示如何用約束執行動畫效果的下一個示例。這是一個待辦事項清單應用,應用中的這個頂欄我想讓它能夠動態調整自己的尺寸。為了給大家更好的演示,我將打開這個故事板文件。在場景頂部有一個視圖,下方是一個表視圖。頂部視圖被固定在場景的頂部,表視圖則被固定到了場景底部,并且它們之間也添加了約束。頂部視圖有一個高度為 60 px 的約束。

我們有兩種通過約束執行動畫的方式。第一種是在打算改變約束值的時候。比如說,你可以通過屬性來編輯其高度。我知道我們要為哪個約束添加動畫,因此我會將這個約束添加輸出口(outlet)到視圖控制器當中。由于約束是在界面構造器中添加的,因此我們可以為之添加輸出口,然后通過該輸出口來實時訪問其約束值。我打算將其放在一個名為 menuHeight 的IBOutlet當中,它的類型是NSLayoutConstraint。

@IBOutlet weak var menuHeight: NSLayoutConstraint!

接著,對于其他諸如按鈕、文本框之類的視圖,我們可以前往故事板,找到其高度約束,然后前往連接檢查器(Connections Inspector),將新的引用輸出口拖到視圖控制器上來建立輸出口和約束之間的關聯。

 @IBAction func actionToggleMenu(sender: AnyObject) {

    menuHeight.constant = 200

}

我改變了這個約束的值,由于其高度被固定為了200,因此現在還不會有任何情況出現。現在我們完全掌控了這個約束,因此動畫現在就變得十分簡單了。將其通過animateWithDuration或者彈性動畫(spring animation)包裹起來。我打算使用彈性動畫。這也是默認的 UIKit API 之一。一切都很正常:
 UIView.animateWithDuration(1.0, delay: 0.0,
  usingSpringWithDamping: 0.33, initialSpringVelocity: 0,
  options: [], animations: {

    self.view.layoutIfNeeded()

  }, completion: nil)

如我的幻燈片上所說,同時這也是我想強調的一點,就是在代碼中任何一處改變約束值,然后在動畫閉包當中讓自動布局自行處理。在我們的代碼當中,我們改變了布局的中心位置和邊緣尺寸之類的東西。改變約束值其實并沒有實際改變什么,它并不做任何事情。讓我們仔細看一下這個。這里我的約束進行了更新,然后在這個動畫閉包當中,我們調用了view.layoutIfNeeded(),這讓自動布局功能立即強制重新查看所有的約束,如果有約束值發生了變化,那么就會重新計算并且改變視圖的位置和大小。這就是為什么這個方法稱之為layoutIfNeeded(),因為很可能你并沒有改變什么東西,這樣的話就不會執行任何改變了。當我點擊這個按鈕之后,已改變的約束值就會被動畫顯示出來。有一點很重要的就是我并沒有對表視圖上的任何東西進行動畫操作,我只是改變了頂欄的約束而已,我并沒有讓表視圖做操作,但是……由于頂視圖和表視圖之間建立了約束,因此改變其中之一的高度也會導致另一個視圖的高度發生變化。我并沒有明顯地聲明出來,我讓自動布局自行解決這些問題。由于我們定下的規則是這兩個視圖之間有關聯,因此它們都會發生變化。并且這兩者的變化都會有動畫效果,因為它們的邊緣尺寸都在動畫閉包當中被改變了。這確實很方便,現在通過這個小小的例子你就可以做幾乎任何類型的約束動畫了。

第二件我想展示的事情就是改變約束的倍數(multiplier)。倍數是只讀的,要改變倍數的話,你必須遍歷所有的約束,找到你想要的那個約束,將其移除,然后用一個新的約束來替代它。

 for c in titleLabel.superview!.constraints {
  print(c)

  if c.identifier == "Center" {
    c.active = false

    let nc = NSLayoutConstraint(
      item: titleLabel,
      attribute: .CenterX,
      relatedBy: .Equal,
      toItem: titleLabel.superview!,
      attribute: .CenterX,
      multiplier: 1.5,
      constant: 0)
    nc.active = true
  }

}

在 Xcode 7 當中,有一個新的編輯約束的方式,那就是“標識符”。標識符是一種指定約束名字的方式。這個屬性同樣也在 iOS 8 中存在,但是 Xcode 6 并沒有對其顯示。雖然在 Xcode 6 中你仍然可以使用它,但是這樣的話你就需要使用用戶定義者(user definer)和時間特性(time attribute)來設置該標識符。在 Cocoa 當中,你必須設置委托,為之建立關系,而不是使用這個 API。通過這個 API,只要你將約束的 active 的屬性設置為 false,下次自動布局進行計算的時候,它會發現“這個約束沒被激活,我不需要它”。對于添加新約束來說也是一樣的。當你將其設置為“激活”的時候,下次自動布局進行計算的時候就會發現“這個約束被激活了”,然后將會將其加入到約束列表當中。這個 API 是NSLayoutConstraint的靜態方法,你可以同時添加多個約束,它會遍歷所有的約束并將其激活。

這兩種是使用約束建立動畫的簡單方式。添加輸出口并改變約束值,或者通過移走舊有約束然后添加新約束來改變倍數的方法。最重要的是,需要的時候在動畫閉包當中調用layoutIfNeeded()。

堆棧視圖(38:12)

堆棧視圖(Stack View)可以被稱為“沒有約束的自動布局”。這是一種不通過改變約束值就可以實現動畫的方式。我用另一個示例項目來演示堆棧視圖。假設這個項目是一個商店類應用。它會為用戶展示商店所有的書籍清單,也可以允許用戶進行購買。這個應用包含了關于書籍的列表,當用戶點擊詳細視圖控制器的時候,就會看到帶有額外信息的書籍封面。

 import UIKit

class DetailViewController: UIViewController {

  var book: MasterViewController.Book!

  @IBOutlet weak var cover: UIImageView!
  @IBOutlet weak var name: UILabel!
  @IBOutlet weak var year: UILabel!

  @IBOutlet weak var topStack: UIStackView!

  override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    configureView()

    view.addGestureRecognizer(
      UITapGestureRecognizer(target: self, action: "didTap")
    )
  }

  func didTap () {
    UIView.animateWithDuration(0.4, animations: {
      self.topStack.arrangedSubviews.last!.hidden = !self.topStack.arrangedSubviews.last!.hidden
      })

  }

  func configureView() {
    //視圖配置
    if cover != nil {
      cover.image = UIImage(named: "\(book.imageName).jpg")
    }
    if name != nil {
      name.text = book.title
    }
    if year != nil {
      year.text = book.year
    }
  }
}

我想做的就是弄一些用來顯示諸如書名、發布日期之類信息的文本。我打算用一個標簽來顯示這本書的名稱。接著,再顯示另一個書名。堆棧視圖是一種集合大量視圖的方式,并且要集合的這些視圖的關系基本不會改變。有一個名為axis的屬性,用來決定堆棧視圖是水平方向還是垂直方向建立堆棧。此外,還有一個alignment屬性用來決定堆棧的位置,有頂部、底部和中央。堆棧視圖將保留放置在其中視圖的引用,并且還會被視圖屬性所影響。堆棧視圖將為其中的視圖進行約束值管理,因此我們就不必改變這些視圖的約束值。它會自動為我們創建好約束,因此視圖將會按照我們所想的方式進行排列。

See the discussion on Hacker News .

Sign up to be notified of new videos — we won’t email you for any other reason, ever.

 

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!