本文是我學習《iOS Animations by Tutorials》 筆記中的一篇。 文中詳細代碼都放在個人Github上 andyRon/LearniOSAnimations。html
自動佈局(Auto Layout) 在iOS 6中首次推出,已經存在了一段時間,每次發佈新版本的iOS和Xcode都經歷了一系列成功的迭代。ios
自動佈局背後的核心理念很是簡單:它容許您根據佈局中的每一個元素之間建立的關係來定義應用程序的UI元素的佈局。git
咱們日常開發時已將自動佈局用於靜態的佈局,在本文中將學習使用約束來設置動畫。github
本章節是用自動佈局完成下一章節須要使用的項目Packing List 。關於自動佈局,可參考我以前的文章開始用Swift開發iOS 10 - 3 介紹Auto Layout,這裏就不重複了。swift
約束動畫(Animating Constraints)並不比屬性動畫困難; 它只是有點不一樣。 一般,只需使用新約束替換現有約束,而後讓Auto Layout爲兩個狀態之間的UI設置動畫就能夠了。數組
開始項目Packing List大概以下:bash
在ViewController
中添加約束接口:閉包
@IBOutlet weak var menuHeightConstraint: NSLayoutConstraint!
複製代碼
並讓它與導航欄視圖的高度約束關聯:app
在右上角加號按鈕的Action方法actionToggleMenu()
中添加:ide
isMenuOpen = !isMenuOpen
menuHeightConstraint.constant = isMenuOpen ? 200.0 : 60.0
titleLabel.text = isMenuOpen ? "Select Item" : "Packing List」
複製代碼
點擊加號按鈕後導航欄高度變大,而且title變化。
繼續在actionToggleMenu()
添加布局變化的彈簧動畫:
UIView.animate(withDuration: 1.0, delay: 0.0, usingSpringWithDamping: 0.4, initialSpringVelocity: 10.0, options: .curveEaseIn, animations: {
// 強制更新佈局
self.view.layoutIfNeeded()
}, completion: nil)
複製代碼
在menuHeightConstraint.constant = isMenuOpen ? 200.0 : 60.0
已經更新了約束值,但iOS尚未機會更新佈局。經過從動畫閉包中調用layoutIfNeeded()
強制更新佈局,能夠設置佈局中涉及的每一個視圖的中心和邊界。好比table view也隨着Menu的收縮或增大而收縮或增大,這就是約束的效果,如今至關於一次設置兩個動畫😊。
效果:
讓+
旋轉45°變成x
在上面的動畫閉包中添加:
let angle: CGFloat = self.isMenuOpen ? .pi/4 : 0.0
self.buttonMenu.transform = CGAffineTransform(rotationAngle: angle)
複製代碼
直接用可視化的方式爲視圖約束添加代碼接口(outlet)是相對簡單的方式。有的時候不方便在Interfa Builder使用Control-drag方式添加接口或者不方便添加有太多outlet,這時能夠利用UIView
提供的constraints
屬性,它是當前視圖全部約束的數組。
好比下面代碼:
titleLabel.superview?.constraints.forEach { constraint in
print("-> \(constraint.description)\n")
}
複製代碼
打印結果:
-> <NSLayoutConstraint:0x600002d04320 UIView:0x7ff7df530c00.height == 200 (active)>
-> <NSLayoutConstraint:0x600002d02210 UILabel:0x7ff7df525350'Select Item'.centerX == UIView:0x7ff7df530c00.centerX (active)>
-> <NSLayoutConstraint:0x600002d02a30 UILabel:0x7ff7df525350'Select Item'.centerY == UIView:0x7ff7df530c00.centerY + 5 (active)>
-> <NSLayoutConstraint:0x600002d02d00 H:[UIButton:0x7ff7df715d20'+']-(8)-| (active, names: '|':UIView:0x7ff7df530c00 )>
-> <NSLayoutConstraint:0x600002d030c0 UIButton:0x7ff7df715d20'+'.centerY == UILabel:0x7ff7df525350'Select Item'.centerY (active)>
複製代碼
看上去有點亂,不過仔細看仍是能看出有五個約束分別對應於:
在 actionToggleMenu()
的isMenuOpen = !isMenuOpen
下添加:
titleLabel.superview?.constraints.forEach { constraint in
if constraint.firstItem === titleLabel && constraint.firstAttribute == .centerX {
constraint.constant = isMenuOpen ? -100.0 : 0.0
return
}
}
複製代碼
約束表達式的通用形式以下:
firstItem.firstItemAttribute == secondItem.secondItemAttribute * multiplier + constant
複製代碼
對應於 NSLayoutConstraint
的各類屬性,名字看着很明顯,其中==
對應於屬性relation
,固然也能夠是<=
、>=
等。
實際例子:
Superview.CenterX = 1.0 * UILabel.CenterX + 0.0
複製代碼
這邊的效果:
每一個約束能夠添加 Identifier
屬性,在代碼中就能夠經過 Identifier
獲取這個約束。
繼續在上面的約束後添加:
if constraint.identifier == "TitleCenterY" {
constraint.isActive = false
let newConstraint = NSLayoutConstraint(item: titleLabel, attribute: .centerY, relatedBy: .equal, toItem: titleLabel.superview!, attribute: .centerY, multiplier: isMenuOpen ? 0.67 : 1.0, constant: 5.0)
newConstraint.identifier = "TitleCenterY"
newConstraint.isActive = true
return
}
複製代碼
新加的約束能夠表示爲Title.CenterY = Menu.CenterY * 0.67 + 0.0
,圖示:
運行後效果:
在 actionToggleMenu()
中添加:
if isMenuOpen {
slider = HorizontalItemList(inView: view)
slider.didSelectItem = { index in
print("add \(index)")
self.items.append(index)
self.tableView.reloadData()
self.actionToggleMenu(self)
}
self.titleLabel.superview!.addSubview(slider)
} else {
slider.removeFromSuperview()
}
複製代碼
HorizontalItemList
是自定義的一個UIScrollView
子類,用於menu中左右滾動的視圖,
當點擊TableView的cell時,會調用showItem(_:)
,在這個方法中添加:
// 點擊後創造圖片
let imageView = UIImageView(image: UIImage(named: "summericons_100px_0\(index).png"))
imageView.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.5)
imageView.layer.cornerRadius = 5.0
imageView.layer.masksToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(imageView)
複製代碼
添加約束代碼:
let conx = imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
複製代碼
此方法使用新的NSLayoutAnchor
類,這使得建立常見約束很是容易。 在這裏,您將在圖像視圖的中心x錨點和視圖控制器的視圖之間建立約束。
添加圖片底部約束:
let conBottom = imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: imageView.frame.height)
複製代碼
此約束設置圖像視圖的底部以匹配視圖控制器視圖的底部,加上圖像高度; 這會將圖像定位在屏幕底部邊緣以外,這將做爲動畫的起點。
添加圖片寬度約束:
let conWidth = imageView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.33, constant: -50.0)
複製代碼
這將圖像寬度設置爲屏幕寬度的1/3減去50磅。 目標尺寸是屏幕的1/3; 你將動畫50磅的差別,使圖像「成長」到位。
最後,添加高度和寬度相等約束,並激活上面全部約束:
let conHeight = imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor)
NSLayoutConstraint.activate([conx, conBottom, conWidth, conHeight])
複製代碼
此時點擊TableView的Cell,只能看到下面:
在showItem(_:)
添加:
UIView.animate(withDuration: 0.8, delay: 0.0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.0, animations: {
conBottom.constant = -imageView.frame.size.height/2
conWidth.constant = 0.0
self.view.layoutIfNeeded()
}, completion: nil)
複製代碼
可是此時的效果是:
**想想:**添加了一個視圖,設置了一些約束,而後改變了這些約束並設置了佈局變化的動畫。 可是,視圖從未有機會執行其初始佈局,所以圖像從其左上角的(0, 0)
的默認位置開始🙄。
要解決此問題,只要在動畫開始以前進行初始佈局,在動畫前添加:
view.layoutIfNeeded()
複製代碼
效果變成從下面上來:
上面的彈出圖片會重疊在一塊兒,下個圖片出來以前,須要把上一個圖片移出。
在以前的代碼下添加:
UIView.animate(withDuration: 0.8, delay: 1.0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.0, animations: {
conBottom.constant = imageView.frame.size.height
conWidth.constant = -50.0
self.view.layoutIfNeeded()
}) { (_) in
imageView.removeFromSuperview()
}
複製代碼
效果爲:😝
本文在個人我的博客中地址:系統學習iOS動畫之二:自動佈局動畫