動畫秘訣和訣竅
更新:2007 年 11 月
在 WPF 中使用動畫時,有一些秘訣和訣竅可以讓動畫以較佳的方式執行,並去除您的挫折感。
一般問題
建立捲軸或滑桿位置的動畫會凍結捲軸或滑桿
如果使用具有 FillBehavior 之 HoldEnd (預設值) 的動畫來建立捲軸或滑桿位置的動畫,則使用者會無法再移動捲軸或滑桿。這是因為即使動畫結束,動畫仍然會覆寫目標屬性的基底值。若要停止動畫覆寫屬性的目前值,請移除動畫,或將 FillBehavior 的 Stop 指定給動畫。如需詳細資訊和範例,請參閱 HOW TO:使用腳本建立屬性的動畫後進行設定。
建立動畫輸出的動畫無效
如果物件是另一個動畫的輸出,則無法建立該物件的動畫。例如,如果使用 ObjectAnimationUsingKeyFrames 來建立 Rectangle 之 Fill 的動畫 (從 RadialGradientBrush 至 SolidColorBrush),則無法建立 RadialGradientBrush 或 SolidColorBrush 之任何屬性的動畫。
建立屬性值的動畫之後無法變更屬性值
在某些情況下,即使動畫已結束,在建立屬性值的動畫之後,還是可能會無法變更該屬性值。這是因為即使動畫結束,動畫仍然會覆寫屬性的基底值。若要停止動畫覆寫屬性的目前值,請移除動畫,或將 FillBehavior 的 Stop 指定給動畫。如需詳細資訊和範例,請參閱 HOW TO:使用腳本建立屬性的動畫後進行設定。
變更時刻表無效
雖然大部分 Timeline 屬性都可以建立動畫,而且可以進行資料繫結,但是變更使用中 Timeline 的屬性值似乎沒有作用。那是因為當開始 Timeline 時,計時系統會複製 Timeline,並使用該複本來建立 Clock 物件。因此,對系統複本而言,修改原始項目並沒有任何作用。
如果 Timeline 要反映變更,則必須重新產生該項目的時鐘,用來取代先前建立的時鐘。時鐘並不會自動產生。下列是數種套用時刻表變更的方式:
如果時刻表就是或屬於 Storyboard,則使用 BeginStoryboard 或 Begin 方法重新套用時刻表的腳本,就可以讓時刻表反映變更。這個舉動同樣會引發重新啟動動畫的副作用。在程式碼中,您可以使用 Seek 方法將腳本回復到它的前一個位置。
如果您已使用 BeginAnimation 方法直接將動畫套用至屬性,請再次呼叫 BeginAnimation 方法,並將修改過的動畫傳遞給它。
如果直接在時鐘層級進行處理,請建立及套用一組新的時鐘,並用它們來取代產生的前一組時鐘。
如需時刻表和時鐘的詳細資訊,請參閱動畫和計時系統概觀。
FillBehavior.Stop 未如預期運作
有時,將 FillBehavior 屬性設定為 Stop 似乎沒有作用 (如因為動畫的 HandoffBehavior 設定為 SnapshotAndReplace 而將動畫「傳遞」至另一個動畫時)。
下列範例會建立 Canvas、Rectangle 和 TranslateTransform。會建立 TranslateTransform 的動畫,以在 Canvas 上移動 Rectangle。
<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) 會將 TranslateTransform 之 X 屬性的動畫從 0 建立到 350,這會將矩形往右移動 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) 也會建立相同 TranslateTransform 之 X 屬性的動畫。因為在這個 Storyboard 中只設定動畫的 To 屬性,所以動畫會使用用來建立動畫的目前屬性值做為開始值。
<!-- 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 時按一下第二個按鈕,可能會希望發生下列行為:
因為動畫具有 Stop 的 FillBehavior,所以第一個腳本會結束,並將矩形傳送回其原始位置。
第二個腳本會作用,並將動畫從目前位置 0 建立到 500。
**但結果並不是那樣,**而是矩形未跳回,而且持續移至右邊。那是因為第二個動畫使用第一個動畫的目前值做為它的開始值,並將動畫從該值建立到 500。當第二個動畫因使用 SnapshotAndReplaceHandoffBehavior 而取代第一個動畫時,第一個動畫的 FillBehavior 會沒有作用。
FillBehavior 和 Completed 事件
下面的範例示範另一個 StopFillBehavior 似乎沒有作用的案例。同樣地,這些範例會使用「腳本」將 TranslateTransform 之 X 屬性的動畫從 0 建立到 350。然而,這次範例會註冊 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);
}
下列是將第二個 Storyboard 定義為資源的標記。
<Page.Resources>
<Storyboard x:Key="TranslationAnimationStoryboardResource">
<DoubleAnimation
Storyboard.TargetName="MyTranslateTransform"
Storyboard.TargetProperty="X"
To="500" Duration="0:0:5" />
</Storyboard>
</Page.Resources>
當您執行 Storyboard 時,可能會預期將 TranslateTransform 之 X 屬性的動畫從 0 建立到 350,然後在完成時回復為 0 (因為它的 FillBehavior 設定是 Stop),然後再將動畫從 0 建立到 500。但是,TranslateTransform 的動畫是從 0 建立到 350,再建立到 500。
那是由於 WPF 引發事件的順序,而且因為已快取屬性值,除非屬性失效,否則並不會進行重新計算屬性值。因為 Completed 事件是由根 (Root) 時刻表 (第一個 Storyboard) 所觸發 (Trigger),所以會先處理該事件。同時,X 屬性因未失效,所以仍然會傳回它的動畫值。第二個 Storyboard 會使用快取值做為它的開始值,並開始建立動畫。
效能
動畫在離開頁面之後繼續執行
當您離開包含執行中動畫的 Page 時,除非對 Page 進行記憶體回收,否則那些動畫仍然會繼續播放。根據使用的巡覽系統,離開的頁面可能會停留在記憶體中一段未定的時間,而它的動畫會耗用資源。當頁面包含不斷執行的 (環境 (Ambient)) 動畫時,這最為明顯。
因此,最好是使用 Unloaded 事件,在您離開頁面時移除動畫。
有幾種不同的方式可以移除動畫。下列技術可以用來移除屬於 Storyboard 的動畫。
若要移除利用事件觸發程序所啟動的 Storyboard,請參閱 HOW TO:移除腳本。
若要使用程式碼來移除 Storyboard,請參閱 Remove 方法。
不論動畫的啟動方式為何,都可能會使用下一個技術。
- 若要從特定屬性移除動畫,請使用 BeginAnimation(DependencyProperty, AnimationTimeline) 方法。將要建立動畫的屬性指定為第一個參數,而 null 做為第二個參數。這會將所有動畫時鐘從這個屬性中移除。
如需各種建立屬性動畫之方式的詳細資訊,請參閱建立屬性動畫技術概觀。
使用 Compose HandoffBehavior 會耗用系統資源
當您使用 ComposeHandoffBehavior 將 Storyboard、AnimationTimeline 或 AnimationClock 套用至屬性時,先前與該屬性相關聯的任何 Clock 物件仍然會繼續耗用系統資源,計時系統並不會自動移除這些時鐘。
為避免在使用 Compose 套用大量時鐘時發生效能問題,請在完成撰寫的時鐘之後,先將它們從動畫屬性移除。有多種方式可以移除時鐘。
若要移除屬性中的所有時鐘,請使用動畫物件的 ApplyAnimationClock(DependencyProperty, AnimationClock) 或 BeginAnimation(DependencyProperty, AnimationTimeline) 方法。請將要建立動畫的屬性指定為第一個參數,而 null 做為第二個參數。這會將所有動畫時鐘從這個屬性中移除。
若要移除時鐘清單中的特定 AnimationClock,請使用 AnimationClock 的 Controller 屬性擷取 ClockController,然後呼叫 ClockController 的 Remove 方法。這通常是在時鐘的 Completed 事件處理常式中完成。請注意,ClockController 只能控制根時鐘,而子時鐘的 Controller 屬性會傳回 null。同時請注意,如果時鐘的有效期間為永久,則不會呼叫 Completed 事件。在該案例中,使用者需要決定何時呼叫 Remove。
這主要是將存留期 (Lifetime) 長的物件顯示為動畫時發生的問題。對物件進行記憶體回收時,也會中斷其時鐘,並一併進行記憶體回收。
如需時鐘物件的詳細資訊,請參閱動畫和計時系統概觀。