【WPF學習】第五十六章 基於幀的動畫

  除基於屬性的動畫系統外,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>
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類實現了依賴與時間的基於幀的動畫。

相關文章
相關標籤/搜索