百日學 Swift(Day 33) – Project 6, part two(項目 6 :第 2 部分)
1. Controlling the animation stack(控制動畫堆棧)
**大叔注:這個標題有些……直譯的誤導。視圖後面的修飾器排列起來就像一個堆棧。動畫堆棧實際上是說在修飾器堆棧裏面多次使用動畫修飾。
先看下面的代碼,這兩段代碼說明了修飾器的順序如何重要。
Button("Tap Me") {
// do nothing
}
.background(Color.blue)
.frame(width: 200, height: 200)
.foregroundColor(.white)
Button("Tap Me") {
// do nothing
}
.frame(width: 200, height: 200)
.background(Color.blue)
.foregroundColor(.white)
其中的道理前面有講,而且我們還反覆使用background()
和padding()
創造一個條紋邊框效果。
這就是概念一:修飾符順序很重要,因爲SwiftUI用修飾符按應用順序包裹視圖。
概念二是我們可以animation()
對視圖應用修飾符,以使其隱含地對更改進行動畫處理。
爲了演示這一點,我們可以修改按鈕代碼,以便根據某些狀態顯示不同的顏色。首先,我們定義狀態:
struct CustomViewModifier: View {
@State var show = false // 定義狀態
var body: some View {
VStack(spacing: 15) {
Button("點我變色"){
self.show.toggle() // 狀態切換
}
.frame(width: 200, height: 100, alignment: .center)
.foregroundColor(.white)
.background(Color(show ? .red : .blue)) // 顏色切換
.animation(.default) // 動畫
}
}
}
運行代碼,將看到點擊按鈕會在藍色和紅色之間爲其設置動畫的顏色。
現在給按鈕增加一個圓角修飾器,
Button("點我變色,圓角沒有動畫"){
self.show.toggle() // 狀態切換
}
.frame(width: 200, height: 200)
.background(show ? Color.blue : Color.red)
.animation(.default)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: show ? 60 : 0))
運行後會看到點擊按鈕會使其在紅色和藍色之間進行動畫處理,但是在正方形和圓角矩形之間的切換不會進行動畫處理。
如果將clipShape()
修改器移到動畫之前,如下所示:
Button("點我變色和圓角都有動畫") {
self.enabled.toggle()
}
.frame(width: 200, height: 200)
.background(enabled ? Color.blue : Color.red)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: enabled ? 60 : 0))
.animation(.default)
運行代碼時,背景顏色和剪輯形狀都將進行動畫處理。再次說明順序很重要:animation()
僅影響在它之前發生的更改。
如果應用多個animation()
修改器,則每個修改器控制着之前動畫處理過的所有內容。這樣能夠以各種不同的方式爲狀態變化設置動畫,而不是爲所有屬性統一設置。
例如,可以使用默認動畫來進行顏色更改,但是對剪輯形狀使用插值彈簧:
Button("點我變色和圓角有不同動畫") {
self.enabled.toggle()
}
.frame(width: 200, height: 200)
.background(enabled ? Color.blue : Color.red)
.animation(.default)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: enabled ? 60 : 0))
.animation(.interpolatingSpring(stiffness: 10, damping: 1))
爲了獲得更多控制,可以通過傳遞nil
到修飾器來完全禁用動畫。如果需要立即進行顏色更改,但剪輯形狀保留其動畫,可以這樣編寫:
Button("點我變色無動畫,圓角有動畫") {
self.enabled.toggle()
}
.frame(width: 200, height: 200)
.background(enabled ? Color.blue : Color.red)
.animation(nil)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: enabled ? 60 : 0))
.animation(.interpolatingSpring(stiffness: 10, damping: 1))
2. Animating gestures(動畫手勢)
SwiftUI 允許將手勢附加到任何視圖,並且這些手勢的效果也可以動畫。稍後,我們將更詳細地介紹手勢,但現在讓我們嘗試一些相對簡單的操作:可以在屏幕上拖動的卡片,但是放開後,它會卡回到其原始位置。
首先,我們的初始佈局:
struct ContentView: View {
var body: some View {
LinearGradient(
gradient: Gradient(colors: [.yellow, .red]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.frame(width: 300, height: 200)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
這樣可以在屏幕中央繪製類似卡片的視圖。我們想根據手指的位置在屏幕上移動它,這需要三個步驟。
首先,我們需要某種狀態來存儲其拖動量:
@State private var dragAmount = CGSize.zero
其次,我們要使用該大小來影響卡在屏幕上的位置。SwiftUI爲此提供了一個專用的修飾符offset()
,它使我們能夠調整視圖的 X 和 Y 座標而無需在其周圍移動其他視圖。您可以根據需要輸入離散的 X 和 Y 座標,但是-絕非偶然- offset()
也可以CGSize
直接採用。
因此,第二步是將此修改器添加到線性漸變中:
.offset(dragAmount)
現在重要的部分到了:我們可以創建一個DragGesture
並將其附加到卡上。在這裏我們對拖動手勢有用的兩個額外的修飾符:移動時運行的onChanged()
和結束拖動時運行的onEnded()
。
它們都有一個參數,描述了拖動操作——它的開始位置,當前位置,移動距離等等。對於onChanged()
修改器,我們將讀取拖動的位移,該位移告訴我們拖動距起點有多遠——可以直接將其賦值給dragAmount
以便視圖隨手勢一起移動。對於onEnded()
要完全忽略輸入,因爲需要將設置dragAmount
復位。
因此,現在將此修飾符添加到線性漸變中:
.gesture(
DragGesture()
.onChanged { self.dragAmount = $0.translation }
.onEnded { _ in self.dragAmount = .zero }
)
如果運行代碼,您會看到現在可以拖動漸變卡了,放開拖動時,它將跳回到中心。卡的偏移量由dragAmount
確定,該偏移量又由拖動手勢設置。
現在一切正常,我們可以通過一些動畫使該動作栩栩如生,我們有兩個選擇:添加一個隱式動畫以使拖動和釋放具有動畫效果,或者添加一個顯式動畫以使釋放成爲動畫。
要查看前者的實際效果,請將此修改器添加到線性漸變中:
.animation(.spring())
拖動時,由於彈簧動畫的作用,卡會稍有延遲地移到拖動位置,但是如果突然移動,它也會輕輕地過沖。
要看到明確的動畫在行動,刪除animation()
修改和改變現有的onEnded()
拖拽手勢的代碼如下:
.onEnded { _ in
withAnimation(.spring()) {
self.dragAmount = .zero
}
}
現在,這張卡將立即跟隨您的拖動(因爲沒有被動畫化),但是當您放開它時,它將進行動畫處理。
如果我們將偏移動畫與拖動手勢並稍加延遲相結合,則無需大量代碼就可以創建非常有趣的動畫。
爲了證明這一點,我們可以將文本“ Hello SwiftUI”編寫爲一系列單獨的字母,每個字母的背景顏色和偏移量都由某個狀態控制。使用Array("Hello SwiftUI")
可以得到一個字符串數組:每個元素是一個字符。
struct ContentView: View {
let letters = Array("Hello SwiftUI")
@State private var enabled = false
@State private var dragAmount = CGSize.zero
var body: some View {
HStack(spacing: 0) {
ForEach(0..<letters.count) { num in
Text(String(self.letters[num]))
.padding(5)
.font(.title)
.background(self.enabled ? Color.blue : Color.red)
.offset(self.dragAmount)
.animation(Animation.default.delay(Double(num) / 20))
}
}
.gesture(
DragGesture()
.onChanged { self.dragAmount = $0.translation }
.onEnded { _ in
self.dragAmount = .zero
self.enabled.toggle()
}
)
}
}
如果運行該代碼,您會發現可以拖動任意字母以使整個字符串都跟隨該字符串,只是短暫的延遲會導致類似蛇的效果。當您釋放拖動時,SwiftUI還將添加顏色更改,即使字母移回中心也可以在藍色和紅色之間進行動畫顯示。
3. Showing and hiding views with transitions(使用過渡顯示或隱藏視圖)
SwiftUI最強大的功能之一是能夠自定義視圖的顯示和隱藏方式。之前,您已經瞭解瞭如何使用常規if
條件有條件地包含視圖,這意味着當條件發生變化時,我們可以從視圖層次結構中插入或刪除視圖。
過渡控制插入和刪除的方式,我們可以使用內置過渡,以不同方式組合它們,甚至創建完全自定義的過渡。
爲了說明這一點,這裏有一個VStack
帶有按鈕和一個矩形的:
struct ContentView: View {
var body: some View {
VStack {
Button("Tap Me") {
// do nothing
}
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
}
}
}
我們可以使矩形僅在滿足特定條件時顯示。首先,我們添加一些可以操縱的狀態:
@State private var isShowingRed = false
接下來,我們將該狀態用作顯示矩形的條件:
if isShowingRed {
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
}
最後,我們可以isShowingRed
在按鈕的操作中在true和false之間切換:
self.isShowingRed.toggle()
如果運行該程序,則會看到按下按鈕會顯示並隱藏紅色方塊。沒有動畫。它只是出現而突然消失。
我們可以使用來包裝狀態更改withAnimation()
,從而獲得SwiftUI的默認視圖過渡,如下所示:
withAnimation {
self.isShowingRed.toggle()
}
有了較小的更改,應用程序現在就可以淡入和淡出紅色矩形,同時還可以向上移動按鈕以騰出空間。看起來不錯,但我們可以使用transition()
修飾符做得更好。
例如,我們可以通過在矩形上添加transition()
修飾符來使矩形放大和縮小:
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
.transition(.scale)
現在點擊按鈕看起來更好:矩形會隨着按鈕的騰出而擴大,然後再次點擊時會縮小。
如果要嘗試,還可以嘗試其他幾種轉換。一個有用的是.asymmetric
,它使我們可以在顯示視圖時使用一個過渡,而在消失時使用另一個過渡。要進行嘗試,請使用以下命令替換矩形的現有過渡:
.transition(.asymmetric(insertion: .scale, removal: .opacity))
4. Building custom transitions using ViewModifier(使用 ViewModifier 創建自定義過渡)
爲SwiftUI創建全新的過渡是可能的,而且實際上出乎意料的容易,這使我們可以使用完全自定義的動畫添加和刪除視圖。
.modifier
過渡使此功能成爲可能,該過渡接受我們想要的任何視圖修飾符。要注意的是,我們需要能夠實例化修飾符,這意味着它必須是我們自己創建的修飾符。
爲了嘗試這一點,我們可以編寫一個視圖修改器,讓我們模仿Keynote中的Pivot動畫-它使新幻燈片從其左上角旋轉入。用SwiftUI講,這意味着創建一個視圖修改器,使我們的視圖從一個角旋轉,而不會逃脫它應該位於的邊界。SwiftUI實際上爲我們提供了修改器來做到這一點:rotationEffect()
讓我們在2D空間中旋轉視圖,並clipped()
阻止將視圖繪製到其矩形空間的外部。
rotationEffect()
與相似rotation3DEffect()
,但它始終繞Z軸旋轉。但是,它也使我們能夠控制旋轉的錨點 -視圖的哪一部分應固定在旋轉中心。SwiftUI爲我們提供了一個UnitPoint
用於控制錨,它可以讓我們指定確切的X / Y點的許多內置選項旋轉或使用一個類型- ,.topLeading
,.bottomTrailing
,.center
等等。
讓我們通過創建一個CornerRotateModifier
結構來構造所有代碼,這些結構具有一個錨點來控制旋轉的位置,並控制一個旋轉量:
struct CornerRotateModifier: ViewModifier {
let amount: Double
let anchor: UnitPoint
func body(content: Content) -> some View {
content.rotationEffect(.degrees(amount), anchor: anchor).clipped()
}
}
clipped()
那裏的添加意味着當視圖旋轉時,不會繪製位於其自然矩形之外的零件。
我們可以使用.modifier
過渡直接嘗試一下,但這有點笨拙。一個更好的主意是將其包裝到的擴展中AnyTransition
,使它在其最前端的角從-90旋轉到0:
extension AnyTransition {
static var pivot: AnyTransition {
.modifier(
active: CornerRotateModifier(amount: -90, anchor: .topLeading),
identity: CornerRotateModifier(amount: 0, anchor: .topLeading)
)
}
}
有了這個,我們現在可以使用以下方法將透視動畫附加到任何視圖:
.transition(.pivot)
(大叔注:說實話,3 和 4 的實驗效果不理想)