本文是我學習《iOS Animations by Tutorials》 筆記中的一篇。 文中詳細程式碼都放在我的Github上 andyRon/LearniOSAnimations。
自動佈局(Auto Layout) 在iOS 6中首次推出,已經存在了一段時間,每次釋出新版本的iOS和Xcode都經歷了一系列成功的迭代。
自動佈局背後的核心理念非常簡單:它允許您根據佈局中的每個元素之間建立的關係來定義應用程式的UI元素的佈局。
我們平常開發時已將自動佈局用於靜態的佈局,在本文中將學習使用約束來設定動畫。
6-自動佈局的介紹
本章節是用自動佈局完成下一章節需要使用的專案Packing List 。關於自動佈局,可參考我之前的文章開始用Swift開發iOS 10 - 3 介紹Auto Layout,這裡就不重複了。
7-約束動畫
約束動畫(Animating Constraints)並不比屬性動畫困難; 它只是有點不同。 通常,只需使用新約束替換現有約束,然後讓Auto Layout為兩個狀態之間的UI設定動畫就可以了。
設定約束動畫
開始專案Packing List大概如下:
導航欄高度變化
在ViewController
中新增約束介面:
@IBOutlet weak var menuHeightConstraint: NSLayoutConstraint!
複製程式碼
並讓它與導航欄檢視的高度約束關聯:
在右上角加號按鈕的Action方法actionToggleMenu()
中新增:
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)>
複製程式碼
看上去有點亂,不過仔細看還是能看出有五個約束分別對應於:
設定UILabel的約束動畫
在 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動畫之二:自動佈局動畫