百日學 Swift(Day 33) – 項目 6 :第 2 部分

百日學 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 的實驗效果不理想)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章