正如上一章介紹,WPF動畫通過一組動畫類(Animation類)表示。使用少數幾個熟悉設定相關資訊,如開始值、結束值以及持續時間。這顯然使得它們非常适合于XAML。不是很清晰的時:如何為特定的事件和屬性關關聯畫,以及如何在正确的時間觸發動畫。
在所有聲明式動畫中都會用到如下兩個要素:
- 故事闆。故事闆是BeginAnimation()方法的XAML等價物。通過故事闆将動畫指定到合适的元素和屬性。
- 事件觸發器。事件觸發器響應屬性變化或事件(如按鈕的Click事件),并控制故事闆。例如,為了開始動畫,事件觸發器必須開始故事闆。
一、故事闆
故事闆是增強的事件線,可用來分組多個動畫,而且具有控制動畫播放的能力——暫停、停止以及改變播放位置。然而,Storyboard類提供的最基本功能是,能夠使用TargetProperty和TargetName屬性指向某個特定屬性和特定元素。換句話說,故事闆在動畫和希望應用動畫的屬性之間架起了一座橋梁。
下面的标記示範了如何定義用于管理DoubleAnimation的故事闆:
<Storyboard TargetName="cmdGrow" TargetProperty="Width">
<DoubleAnimation From="160" To="300" Duration="0:0:5"></DoubleAnimation>
</Storyboard>
TargetName和TargetProperty都是附加屬性。這意味着可以直接将他們應用于動畫,如下所示:
<Storyboard >
<DoubleAnimation Storyboard.TargetName="cmdGrow" Storyboard.TargetProperty="Width"
From="160" To="300" Duration="0:0:5">
</DoubleAnimation>
</Storyboard>
上面的文法更常用,因為通過這種文法可在同一個故事闆中放置幾個動畫,并且每個動畫可用于不同的元素和屬性。
定義故事闆是建立動畫的第一步。為讓故事闆實際運作起來,還需要有事件觸發器。
二、事件觸發器
在“【WPF學習】第三十七章 觸發器 ”時第一次提到事件觸發器。樣式提供了一種将事件觸發器關聯到元素的方法。然而,可在如下4個位置定義事件觸發器:
- 在樣式中(Styles.Triggers集合)
- 在資料目标中(DataTemplate.Triggers集合)
- 在控件模闆中(ControlTemplate.Triggers集合)
- 直接在元素中定義事件觸發器(FrameworkElement.Triggers集合)
當建立事件觸發器時,需要制定開始出發其的路由事件和由觸發器執行的一個或多個動作。對于動畫,最常用的動作是BeginStoryboard,該動作相當于調用BeginAnimation()方法。
下面的示例使用按鈕的Triggers集合為Click事件關聯某個動畫。當單擊按鈕時,該動畫增長按鈕:
<Button Margin="10" Name="cmdGrow" Height="40" Width="160"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Width"
To="300" Duration="0:0:5">
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
<Button.Content>
Click and Make Me Grow
</Button.Content>
</Button>
Storyboard.TargetProperty屬性指定了希望改變的屬性(在這個示例中是Width屬性)。如果沒有提供類的名稱,故事闆使用其父元素,在此使用的是希望擴充的按鈕。如果希望設定附加屬性(如Canvas.Left或Canvas.Top),需要在括号中封裝整個屬性,如下所示:
<DoubleAnimation Storyboard.TargetName="(Canvas.Top)" .../>
在這個示例中需不需要使用Storyboard.TargetName屬性。當忽略該屬性時,故事闆使用父元素,在此是按鈕。
在這個示例中使用的聲明式方法和前面示範的隻使用代碼的方法存在如下差別:To值被寫死為300個機關,而不是相對于包含按鈕的視窗的尺寸設定。如果希望使用視窗寬度,需要使用資料綁定表達式,如下所示:
<DoubleAnimation Storyboard.TargetProperty="Width"
To="{Binding ElementName=cmdGrow, Path=Width}" Duration="0:0:5">
</DoubleAnimation>
這仍不能準确地得到所希望的結果。在此,按鈕從目前尺寸增大到視窗的完整寬度。隻使用代碼的方法使用一種簡單的計算,将按鈕擴大到比整個視窗寬度小30個機關的值。但XAML不支援内聯計算。一種解決方法是建構能夠自動完成工作的IValueConverter接口。如下所示的示例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Data;
namespace Animation
{
public class ArithmeticConverter : IValueConverter
{
private const string ArithmeticParseExpression = "([+\\-*/]{1,1})\\s{0,}(\\-?[\\d\\.]+)";
private Regex arithmeticRegex = new Regex(ArithmeticParseExpression);
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is double && parameter != null)
{
string param = parameter.ToString();
if (param.Length > 0)
{
Match match = arithmeticRegex.Match(param);
if (match != null && match.Groups.Count == 3)
{
string operation = match.Groups[1].Value.Trim();
string numericValue = match.Groups[2].Value;
double number = 0;
if (double.TryParse(numericValue, out number)) // this should always succeed or our regex is broken
{
double valueAsDouble = (double)value;
double returnValue = 0;
switch (operation)
{
case "+":
returnValue = valueAsDouble + number;
break;
case "-":
returnValue = valueAsDouble - number;
break;
case "*":
returnValue = valueAsDouble * number;
break;
case "/":
returnValue = valueAsDouble / number;
break;
}
return returnValue;
}
}
}
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
ArithmeticConverter
<Window x:Class="Animation.XamlAnimation"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Animation"
Title="XamlAnimation" Height="300" Width="300">
<Window.Resources>
<local:ArithmeticConverter x:Key="converter"></local:ArithmeticConverter>
</Window.Resources>
<Button Padding="10" Name="cmdGrow" Height="40" Width="160"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Width"
To="{Binding ElementName=window,Path=Width,Converter={StaticResource converter},ConverterParameter=-30}"
Duration="0:0:5"></DoubleAnimation>
<DoubleAnimation Storyboard.TargetProperty="Height"
To="{Binding ElementName=window,Path=Height,Converter={StaticResource converter},ConverterParameter=-50}"
Duration="0:0:5"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Button.Triggers>
<Button.Content>
Click and Make Me Grow
</Button.Content>
</Button>
</Window>
XamlAnimation
使用樣式關聯觸發器
FrameworkElement.Triggers集合有點奇怪,它僅支援事件觸發器。其他觸發器集合(Style.Triggers、DataTemplate.Triggers與ControlTemplate.Triggers)的功能更強大,他們支援三種基本類型的WPF觸發器:屬性觸發器、資料觸發器以及事件觸發器。
使用事件觸發器是關關聯畫的最常用方式,但并不是唯一的選擇。如果使用位于樣式、資料模闆或控件模闆中的Triggers集合,還可建立當屬性值發生變化時進行響應的屬性觸發器。例如,下面的樣式複制了前面顯示的示例。當IsPressed屬性為true時,該樣式觸發一個故事闆:
<Window.Resources>
<Style x:Key="GrowButtonStyle">
<Style.Triggers>
<Trigger Property="Button.IsPressed" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Width"
To="250" Duration="0:0:5"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
可使用兩種方式為屬性觸發器關關聯作。可使用Trigger.EnterActions設定當屬性改變到指定的數值時希望執行的動作(在上面的示例中,當IsPressed屬性值變為true時),也可以使用Trigger.ExitActions設定當屬性改變回原來的數值時執行的動作(當IsPressed屬性的值變回false時)。這是一種封裝一堆互補動畫的簡便方法。
下面的按鈕使用上面顯示的樣式:
<Button Padding="10" Name="cmdGrow" Height="40" Width="160" Style="{StaticResource GrowButtonStyle}"
HorizontalAlignment="Center" VerticalAlignment="Center">
Click and Make Me Grow
</Button>
請記住,不見得在樣式中使用屬性觸發器。也可使用事件觸發器,就像在前面介紹的那樣。最後,不見得以與使用樣式的按鈕相分離的方式定義樣式(也可使用内聯樣式設定Button.Style屬性)。但是這種兩部分相分離的方法更常用,并且提供了為多個元素應用相同的靈活性。
三、重疊動畫
故事闆提供了改變處理重疊動畫方式的能力——換句話說,決定第二個動畫何時被應用到已經具有一個正在運作的動畫的屬性上。可使用BeginStoryboard.HandoffBehavior屬性改變處理重疊動畫的方式。
通常,當兩個動畫互相重疊時,第二個動畫會立即覆寫第一個動畫。這種行為就是所謂的“快照并替換”(由HandoffBehavior枚舉中的SnapshotAndReplace值表示)。當第二個動畫開始時,第二個動畫擷取屬性目前值(基于第一個動畫)的快照,停止動畫,并用新動畫替換第一個動畫。
另一個HandoffBehavior選項是Compose,這種方式将第二個動畫融合到第一個動畫的時間線中。例如,分析ListBox示例的修改版本,當縮小按鈕時使用HandoffBehavior.Compose:
<EventTrigger RoutedEvent="ListBoxItem.MouseLeave">
<EventTrigger.Actions>
<BeginStoryboard HandoffBehavior="Compose">
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="FontSize"
BeginTime="0:0:0.5" Duration="0:0:0.2"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
現在,如果将滑鼠移到ListBoxItem對象上,然後在移開,将看到不同的行為。當滑鼠移開項時,項會繼續擴張,這種行為非常明顯,知道第二個動畫到達其0.5秒得開始時間延遲,然後,第二個動畫會縮小按鈕。如果不使用Compose行為,在第二個動畫開始之前的0.5秒得時間間隔内,按鈕會處于等待狀态,并固定為目前尺寸。
使用組合的HandoffBehavior行為需要更大開銷。這是因為當第二個動畫開始時,用于運作原來動畫的時鐘不能被釋放。相反,這個時鐘會繼續保持存活,知道ListBoxItem對象被垃圾回收或為相同的屬性應用新的動畫為止。
四、同步的動畫
Storyboard類間接地繼承自TimelineGroup類,是以Storyboard類能包含多個動畫,最令人高興的是,這些動畫可以作為一組進行管理——這意味着他們在同一時間開始。
為檢視這個一個示例,分析下面的故事闆。它開始兩個動畫,一個動畫用于按鈕的Width屬性,而另一個動畫用于按鈕的Height屬性。因為動畫被分組到故事闆中,它們共同增加按鈕的尺寸,是以可得到比在代碼中通過簡單地多次調用BeginAnimation()方法得到的效果更趨向同步的效果。
<EventTrigger RoutedEvent="Button.Click">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Width"
To="300" Duration="0:0:5"></DoubleAnimation>
<DoubleAnimation Storyboard.TargetProperty="Height"
To="300" Duration="0:0:5"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
在這個示例中,兩個動畫具有相同的持續時間,但這并不是必須的,對于在不同時間結束的動畫,唯一需要考慮的是它們的FillBehavior行為。如果一個動畫的FillBehavior屬性被設定為HoldEnd,它會保持值直到故事闆中所有的動畫都結束。如果故事闆的FillBehavior屬性是HoldEnd,最後那個動畫的值将被永久儲存(直到使用新的動畫替換這個動畫或手動删除了這個動畫)。
上一章列出的Timeline類的屬性開始變得特别有用。例如,可通過SpeedRatio屬性使故事闆中的某個動畫比其他動畫更快,也可以使用BeginTime屬性相對于一個動畫來編譯另一個動畫的開始時間,使該動畫在特定的時間點開始。
五、控制播放
到目前位置,已在事件觸發器中使用了一個動作——加載動畫的BeginStoryboard動作。然而,一旦建立故事闆,就可以用在其他動作控制故事闆。這些工作類都繼承自ControllableStoryboardAction類,下表列出了這些類。
表 控制故事闆的動作類
幫助文檔中沒有記載會妨礙使用這些動作的内容。為成功地執行這些動作,必須在同一個Triggers集合中定義所有觸發器。如果将BeginStoryboard動作的觸發器和PauseStoryboard動作的觸發器放置到不同集合中,PauseStoryboard動作就無法工作。為檢視需要使用的設計,分析示例是有幫助的。
例如,分析下圖中顯示的視窗。該視窗使用一個網格在完全相同的位置精确地重疊了兩個Image元素。最初,隻有最頂部的圖像可見。但當動畫運作是,該圖像從1到0逐漸地增加透明度,最終使夜間的場景完全蓋過白天場景。效果就像是圖像從白天變換到黑夜,就像連續的随時間流逝的照片。
下面的标記定義了包含兩個圖像的Grid控件:
<Grid>
<Image Source="night.jpg"></Image>
<Image Source="day.jpg" Name="imgDay"></Image>
</Grid>
下面是從一幅圖像淡入到另一幅圖像的動畫:
<DoubleAnimation Storyboard.TargetName="imgDay" Storyboard.TargetProperty="Opacity"
From="1" To="0" Duration="0:0:10"></DoubleAnimation>
為增加這個示例的趣味性,還在底部提供了幾個用于控制動畫播放的按鈕。使用這些按鈕,可執行典型的媒體播放器動作,如暫停、恢複播放以及停止(可添加其他按鈕來改變速度系數以及挑選特定的時間)。
下面的标記定義了這些按鈕:
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center">
<Button Name="cmdStart">Start</Button>
<Button Name="cmdPause">Pause</Button>
<Button Name="cmdResume">Resume</Button>
<Button Name="cmdStop">Stop</Button>
<Button Name="cmdMiddle">Move To Middle</Button>
</StackPanel>
通常,可選擇在每個按鈕的Triggers集合中放置事件觸發器。然而,在前面已解釋過,對于動畫這種方法不能工作。最簡單的解決方法是在一個地方定義所有事件觸發器,例如,在包含元素的Triggers集合中,使用EventTrigger.SourceName屬性關聯這些事件觸發器。隻要SourceName屬性和為按鈕設定的Name屬性相比對,觸發器就會應用到恰當的按鈕上。
這個示例中,可使用包含這些按鈕的StackPanel面闆的Triggers集合。然而,使用頂級元素(在這個示例中是視窗)的Triggers集合通常最簡單。這樣,就可在使用者界面中将按鈕移到不同的位置,而不會禁用他們的功能。
<Window.Triggers>
<EventTrigger SourceName="cmdStart" RoutedEvent="Button.Click">
<BeginStoryboard Name="fadeStoryboardBegin">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="imgDay" Storyboard.TargetProperty="Opacity"
From="1" To="0" Duration="0:0:10"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger SourceName="cmdPause" RoutedEvent="Button.Click">
<PauseStoryboard BeginStoryboardName="fadeStoryboardBegin">
</PauseStoryboard>
</EventTrigger>
<EventTrigger SourceName="cmdResume" RoutedEvent="Button.Click">
<ResumeStoryboard BeginStoryboardName="fadeStoryboardBegin"></ResumeStoryboard>
</EventTrigger>
<EventTrigger SourceName="cmdStop" RoutedEvent="Button.Click">
<StopStoryboard BeginStoryboardName="fadeStoryboardBegin"></StopStoryboard>
</EventTrigger>
<EventTrigger SourceName="cmdMiddle" RoutedEvent="Button.Click">
<SeekStoryboard BeginStoryboardName="fadeStoryboardBegin"
Offset="0:0:5"></SeekStoryboard>
</EventTrigger>
</Window.Triggers>
注意,必須為BeginStoryboard動作指定名稱(在這個示例中,名稱是fadeStoryboardBegin)。其他觸發器通過為BeginStoryboardName屬性指定這個名稱,連接配接到相同的故事闆。
當使用故事闆動作時将遇到限制。他們提供的屬性(如SeekStoryboard.Offset和SetStoryboardSpeedRatio.SpeedRatio屬性)不是依賴性項屬性,這會限制使用資料綁定表達式。例如,不能自動讀取Slider.Value屬性值并将其應用到SetStoryboardSpeedRatio.SpeedRatio動作,因為SpeedRatio屬性不接受資料綁定表達式。可能認為通過使用Storyboard對象的SpeedRatio屬性來解決這個問題。但這是行不同的,當動畫開始時,讀取SpeedRatio值并建立一個動畫時鐘。此後,即使改變了SpeedRatio屬性的值,動畫也仍會保持正常的速度。
如果希望動态調整速度或位置,唯一的解決方法是使用代碼。Storyboard類中的方法提供了與故事闆觸發器相同的功能,包括Begin()、Pause()、Resume()、Seek()、Stop()、SkipToFill()、SetSpeedRatio()以及Remove()方法。
要通路Storyboard對象,必須在标記中設定其Name屬性:
<Storyboard Name="fadeStoryboard">
現在隻需要編寫恰當的事件處理程式,并使用Storyboard對象的方法(請記住,簡單地改變故事闆的屬性(比如SpeedRatio)是沒有任何效果的,它們僅配置當動畫開始時将要使用的設定)。
當拖動Slider控件上的滑塊時,下面的事件處理程式會進行響應。該事件處理程式擷取滑動條的值(範圍是0~3),并使用該數值應用新的速率:
private void sldSpeed_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
fadeStoryboard.SetSpeedRatio(this, sldSpeed.Value);
}
注意,SetSpeedRatio()方法需要兩個參數。第一個參數是頂級動畫容器(在這個示例中,是指目前視窗)。所有故事闆方法都需要這個引用。第二個參數是新的速率。
六、監視動畫進度
上一節顯示的動畫播放器仍缺少一個在大多數媒體播放器中都具有的功能——确定目前位置的能力。為使這個動畫播放器更加精緻,可添加一些文本來顯示時間的流逝,并添加進度條來訓示動畫隻需的速度。下圖顯示了使用這兩個細節的動畫播放器的修改版。
添加這些細節相當簡單。首先需要使用TextBlock元素顯示時間,而後需要使用ProgressBar控件顯示圖形進度條,可能認為,可使用資料綁定表達式設定TextBlock值和ProgressBar内容,但這是行不同的。因為從故事闆中檢索目前動畫時鐘相關的唯一方式是使用方法,如GetCurrentTime()和GetCurrentProgress()。無法從屬性中擷取相同的資訊。
最簡單的解決方法是響應下表中列出的某個故事闆事件。
表 故事闆事件
名 稱 | 說 明 |
Completed | 動畫已經到達終點 |
CurrentGlobalSpeedInvalidated | 速度發生了變化,或者動畫被暫停、重新開始、停止或移到某個新的位置。當動畫時鐘反轉時(在可反轉動畫的終點),以及當動畫加速和減速時,也會引發該事件 |
CurrentStateInvalidated | 動畫已經開始或結束 |
CurrentTimeInvalidated | 動畫時鐘已經向前移動了一個步長,正在更改動畫。當動畫開始、停止或結束時也會引發該事件 |
RemoveRequested | 動畫正在被移除。使用動畫的屬性随後會傳回為原來的值 |
這個示例需要使用CurrentTimeInvalidated事件,每次向前移動動畫時鐘都會引發該事件(通常,每秒移動60此,但如果執行的代碼需要更長時間,可能會丢失時鐘刻度)。
當引發CurrentTimeInvalidated事件時,發送者是Clock對象(Clock類位于System.Windows.Media.Animation名稱空間)。可以通過Clock對象檢索目前時間,目前時間使用TimeSpan對象表示;并且可檢索目前進度,目前進度使用0~1之間的數值表示。
下面的代碼更新标簽和進度條:
private void storyboard_CurrentTimeInvalidated(object sender, EventArgs e)
{
// Sender is the clock that was created for this storyboard.
Clock storyboardClock = (Clock)sender;
if (storyboardClock.CurrentProgress == null)
{
lblTime.Text = "[[ stopped ]]";
progressBar.Value = 0;
}
else
{
lblTime.Text = storyboardClock.CurrentTime.ToString();
progressBar.Value = (double)storyboardClock.CurrentProgress;
}
}
作者:Peter Luo
出處:https://www.cnblogs.com/Peter-Luo/
本文版權歸作者和部落格園共有,歡迎轉載,但必須給出原文連結,并保留此段聲明,否則保留追究法律責任的權利。