在 SwiftUI 中設置漸變動畫 (animated gradient) 的 3 個方法


最近有讀者提問:如何利用 SwiftUI 設置漸變動畫 (animated gradient)、或創建漸變背景動畫?SwiftUI 框架有許多內置組件,例如 LinearGradientAngularGradient,讓開發者設置漸變效果。另外,SwiftUI 也有 .animation 修飾符 (modifier),讓開發者可以簡單地創建動畫。問題是,我們如何結合漸變組件和 .animation 修飾符來創建漸變動畫呢?

在這篇教學文章中,我們會用以下 3 種方法來實作漸變動畫:

  1. 更改 startPointendPoint
  2. 利用 .hueRotation 修飾符
  3. 利用 AnimatableModifier

在編寫這篇文章時,我是使用 Xcode 13 (beta) 版本的,但文章的程式碼也應該適用於 Xcode 12。

更改 startPoint 和 endPoint 來實作漸變動畫

首先,讓我們從一個簡單的線性漸變 (linear gradient) 開始。在 Xcode 創建一個 SwiftUI 專案後,我們就可以在 ContentView 中加入以下程式碼:

LinearGradient(colors: [.purple, .yellow], startPoint: .topLeading, endPoint: .bottomTrailing)
    .ignoresSafeArea()

以上的程式碼會創建出一個漸變背景,從左上角的紫色向右下角漸變到黃色。

swiftui-animated-gradient

接下來,我們就要更改 startPointendPoint,來為漸變顏色設置動畫。動畫開始時,startPoint 會從左上角變為左下角,而 endPoint 就會從右下角變為右上角。為了實作動畫,讓我們宣告一個狀態變數 (state variable) 來保存動畫的狀態:

@State private var animateGradient = false

讓我們如此更新 LinearGradient 視圖的程式碼:

LinearGradient(colors: [.purple, .yellow], startPoint: animateGradient ? .topLeading : .bottomLeading, endPoint: animateGradient ? .bottomTrailing : .topTrailing)
    .ignoresSafeArea()
    .onAppear {
        withAnimation(.linear(duration: 2.0).repeatForever(autoreverses: true)) {
            animateGradient.toggle()
        }
    }

我們在視圖出現時,開始利用動畫來呈現 startPointendPoint 的變化。為了示範,我們在這裡只用了線性動畫,並設置為連續執行。

然後,讓我們在預覽面板中運行範例,來看看漸變動畫。

swiftui-animating-gradient-change

同樣的方法可以應用於不同的漸變效果,例如 RadialGradient。我們可以如此修改程式碼來試試看:

RadialGradient(colors: [.purple, .yellow], center: .center, startRadius: animateGradient ? 400 : 200, endRadius: animateGradient ? 20 : 40)
    .ignoresSafeArea()
    .onAppear {
        withAnimation(.linear(duration: 2.0).repeatForever(autoreverses: true)) {
            animateGradient.toggle()
        }
    }

利用色調旋轉 (Hue Rotation) 來實作漸變動畫

與第一個方法不同,這個方法是透過改變色調旋轉 (hue rotation) 角度,來創建動畫漸變。SwiftUI 有一個內置修飾符名為 .hueRotation,讓我們按指定角度轉換視圖中所有色調。

舉個例子,如果我們在線性漸變中附加了 .hueRotation 修飾符,並如此以 45 度旋轉色調:

LinearGradient(colors: [.purple, .yellow], startPoint: .topLeading, endPoint: .bottomTrailing)
    .hueRotation(.degrees(45))
    .ignoresSafeArea()

漸變顏色就會調整成這樣:

swiftui-animating-gradient-hue-rotation

然後,我們就可以色調旋轉角度的變化進行動畫處理,來創建漸變動畫。讓我們在宣告 animateGradient 狀態變數後,如此修改 .hueRotation 修飾符:

LinearGradient(colors: [.purple, .yellow], startPoint: .topLeading, endPoint: .bottomTrailing)
    .hueRotation(.degrees(animateGradient ? 45 : 0))
    .ignoresSafeArea()
    .onAppear {
        withAnimation(.easeInOut(duration: 5.0).repeatForever(autoreverses: true)) {
            animateGradient.toggle()
        }
    }

我們將狀態變數切換為 true 後,SwiftUI 就會動畫化色調旋轉角度的變化,來呈現一個漸變動畫。

swiftui-gradient-animation

利用 AnimatableModifier 來實作漸變動畫

我們的視圖本來是從上而下由紫色漸變成黃色,如果我們想把兩種漸變顏色轉換成其他顏色,應該如何實作這樣的漸變動畫呢?

如果是這樣,這個方法就最適合了,只是步驟也會比前面兩個方法複雜。

在 Swift 中,我們可以利用 Gradient 結構定義一個漸變,看看以下例子:

let gradient1 = Gradient(colors: [.purple, .yellow])
let gradient2 = Gradient(colors: [.blue, .purple])

如果是從一組顏色轉換成另一組顏色的改變,SwiftUI 無法自動為此設置動畫,因此我們需要利用 AnimatableModifier 協定,來構建自己的實作方法。如果你不了解這個協定,可以參考我們《精通 SwiftUI》一書。

SwiftUI can’t automatically animate the gradient change from one set of colors to another set of color. We have to create our own implementation by adopting the AnimatableModifier protocol. If you are new to the protocol, we have an in-depth discussion in our Mastering SwiftUI book.

首先,讓我們建立一個 AnimatableGradientModifier 結構,並調用 AnimatableModifier 協定,來為漸變顏色的改變創建動畫。

struct AnimatableGradientModifier: AnimatableModifier {
    let fromGradient: Gradient
    let toGradient: Gradient
    var progress: CGFloat = 0.0

    var animatableData: CGFloat {
        get { progress }
        set { progress = newValue }
    }

    func body(content: Content) -> some View {
        var gradientColors = [Color]()

        for i in 0..<fromGradient.stops.count {
            let fromColor = UIColor(fromGradient.stops[i].color)
            let toColor = UIColor(toGradient.stops[i].color)

            gradientColors.append(colorMixer(fromColor: fromColor, toColor: toColor, progress: progress))
        }

        return LinearGradient(gradient: Gradient(colors: gradientColors), startPoint: .topLeading, endPoint: .bottomTrailing)
    }

    func colorMixer(fromColor: UIColor, toColor: UIColor, progress: CGFloat) -> Color {
        guard let fromColor = fromColor.cgColor.components else { return Color(fromColor) }
        guard let toColor = toColor.cgColor.components else { return Color(toColor) }

        let red = fromColor[0] + (toColor[0] - fromColor[0]) * progress
        let green = fromColor[1] + (toColor[1] - fromColor[1]) * progress
        let blue = fromColor[2] + (toColor[2] - fromColor[2]) * progress

        return Color(red: Double(red), green: Double(green), blue: Double(blue))
    }
}

這個結構會以最初的漸變 (fromGradient) 和目標漸變 (toGradient) 為輸入參數 (parameter)。progress 變數會追踪漸變的變化,初始值會設置為 0。當該值設置為 1 時,就代表漸變顏色完全變為 toGradient 的顏色組合。

我在前文提過,我們需要構建自己的方法來實作這種漸變動畫呢。在上面的程式碼中,我們創建了一個 colorMixer 函數(特別鳴謝:SwiftUI-lab),來根據 fromColortoColor、和特定的 progress 計算結果顏色。

progress 數值隨著時間變化,body 就會使用計算出的顏色創建 LinearGradient 視圖。

如此一來,我們就可以動畫化這種從一組顏色轉換成另一組顏色的改變。為方便起見,讓我們創建一個視圖擴充功能 (extension) 來應用 AnimatableGradientModifier

extension View {
    func animatableGradient(fromGradient: Gradient, toGradient: Gradient, progress: CGFloat) -> some View {
        self.modifier(AnimatableGradientModifier(fromGradient: fromGradient, toGradient: toGradient, progress: progress))
    }
}

現在,我們可以如此更新 ContentView 來使用 animatableGradient 修飾符:

struct ContentView: View {
    @State private var progress: CGFloat = 0
    let gradient1 = Gradient(colors: [.purple, .yellow])
    let gradient2 = Gradient(colors: [.blue, .purple])

    var body: some View {

        Rectangle()
            .animatableGradient(fromGradient: gradient1, toGradient: gradient2, progress: progress)
            .ignoresSafeArea()
            .onAppear {
                withAnimation(.linear(duration: 5.0).repeatForever(autoreverses: true)) {
                    self.progress = 1.0
                }
            }
    }
}

我試過在預覽面板執行 App,但它未能呈現動畫。讓我們使用 iPhone 模擬器來測試漸變動畫。

swiftui-gradient-animation-animatablemodifier

譯者簡介:Kelly Chan-AppCoda 編輯小姐。
原文How to Create Animated Gradients in SwiftUI


軟件工程師,AppCoda 創辦人。著有《iOS 13 App 程式設計實力超進化實戰攻略》、《iOS 13 App 程式設計實力超進化實戰攻略》以及《精通 SwiftUI》。曾任職於HSBC, FedEx等公司,專責軟體開發、系統設計。2012年創立AppCoda技術部落格,定期發表iOS程式教學文章。現時專注發展AppCoda,致力於iOS程式教學,產品設計及開發。

blog comments powered by Disqus
Shares
Share This