除基於屬性的動畫系統外,WPF提供了一種建立基於幀的動畫的方法,這種方法只使用代碼。須要作的所有工做是響應靜態的CompositionTarge.Rendering事件,觸發該事件是爲了給每幀獲取內容。這是一種很是低級的方法,除非使用標準的基於屬性的動畫模型不能知足須要(例如,構建簡單的側邊滾動遊戲、建立基於物理的動畫式構建粒子效果模型(如火焰、雪花以及氣泡)),不然不會但願使用這種方法。canvas
構建基於幀的動畫的基本技術很容易。只須要爲靜態的CompositionTarget.Rendering事件關聯事件處理程序。一旦關聯事件處理程序,WPF就開始不斷地調用這個事件處理程序(只要渲染代碼的執行速度足夠快,WPF每秒將調用60次)。在渲染事件處理程序中,須要在窗口中相應地建立或調整元素。換句話說,須要自行管理所有工做。當動畫結束時,分離事件處理程序。dom
下圖顯示了一個簡單示例。在此,隨機數量的圓從Canvas面板的頂部向底部下落。它們(根據隨機生成的開始速度)以不一樣速度降低,但一相同的速率加速。當全部的圓到達底部時,動畫結束。ide
在這個示例中,每一個下落的圓由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軸隨機移動:this
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()方法緩慢移動每一個橢圓。移動距離取決於指定的速度。spa
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面板的底部,就從跟蹤集合中刪除橢圓。這樣,就不須要再處理它們。當遍歷集合時,爲了可以工做而不會致使丟失位置,須要向後迭代,從集合的末尾向起始位置迭代。code
若是橢圓還沒有到達Canvas面板的底部,代碼會提升速度(此外,爲得到磁鐵吸引效果,還能夠根據橢圓與Canvas面板底部的距離來設置速度):xml
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>
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; } } }
顯然,可擴展的這個動畫以使圓跳躍和分散等。使用的技術是相同的——只須要使用更復雜的公式計算速度。
當構建基於幀的動畫時須要注意以下問題:它們不依賴與時間。換句話說,動畫可能在性能好的計算機上運動更快,由於幀率會增長,會更頻繁地調用CompositionTarget.Rendering事件。爲補償這種效果,須要編寫考慮當前時間的代碼。
開始學習基於幀的動畫的最好方式是查看WPF SDK提供的每一幀動畫都很是詳細的示例。該例演示了幾種粒子系統效果,而且使用自定義的TimeTracker類實現了依賴與時間的基於幀的動畫。