動畫祕訣和訣竅
在 WPF 中使用動畫時有一些秘訣和訣竅,可以讓動畫有較好的表現,並減少您的挫折。
一般問題
以動畫顯示捲軸或滑桿位置會凍結
如果您使用在 FillBehavior 中有 HoldEnd (預設值) 的動畫,以動畫顯示捲軸或滑桿的位置,使用者將無法再移動捲軸或滑桿。 這是因為即使動畫結束,仍然在覆寫目標屬性的基底值。 若要停止動畫繼續覆寫屬性的目前值,請移除動畫,或在動畫的 FillBehavior 中指定 Stop。 如需詳細資訊和範例,請參閱 使用分鏡腳本設定以動畫顯示之後的屬性。
以動畫顯示動畫的輸出沒有任何效果
您無法以動畫顯示已是另一個動畫輸出的物件。 例如,如果您使用 ObjectAnimationUsingKeyFrames 以 Fill 到 Rectangle 的動畫顯示 RadialGradientBrush 的 SolidColorBrush,您無法將 RadialGradientBrush 或 SolidColorBrush 的任何屬性動畫化。
無法在以動畫顯示屬性之後變更該屬性的值
在某些情況下,即使動畫已結束,您可能無法變更已經以動畫顯示的屬性值。 這是因為即使動畫結束,仍然在覆寫屬性的基底值。 若要停止動畫繼續覆寫屬性的目前值,請移除動畫,或在動畫的 FillBehavior 中指定 Stop。 如需詳細資訊和範例,請參閱 使用分鏡腳本設定以動畫顯示之後的屬性。
變更時間軸沒有任何作用
雖然大部分的 Timeline 屬性都可以動畫化且可以建立資料繫結,但變更使用中 Timeline 的屬性值似乎沒有任何作用。 這是因為,當 Timeline 開始時,計時系統就會建立 Timeline 的複本並用來建立 Clock 物件。 修改原始內容不會影響系統的複本。
若要讓 Timeline 反映變更,必須重新產生時鐘並用來置換之前建立的時鐘。 時鐘不會自動產生。 以下是幾種可套用時間軸變更的方式︰
如果時間軸是或屬於 Storyboard,您可以使用 BeginStoryboard 或 Begin 方法重新套用其分鏡腳本來反映變更。 這也會一併重新啟動動畫。 在程式碼中,您可以使用 Seek 方法,讓分鏡腳本回到先前的位置。
如果您使用 BeginAnimation 方法直接將動畫套用至屬性,請再次呼叫 BeginAnimation 方法並將已修改的動畫傳送至該方法。
如果您直接在時鐘層級運作,請建立並套用一組新的時鐘,並使用它們來取代前一組產生的時鐘。
如需時間軸和時鐘的詳細資訊,請參閱動畫和計時系統概觀。
FillBehavior.Stop 未如預期運作
有時將 FillBehavior 屬性設定為 Stop 似乎沒有任何作用,例如某個動畫「交接」給另一個動畫時,因為 HandoffBehavior 設定為 SnapshotAndReplace。
下列範例會建立 Canvas、Rectangle 和 TranslateTransform。 TranslateTransform 將動畫化,讓 Rectangle 在 Canvas 四處移動。
<Canvas Width="600" Height="200">
<Rectangle
Canvas.Top="50" Canvas.Left="0"
Width="50" Height="50" Fill="Red">
<Rectangle.RenderTransform>
<TranslateTransform
x:Name="MyTranslateTransform"
X="0" Y="0" />
</Rectangle.RenderTransform>
</Rectangle>
</Canvas>
本節中的範例會使用上述物件來示範 FillBehavior 屬性的表現行為不如您預期的幾種情況。
FillBehavior="Stop" 且有多個動畫的 HandoffBehavior
有時,這看起來像是動畫被第二個動畫取代時忽略其 FillBehavior 屬性。 舉例來說,下列範例建立了兩個 Storyboard 物件並用來動畫化上例所顯示的相同 TranslateTransform。
第一個 Storyboard,也就是 B1
會以動畫顯示 X 從 0 到 350 的 TranslateTransform 屬性,這會讓矩形向右移動 350 像素。 當動畫到達其持續時間結束並停止播放時,X 屬性會還原成其原始值 0。 結果,矩形會向右移動 350 像素,然後再跳回其原始位置。
<Button Content="Start Storyboard B1">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard x:Name="B1">
<DoubleAnimation
Storyboard.TargetName="MyTranslateTransform"
Storyboard.TargetProperty="X"
From="0" To="350" Duration="0:0:5"
FillBehavior="Stop"
/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
第二 Storyboard,也就是 B2
,也會以動畫顯示相同 X 的 TranslateTransform 屬性。 因為在這個 To 中只設定以動畫顯示 Storyboard 屬性,所以動畫會使用該屬性的目前值作為動畫化的起始值。
<!-- Animates the same object and property as the preceding
Storyboard. -->
<Button Content="Start Storyboard B2">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard x:Name="B2">
<DoubleAnimation
Storyboard.TargetName="MyTranslateTransform"
Storyboard.TargetProperty="X"
To="500" Duration="0:0:5"
FillBehavior="Stop" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
如果您在播放第一個 Storyboard 時按下第二個按鈕,則可預期發生下列行為:
第一個分鏡腳本結束並將矩形傳回其原始位置,因為動畫的 FillBehavior 為 Stop。
第二個分鏡腳本會生效,並從目前的位置 (現在是 0) 以動畫顯示到 500。
但這不是發生的動作。 相反地,矩形並未跳回,而是繼續向右移動。 這是因為第二個動畫使用第一個動畫的目前值做為其開始值,並從該值以動畫顯示到 500。 當第二個動畫取代第一個動畫時,因為使用 SnapshotAndReplaceHandoffBehavior,所以第一個動畫的 FillBehavior 無關緊要。
FillBehavior 和已完成的事件
下一個範例示範另一個 StopFillBehavior 似乎沒有作用的案例。 同樣地,此範例使用分鏡腳本,以動畫顯示 X 從 0 到 350 的 TranslateTransform 屬性。 不過,這次範例會註冊 Completed 事件。
<Button Content="Start Storyboard C">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard Completed="StoryboardC_Completed">
<DoubleAnimation
Storyboard.TargetName="MyTranslateTransform"
Storyboard.TargetProperty="X"
From="0" To="350" Duration="0:0:5"
FillBehavior="Stop" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
Completed 事件處理常式會啟動另一個 Storyboard,以動畫顯示相同屬性的目前值到 500。
private void StoryboardC_Completed(object sender, EventArgs e)
{
Storyboard translationAnimationStoryboard =
(Storyboard)this.Resources["TranslationAnimationStoryboardResource"];
translationAnimationStoryboard.Begin(this);
}
Private Sub StoryboardC_Completed(ByVal sender As Object, ByVal e As EventArgs)
Dim translationAnimationStoryboard As Storyboard = CType(Me.Resources("TranslationAnimationStoryboardResource"), Storyboard)
translationAnimationStoryboard.Begin(Me)
End Sub
以下是將第二個 Storyboard 定義為資源的標記。
<Page.Resources>
<Storyboard x:Key="TranslationAnimationStoryboardResource">
<DoubleAnimation
Storyboard.TargetName="MyTranslateTransform"
Storyboard.TargetProperty="X"
To="500" Duration="0:0:5" />
</Storyboard>
</Page.Resources>
當您執行 Storyboard 時,您可能會預期 X 的 TranslateTransform 屬性從 0 到 350 會以動畫顯示,然後在完成後還原為 0 (因為 FillBehavior 設定為 Stop),然後以動畫顯示從 0 到 500。 TranslateTransform 則會改為以動畫顯示從 0 到 350,然後再到 500。
這是因為 WPF 引發事件的順序,而且因為屬性值已快取,所以除非屬性失效,否則不會重新計算。 Completed 事件會優先處理,因為它是由根時間軸 (第一個 Storyboard) 觸發。 目前,X 屬性仍會傳回其動畫化的值,因為它尚未失效。 第二個 Storyboard 使用快取的值作為其起始值,並開始建立動畫效果。
效能
動畫在離開頁面之後繼續執行
當您離開包含執行中動畫的 Page 時,這些動畫都會繼續播放,直到記憶體回收 Page 為止。 根據使用的導覽系統,您離開的頁面可能會留在記憶體中一段不等的時間,同時會因為動畫而耗用資源。 當頁面含有不斷執行的 (「環境」) 動畫時,這種情形會最明顯。
因此,當您離開頁面時,最好使用 Unloaded 事件來移除動畫。
移除動畫有許多不同的方式。 下列技術可用來移除屬於 Storyboard 的動畫。
若要移除您使用事件觸發程序啟動的 Storyboard,請參閱如何:移除分鏡腳本。
若要使用程式碼移除 Storyboard,請參閱 Remove 方法。
不論啟動動畫的方式為何,都可以使用下一個技術。
- 若要從特定屬性移除動畫,請使用 BeginAnimation(DependencyProperty, AnimationTimeline) 方法。 指定要顯示動畫的屬性作為第一個參數,並指定
null
作為第二個。 這將會從屬性移除所有動畫時鐘。
如需以動畫顯示屬性不同方法的詳細資訊,請參閱 屬性動畫技術概觀。
使用 Compose HandoffBehavior 會耗用系統資源
當您使用 StoryboardAnimationTimeline 將 AnimationClock、Compose 或 HandoffBehavior 套用至屬性時,先前與該屬性相關聯的任何 Clock 物件都會繼續取用系統資源;計時系統不會自動移除這些時鐘。
若要避免使用 Compose 套用大量時鐘時發生效能問題,在時鐘完成後,您應該從動畫化的屬性中移除組成的時鐘。 有幾個方式可移除時鐘。
若要從屬性移除所有時鐘,請使用動畫化物件的 ApplyAnimationClock(DependencyProperty, AnimationClock) 或 BeginAnimation(DependencyProperty, AnimationTimeline) 方法。 指定要顯示動畫的屬性作為第一個參數,並指定
null
作為第二個。 這將會從屬性移除所有動畫時鐘。若要從時鐘清單中移除特定 AnimationClock,請使用 Controller 的 AnimationClock 屬性來擷取 ClockController,然後呼叫 Remove 的 ClockController 方法。 這通常是在時鐘的 Completed 事件處理常式中完成。 請注意,只有根時鐘可以由 ClockController 控制;子時鐘的 Controller 屬性會傳回
null
。 另請注意,如果時鐘的有效持續時間是永遠,則不會呼叫 Completed 事件。 在此情況下,使用者必須判斷呼叫 Remove 的時機。
這主要是在存留期較長的物件才會發生的動畫問題。 記憶體回收物件時,也會中斷連接並記憶體回收其時鐘。
如需時鐘物件的詳細資訊,請參閱動畫和計時系統概觀。