系統學習iOS動畫之二:自動佈局

Andy_Ron發表於2018-12-27

本文是我學習《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大概如下:

系統學習iOS動畫之二:自動佈局

導航欄高度變化

ViewController中新增約束介面:

@IBOutlet weak var menuHeightConstraint: NSLayoutConstraint!
複製程式碼

並讓它與導航欄檢視的高度約束關聯:

系統學習iOS動畫之二:自動佈局

在右上角加號按鈕的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的收縮或增大而收縮或增大,這就是約束的效果,現在相當於一次設定兩個動畫?。

效果:

系統學習iOS動畫之二:自動佈局

旋轉

+旋轉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)>
複製程式碼

看上去有點亂,不過仔細看還是能看出有五個約束分別對應於:

系統學習iOS動畫之二:自動佈局

設定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,當然也可以是<=>=等。

系統學習iOS動畫之二:自動佈局

實際例子:

Superview.CenterX = 1.0 * UILabel.CenterX + 0.0
複製程式碼

這邊的效果:

系統學習iOS動畫之二:自動佈局

替代約束

每個約束可以新增 Identifier屬性,在程式碼中就可以通過 Identifier獲取這個約束。

image-20181015101142175

繼續在上面的約束後新增:

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,圖示:

image-20181015101900852

執行後效果:

系統學習iOS動畫之二:自動佈局

新增導航欄內容

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中左右滾動的檢視,

系統學習iOS動畫之二:自動佈局

動態建立檢視

當點選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,只能看到下面:

系統學習iOS動畫之二:自動佈局

為動態建立的檢視建立動畫

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)
複製程式碼

但是此時的效果是:

系統學習iOS動畫之二:自動佈局

**想一想:**新增了一個檢視,設定了一些約束,然後改變了這些約束並設定了佈局變化的動畫。 但是,檢視從未有機會執行其初始佈局,因此影像從其左上角的(0, 0)的預設位置開始?。

要解決此問題,只要在動畫開始之前進行初始佈局,在動畫前新增:

view.layoutIfNeeded()
複製程式碼

效果變成從下面上來:

系統學習iOS動畫之二:自動佈局

移出已經出現的圖片

上面的彈出圖片會重疊在一起,下個圖片出來之前,需要把上一個圖片移出。

在之前的程式碼下新增:

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動畫之二:自動佈局

本文在我的個人部落格中地址:系統學習iOS動畫之二:自動佈局動畫

相關文章