除基於屬性的動畫系統外,WPF提供了一種創建基於幀的動畫的方法,這種方法只使用代碼。需要做的全部工作是響應靜態的CompositionTarge.Rendering事件,觸發該事件是為了給每幀獲取內容。這是一種非常低級的方法,除非使用標準的基於屬性的動畫模型不能滿足需要(例如,構建簡單的側邊滾動游戲 ...
除基於屬性的動畫系統外,WPF提供了一種創建基於幀的動畫的方法,這種方法只使用代碼。需要做的全部工作是響應靜態的CompositionTarge.Rendering事件,觸發該事件是為了給每幀獲取內容。這是一種非常低級的方法,除非使用標準的基於屬性的動畫模型不能滿足需要(例如,構建簡單的側邊滾動游戲、創建基於物理的動畫式構建粒子效果模型(如火焰、雪花以及氣泡)),否則不會希望使用這種方法。
構建基於幀的動畫的基本技術很容易。只需要為靜態的CompositionTarget.Rendering事件關聯事件處理程式。一旦關聯事件處理程式,WPF就開始不斷地調用這個事件處理程式(只要渲染代碼的執行速度足夠快,WPF每秒將調用60次)。在渲染事件處理程式中,需要在視窗中相應地創建或調整元素。換句話說,需要自行管理全部工作。當動畫結束時,分離事件處理程式。
下圖顯示了一個簡單示例。在此,隨機數量的圓從Canvas面板的頂部向底部下落。它們(根據隨機生成的開始速度)以不同速度下降,但一相同的速率加速。當所有的圓到達底部時,動畫結束。
在這個示例中,每個下落的圓由Ellipse元素表示。使用自定義的EllipseInfo類保存橢圓的引用,並跟蹤對於物理模型而言十分重要的一些細節。在這個示例中,只有如下信息很重要——橢圓沿X軸的移動速度(可很容易地擴張這個類,使其包含沿著Y軸運動的速度、額外的加速信息等)。
public class EllipseInfo { public Ellipse Ellipse { get; set; } public double VelocityY { get; set; } public EllipseInfo(Ellipse ellipse, double velocityY) { VelocityY = velocityY; Ellipse = ellipse; } }
應用程式使用集合跟蹤每個橢圓的EllipseInfo對象。還有幾個視窗級別的欄位,它們記錄計算橢圓下落時使用的各種細節。可很容易地使這些細節變成可配置的。
private List<EllipseInfo> ellipses = new List<EllipseInfo>(); private double accelerationY = 0.1; private int minStartingSpeed = 1; private int maxStartingSpeed = 50; private double speedRatio = 0.1; private int minEllipses = 20; private int maxEllipses = 100; private int ellipseRadius = 10;
當單擊其中某個按鈕時,清空集合,並將事件處理程式關聯到CompositionTarget.Rendering事件:
private bool rendering = false; private void cmdStart_Clicked(object sender, RoutedEventArgs e) { if (!rendering) { ellipses.Clear(); canvas.Children.Clear(); CompositionTarget.Rendering += RenderFrame; rendering = true; } } private void cmdStop_Clicked(object sender, RoutedEventArgs e) { StopRendering(); } private void StopRendering() { CompositionTarget.Rendering -= RenderFrame; rendering = false; }
如果橢圓不存在,渲染代碼會自動創建它們。渲染代碼創建隨機數量的橢圓(當前為20到100個),並使他們具有相同的尺寸和顏色。橢圓被放在Canvas面板的頂部,但他們沿著X軸隨機移動:
private void RenderFrame(object sender, EventArgs e) { if (ellipses.Count == 0) { // Animation just started. Create the ellipses. int halfCanvasWidth = (int)canvas.ActualWidth / 2; Random rand = new Random(); int ellipseCount = rand.Next(minEllipses, maxEllipses + 1); for (int i = 0; i < ellipseCount; i++) { Ellipse ellipse = new Ellipse(); ellipse.Fill = Brushes.LimeGreen; ellipse.Width = ellipseRadius; ellipse.Height = ellipseRadius; Canvas.SetLeft(ellipse, halfCanvasWidth + rand.Next(-halfCanvasWidth, halfCanvasWidth)); Canvas.SetTop(ellipse, 0); canvas.Children.Add(ellipse); EllipseInfo info = new EllipseInfo(ellipse, speedRatio * rand.Next(minStartingSpeed, maxStartingSpeed)); ellipses.Add(info); } } }
如果橢圓已經存在,代碼處理更有趣的工作,以便進行動態顯示。使用Canvas.SetTop()方法緩慢移動每個橢圓。移動距離取決於指定的速度。
else { for (int i = ellipses.Count - 1; i >= 0; i--) { EllipseInfo info = ellipses[i]; double top = Canvas.GetTop(info.Ellipse); Canvas.SetTop(info.Ellipse, top + 1 * info.VelocityY); }
為提高性能,一旦橢圓到達Canvas面板的底部,就從跟蹤集合中刪除橢圓。這樣,就不需要再處理它們。當遍歷集合時,為了能夠工作而不會導致丟失位置,需要向後迭代,從集合的末尾向起始位置迭代。
如果橢圓尚未到達Canvas面板的底部,代碼會提高速度(此外,為獲得磁鐵吸引效果,還可以根據橢圓與Canvas面板底部的距離來設置速度):
if (top >= (canvas.ActualHeight - ellipseRadius * 2 - 10)) { // This circle has reached the bottom. // Stop animating it. ellipses.Remove(info); } else { // Increase the velocity. info.VelocityY += accelerationY; }
最後,如果所有橢圓都已從集合中刪除,就移除事件處理程式,然後結束動畫:
if (ellipses.Count == 0) { // End the animation. // There's no reason to keep calling this method // if it has no work to do. StopRendering(); }
示例完整XAML標記如下所示:
<Window x:Class="Animation.FrameBasedAnimation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="FrameBasedAnimation" Height="396" Width="463.2"> <Grid Margin="3"> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <StackPanel Orientation="Horizontal"> <Button Margin="3" Padding="3" Click="cmdStart_Clicked">Start</Button> <Button Margin="3" Padding="3" Click="cmdStop_Clicked">Stop</Button> </StackPanel> <Canvas Name="canvas" Grid.Row="1" Margin="3"></Canvas> </Grid> </Window>FrameBasedAnimation.xaml
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; namespace Animation { /// <summary> /// FrameBasedAnimation.xaml 的交互邏輯 /// </summary> public partial class FrameBasedAnimation : Window { public FrameBasedAnimation() { InitializeComponent(); } private bool rendering = false; private void cmdStart_Clicked(object sender, RoutedEventArgs e) { if (!rendering) { ellipses.Clear(); canvas.Children.Clear(); CompositionTarget.Rendering += RenderFrame; rendering = true; } } private void cmdStop_Clicked(object sender, RoutedEventArgs e) { StopRendering(); } private void StopRendering() { CompositionTarget.Rendering -= RenderFrame; rendering = false; } private List<EllipseInfo> ellipses = new List<EllipseInfo>(); private double accelerationY = 0.1; private int minStartingSpeed = 1; private int maxStartingSpeed = 50; private double speedRatio = 0.1; private int minEllipses = 20; private int maxEllipses = 100; private int ellipseRadius = 10; private void RenderFrame(object sender, EventArgs e) { if (ellipses.Count == 0) { // Animation just started. Create the ellipses. int halfCanvasWidth = (int)canvas.ActualWidth / 2; Random rand = new Random(); int ellipseCount = rand.Next(minEllipses, maxEllipses + 1); for (int i = 0; i < ellipseCount; i++) { Ellipse ellipse = new Ellipse(); ellipse.Fill = Brushes.LimeGreen; ellipse.Width = ellipseRadius; ellipse.Height = ellipseRadius; Canvas.SetLeft(ellipse, halfCanvasWidth + rand.Next(-halfCanvasWidth, halfCanvasWidth)); Canvas.SetTop(ellipse, 0); canvas.Children.Add(ellipse); EllipseInfo info = new EllipseInfo(ellipse, speedRatio * rand.Next(minStartingSpeed, maxStartingSpeed)); ellipses.Add(info); } } else { for (int i = ellipses.Count - 1; i >= 0; i--) { EllipseInfo info = ellipses[i]; double top = Canvas.GetTop(info.Ellipse); Canvas.SetTop(info.Ellipse, top + 1 * info.VelocityY); if (top >= (canvas.ActualHeight - ellipseRadius * 2 - 10)) { // This circle has reached the bottom. // Stop animating it. ellipses.Remove(info); } else { // Increase the velocity. info.VelocityY += accelerationY; } if (ellipses.Count == 0) { // End the animation. // There's no reason to keep calling this method // if it has no work to do. StopRendering(); } } } } } public class EllipseInfo { public Ellipse Ellipse { get; set; } public double VelocityY { get; set; } public EllipseInfo(Ellipse ellipse, double velocityY) { VelocityY = velocityY; Ellipse = ellipse; } } }FrameBasedAnimation.xaml.cs
顯然,可擴展的這個動畫以使圓跳躍和分散等。使用的技術是相同的——只需要使用更複雜的公式計算速度。
當構建基於幀的動畫時需要註意如下問題:它們不依賴與時間。換句話說,動畫可能在性能好的電腦上運動更快,因為幀率會增加,會更頻繁地調用CompositionTarget.Rendering事件。為補償這種效果,需要編寫考慮當前時間的代碼。
開始學習基於幀的動畫的最好方式是查看WPF SDK提供的每一幀動畫都非常詳細的示例。該例演示了幾種粒子系統效果,並且使用自定義的TimeTracker類實現了依賴與時間的基於幀的動畫。