來翻新老專案 (5) - MapleLooker - AnimationView 動畫組件優化
上次翻新的 AnimationView 使用了 CompositionTarget.Rendering
事件
在每次 UI 渲染時更新 Image 物件,並判斷經過時間切換圖片來完成影格動畫
雖然最後呈現效果不錯,但 CPU 佔用量高了點
這次想進一步使用 WPF 裡的動畫系統來改寫原本的動畫組件,優化 AnimationView 的效能
WPF 的動畫系統
WPF 的動畫系統主要由 Animation、Timeline 組成
簡單來說,就是在一個時間軸上定義 UI 在某個時間點應該在哪些位置、移動多少、旋轉或放大
詳細資訊,可參考 官方文件,這邊不多做說明
只要知道,在 XAML 或程式碼內定義好一組動畫,並透過事件或是程式觸發,WPF 就會依照 Timeline 內的描述,更新 UI 的狀態
StoryBoard - 分鏡腳本
是 Timeline 的實作,正如其名,就是用來描述動畫的分鏡腳本
一個分鏡腳本會由多個動畫元素組成
需要規劃在什麼時間,哪些物件的屬性,會在某個持續時間內發生變化
下面是一個簡單的 StoryBoard
範例:
<StoryBoard> |
在這個 StoryBoard 共會執行兩個操作:
- 把 button1 的不透明度在 0.0 秒到 0.5 秒間,從 0.5 調整到 1.0
- 把 button2 的不透明度在 0.5 秒到 1.0 秒間,從 1.0 調整到 0.5
StoryBoard
可以定義在 Resource 內,或是直接透過程式建立
由於 AnimationView 的動畫是動態載入,所以需要透過程式碼動態建立分鏡腳本
Animation - 動畫
WPF 主要有兩個類型的動畫:插值動畫與關鍵影格動畫
插值動畫
最基礎的動畫,設定起始值 From
、終點值 To
與持續時間 Duration
當輸入一個時間 t
,插值動畫會根據時間 t
在 Duration
的位置,計算該時間點的數值
前面範例的 DoubleAnimation
即是一個例子,透明度的變化如下圖:
在 0.0 秒時,button1 的不透明度設定會是 0.5,0.5 秒時則會是 1.0
如果時間 t
在 0.25 秒,則不透明度會透過線性插值計算得到 0.75
除了一般的線性變化,插值動畫也可以設定 Easing Function 讓變化曲線更平滑
下列是一些 Easing Function 的變化曲線範例,圖片取自微軟官方文件
BackEase:
BounceEase:
有關 Easing Function 的部份可參考: Easing 函式 - WPF .NET Framework | Microsoft Learn
除了預設的 Int、Double、Point 等基礎動畫
WPF 還提供 ObjectAnimation 類別可實作自訂動畫
KeyFrame - 關鍵影格動畫
KeyFrame (關鍵影格) 是比插值動畫更有彈性的動畫類型,名稱通常是 XxxAnimationUsingKeyFrames
插值動畫的變化比較單一,沒辦法自由設定。而 KeyFrames 的動畫則可以定義多個時間段的數值,並設定該如何變化
下面是 KeyFrames 動畫的範例:
<DoubleAnimationUsingKeyFrames |
範例中 button1 的不透明度共設定三個關鍵影格,,根據輸入的時間,透明度會在兩個關鍵時間內透過插值 (Interpolation) 來計算當下的值:
由於採用了線性插值 (Linear Interpolation),因此第 0.5 秒的值會是 0.25,第 2 秒的值會是 0.75
除了線性插值以外,WPF 還有提供離散插值 (Discrete Interpolation) 與樣條插值 (Splined Interpolation):
- 離散插值:瞬間變化,在抵達關鍵時間以前數值並不會發生變化,非常適合製作逐格動畫
- 樣條插值:與 Easing Function 類似,可呈現更平滑的數值變化
Path - 路徑動畫
除了插值動畫跟 KeyFrame 動畫以外,還有一個路徑動畫,名稱通常是 XxxAnimationUsingPath
主要是可以使用 Point 設定一系列的位置,且能夠以較平滑的路徑描述移動軌跡
這種類型的動畫我並沒有深入研究,詳細資訊可參考官方文件: 路徑動畫概觀 - WPF .NET Framework | Microsoft Learn
Transform - 轉換
除了調整 UI 的位置、透明度以外,還要能做旋轉、縮放或變形的操作
這些都可以使用 Transform 來達成
Transform 可以在 UI 的 LayoutTransform
跟 RenderTransform
做設定
LayoutTransform
會影響 UI 布局,而 RenderTransform
只影響畫面渲染的結果
常見的 Transform 有:TranslateTransform
、ScaleTransform
和 RotateTransform
- TranslateTransform: 對座標做轉譯,可調整 UI 渲染的位置。(在
LayoutTransform
設定沒有效果,因為某些布局會自動調整 UI 位置,如:GridLayout
) - ScaleTransform: 調整 UI 渲染的大小,需要設定中心點
- RotateTransform: 調整 UI 渲染時的角度,需要設定中心點
除了預設的 Transform 以外,還可以使用 MatrixTransform
自己設定該如何對座標做轉換
RenderTransformOrigin
如果是設定 RenderTransform
,UI 還有一個 RenderTransformOrigin
的選項可以設定轉換時的座標中心
以 0 跟 1 表示 UI 的邊界, 0.5 為 UI 的中心點
如果要以 UI 中心點做旋轉,則 RenderTransformOrigin
需要設定為 0.5,0.5
或是 new Point(0.5, 0.5)
RenderTransformOrigin
只會影響 ScaleTransform
跟 RotateTransform
,對 TranslateTransform
不起作用
用 WPF 的動畫重新描述 AnimationView 的影格動畫
在之前的動畫中,我只定義了 Canvas
,並在每次渲染的時候重新建立 Image
並套用透明度、旋轉、與原點
現在我直接把渲染事件拔掉,預設配置 Canvas
和 Image
,並交由之後建立的 StoryBoard 來操作:
<Canvas |
載入動畫的地方,則建立一個 StoryBoard 並為每個影格建立對應的動畫:
public void LoadAnimation(WzSub property) { |
用程式寫感覺很長一段,但整體上不難理解,就是將每個時間點該播放的動畫都加入 StoryBoard 內
需要注意的是跟 Transform 有關的動畫,以及 StoryBoard 的啟動方式
對 Transform 套用動畫
動畫能夠直接影響 UI 元件,但 Transform 本身不是 UI 元件,因此直接對 Transform 套用動畫並沒有任何效果
為了要讓 Transform 套用動畫,需要先註冊 Transform 物件到 DependencyObject 上 (以 AnimationView 的元件來說,就是綁到 AnimationView 身上)
然後使用 TargetName
去讓動畫操作對應的物件
我在 xaml 定義的階段就已經先用 x:Name
將 Transform 註冊到 AnimationView 上,所以建立動畫時我只要指定 TargetName
就好
StoryBoard 啟動/釋放
從前面的程式中可以看到,我對 StoryBoard 操作時,都會把 canvas
帶入
這樣做的目的是要讓 StoryBoard 在 canvas
內建立 Clock 使動畫可以運行
而一開始的 storyboard.Remove
則是要釋放先前在 canvas
內建立的 Clock 資源
如果沒有指定物件就播放動畫,除了沒辦法操作動畫的播放以外,也沒辦法對 Transform 套用動畫
指定的物件會影響 TargetName
參照的對象
對 UserControl 來說,不論在裡面建立多少元件,預設 NameScope 都會跟 UserControl 一樣
所以不論是從 canvas
去操作 frameOrigin
,還是從 AnimationView
本身去操作 frameOrigin
,都是一樣效果
如果直接對 canvas
建立新的 NameScope,那動畫會找不到 frameOrigin
這個物件
操作 StoryBoard
程式可以對 StoryBoard 做暫停、恢復、設定動畫時間等操作
前提是在使用 Begin()
執行動畫時,需要設定 isControllable
為 true
之前做的暫停跟重設的功能,這邊改操作 StoryBoard 去實現:
private void OnCanvasKeyDown(object sender, KeyEventArgs e) { |
效能
效能這塊比原本直接在渲染事件內設定好很多
原本的方法使得 WPF 在執行動畫時會吃掉大概 9% 的 CPU:
改用動畫系統之後,除了移動畫面比較耗 CPU 以外,一般動畫播放幾乎不耗 CPU:
結語
動畫這塊熟悉之後就比較好操作,一般使用上沒什麼難度,所以這篇比起優化的歷程,主要都是在講動畫系統的部份
一開始我還沒意識到,它其實就是類似 Adobe Flash 或是 Adobe After Effect 之類動畫程式的設計邏輯
因為不是透過 UI 設計動畫,所以寫起來感覺怪怪的,但理解之後,就沒有那麼困難了
由於楓谷的動畫主要還是這種影格動畫,所以才好用 WPF 的動畫系統呈現
如果是 Spine 動畫,大概就只能掛 DirectX 然後直接渲染在上面了