正如上一章介紹,WPF動畫經過一組動畫類(Animation類)表示。使用少數幾個熟悉設置相關信息,如開始值、結束值以及持續時間。這顯然使得它們很是適合於XAML。不是很清晰的時:如何爲特定的事件和屬性關聯動畫,以及如何在正確的時間觸發動畫。html
在全部聲明式動畫中都會用到以下兩個要素:ide
1、故事板學習
故事板是加強的事件線,可用來分組多個動畫,並且具備控制動畫播放的能力——暫停、中止以及改變播放位置。然而,Storyboard類提供的最基本功能是,可以使用TargetProperty和TargetName屬性指向某個特定屬性和特定元素。換句話說,故事板在動畫和但願應用動畫的屬性之間架起了一座橋樑。動畫
下面的標記演示瞭如何定義用於管理DoubleAnimation的故事板:this
<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>
上面的語法更經常使用,由於經過這種語法可在同一個故事板中放置幾個動畫,而且每一個動畫可用於不一樣的元素和屬性。spa
定義故事板是建立動畫的第一步。爲讓故事板實際運行起來,還須要有事件觸發器。設計
2、事件觸發器3d
在「【WPF學習】第三十七章 觸發器 」時第一次提到事件觸發器。樣式提供了一種將事件觸發器關聯到元素的方法。然而,可在以下4個位置定義事件觸發器:code
當建立事件觸發器時,須要制定開始出發其的路由事件和由觸發器執行的一個或多個動做。對於動畫,最經常使用的動做是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(); } } }
<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>
使用樣式關聯觸發器
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屬性)。可是這種兩部分相分離的方法更經常使用,而且提供了爲多個元素應用相同的靈活性。
3、重疊動畫
故事板提供了改變處理重疊動畫方式的能力——換句話說,決定第二個動畫什麼時候被應用到已經具備一個正在運行的動畫的屬性上。可以使用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對象被垃圾回收或爲相同的屬性應用新的動畫爲止。
4、同步的動畫
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屬性相對於一個動畫來編譯另外一個動畫的開始時間,使該動畫在特定的時間點開始。
5、控制播放
到目前位置,已在事件觸發器中使用了一個動做——加載動畫的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()方法須要兩個參數。第一個參數是頂級動畫容器(在這個示例中,是指當前窗口)。全部故事板方法都須要這個引用。第二個參數是新的速率。
6、監視動畫進度
上一節顯示的動畫播放器仍缺乏一個在大多數媒體播放器中都具備的功能——肯定當前位置的能力。爲使這個動畫播放器更加精緻,可添加一些文原本顯示時間的流逝,並添加進度條來指示動畫只需的速度。下圖顯示了使用這兩個細節的動畫播放器的修改版。
添加這些細節至關簡單。首先須要使用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; } }
原文出處:https://www.cnblogs.com/Peter-Luo/p/12380975.html