|
除基于屬性的動(dòng)畫系統(tǒng)外,WPF提供了一種創(chuàng)建基于幀的動(dòng)畫的方法,這種方法只使用代碼。需要做的全部工作是響應(yīng)靜態(tài)的CompositionTarge.Rendering事件,觸發(fā)該事件是為了給每幀獲取內(nèi)容。這是一種非常低級(jí)的方法,除非使用標(biāo)準(zhǔn)的基于屬性的動(dòng)畫模型不能滿足需要(例如,構(gòu)建簡(jiǎn)單的側(cè)邊滾動(dòng)游戲、創(chuàng)建基于物理的動(dòng)畫式構(gòu)建粒子效果模型(如火焰、雪花以及氣泡)),否則不會(huì)希望使用這種方法。 構(gòu)建基于幀的動(dòng)畫的基本技術(shù)很容易。只需要為靜態(tài)的CompositionTarget.Rendering事件關(guān)聯(lián)事件處理程序。一旦關(guān)聯(lián)事件處理程序,WPF就開(kāi)始不斷地調(diào)用這個(gè)事件處理程序(只要渲染代碼的執(zhí)行速度足夠快,WPF每秒將調(diào)用60次)。在渲染事件處理程序中,需要在窗口中相應(yīng)地創(chuàng)建或調(diào)整元素。換句話說(shuō),需要自行管理全部工作。當(dāng)動(dòng)畫結(jié)束時(shí),分離事件處理程序。 下圖顯示了一個(gè)簡(jiǎn)單示例。在此,隨機(jī)數(shù)量的圓從Canvas面板的頂部向底部下落。它們(根據(jù)隨機(jī)生成的開(kāi)始速度)以不同速度下降,但一相同的速率加速。當(dāng)所有的圓到達(dá)底部時(shí),動(dòng)畫結(jié)束。
在這個(gè)示例中,每個(gè)下落的圓由Ellipse元素表示。使用自定義的EllipseInfo類保存橢圓的引用,并跟蹤對(duì)于物理模型而言十分重要的一些細(xì)節(jié)。在這個(gè)示例中,只有如下信息很重要——橢圓沿X軸的移動(dòng)速度(可很容易地?cái)U(kuò)張這個(gè)類,使其包含沿著Y軸運(yùn)動(dòng)的速度、額外的加速信息等)。 public class EllipseInfo { public Ellipse Ellipse { get; set; } public double VelocityY { get; set; } public EllipseInfo(Ellipse ellipse, double velocityY) { VelocityY = velocityY; Ellipse = ellipse; } } 應(yīng)用程序使用集合跟蹤每個(gè)橢圓的EllipseInfo對(duì)象。還有幾個(gè)窗口級(jí)別的字段,它們記錄計(jì)算橢圓下落時(shí)使用的各種細(xì)節(jié)??珊苋菀椎厥惯@些細(xì)節(jié)變成可配置的。 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; 當(dāng)單擊其中某個(gè)按鈕時(shí),清空集合,并將事件處理程序關(guān)聯(lián)到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; } 如果橢圓不存在,渲染代碼會(huì)自動(dòng)創(chuàng)建它們。渲染代碼創(chuàng)建隨機(jī)數(shù)量的橢圓(當(dāng)前為20到100個(gè)),并使他們具有相同的尺寸和顏色。橢圓被放在Canvas面板的頂部,但他們沿著X軸隨機(jī)移動(dòng): 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); } } } 如果橢圓已經(jīng)存在,代碼處理更有趣的工作,以便進(jìn)行動(dòng)態(tài)顯示。使用Canvas.SetTop()方法緩慢移動(dòng)每個(gè)橢圓。移動(dòng)距離取決于指定的速度。 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); } 為提高性能,一旦橢圓到達(dá)Canvas面板的底部,就從跟蹤集合中刪除橢圓。這樣,就不需要再處理它們。當(dāng)遍歷集合時(shí),為了能夠工作而不會(huì)導(dǎo)致丟失位置,需要向后迭代,從集合的末尾向起始位置迭代。 如果橢圓尚未到達(dá)Canvas面板的底部,代碼會(huì)提高速度(此外,為獲得磁鐵吸引效果,還可以根據(jù)橢圓與Canvas面板底部的距離來(lái)設(shè)置速度): 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; } 最后,如果所有橢圓都已從集合中刪除,就移除事件處理程序,然后結(jié)束動(dòng)畫: 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標(biāo)記如下所示:
<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; } } } 顯然,可擴(kuò)展的這個(gè)動(dòng)畫以使圓跳躍和分散等。使用的技術(shù)是相同的——只需要使用更復(fù)雜的公式計(jì)算速度。 當(dāng)構(gòu)建基于幀的動(dòng)畫時(shí)需要注意如下問(wèn)題:它們不依賴與時(shí)間。換句話說(shuō),動(dòng)畫可能在性能好的計(jì)算機(jī)上運(yùn)動(dòng)更快,因?yàn)閹蕰?huì)增加,會(huì)更頻繁地調(diào)用CompositionTarget.Rendering事件。為補(bǔ)償這種效果,需要編寫考慮當(dāng)前時(shí)間的代碼。 開(kāi)始學(xué)習(xí)基于幀的動(dòng)畫的最好方式是查看WPF SDK提供的每一幀動(dòng)畫都非常詳細(xì)的示例。該例演示了幾種粒子系統(tǒng)效果,并且使用自定義的TimeTracker類實(shí)現(xiàn)了依賴與時(shí)間的基于幀的動(dòng)畫。 |
|
|