【WPF学习】第五十六章 基于帧的动画

摘要:
除了基于属性的动画系统之外,WPF还提供了一种仅使用代码创建基于帧的动画的方法。构建基于帧的动画的基本技术很简单。关联事件处理程序后,WPF开始连续调用事件处理程序。动画结束后,分离事件处理程序。在这里,从“画布”面板的顶部到底部会出现随机数量的圆圈。当所有圆到达底部时,动画结束。在本例中,每个下降的圆由椭圆元素表示。行驶距离取决于指定速度。

  除基于属性的动画系统外,WPF提供了一种创建基于帧的动画的方法,这种方法只使用代码。需要做的全部工作是响应静态的CompositionTarge.Rendering事件,触发该事件是为了给每帧获取内容。这是一种非常低级的方法,除非使用标准的基于属性的动画模型不能满足需要(例如,构建简单的侧边滚动游戏、创建基于物理的动画式构建粒子效果模型(如火焰、雪花以及气泡)),否则不会希望使用这种方法。

  构建基于帧的动画的基本技术很容易。只需要为静态的CompositionTarget.Rendering事件关联事件处理程序。一旦关联事件处理程序,WPF就开始不断地调用这个事件处理程序(只要渲染代码的执行速度足够快,WPF每秒将调用60次)。在渲染事件处理程序中,需要在窗口中相应地创建或调整元素。换句话说,需要自行管理全部工作。当动画结束时,分离事件处理程序。

  下图显示了一个简单示例。在此,随机数量的圆从Canvas面板的顶部向底部下落。它们(根据随机生成的开始速度)以不同速度下降,但一相同的速率加速。当所有的圆到达底部时,动画结束。

【WPF学习】第五十六章 基于帧的动画第1张

  在这个示例中,每个下落的圆由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标记如下所示:

【WPF学习】第五十六章 基于帧的动画第2张【WPF学习】第五十六章 基于帧的动画第3张
<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
【WPF学习】第五十六章 基于帧的动画第2张【WPF学习】第五十六章 基于帧的动画第3张
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类实现了依赖与时间的基于帧的动画。

免责声明:文章转载自《【WPF学习】第五十六章 基于帧的动画》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Shell基本命令SpringBoot简单连接数据库以及查询数据下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

WPF 基础学习笔记

学习笔记: WPF中对于控件名字的命名,可在code中找到,例如如下:x:Name=“MassText” ... 如何获取WPF richTextBox的text?有别于winform,比较复杂。 string richText1 = new TextRange(RichTextBox1.Document.ContentStart, RichTextBo...

aos.js超赞页面滚动元素动画jQuery动画库

aos.js超赞页面滚动元素动画jQuery动画库 插件描述:aos.js 是一款效果超赞的页面滚动元素动画jQuery动画库插件。该动画库可以在页面滚动时提供28种不同的元素动画效果,以及多种easing效果。在页面往回滚动时,元素会恢复到原来的状态。 简要教程 aos.js是一款效果超赞的页面滚动元素动画jQuery动画库插件。该动画库可以在页面滚动...

【转】编写高质量代码改善C#程序的157个建议——建议87:区分WPF和WinForm的线程模型

建议87:区分WPF和WinForm的线程模型WPF和WinForm窗体应用程序都有一个要求,那就是UI元素(如Button、TextBox等)必须由创建它的那个线程进行更新。WinForm在这方面的限制并不是很严格,所以像下面这样的代码,在WinForm中大部分情况下还能运行(本建议后面会详细解释为什么会出现这种现象): private void but...

WPF 自定义按钮 Style

<Style TargetType="{x:Type Button}" x:Key="DefaultButton"> <Setter Property="Foreground" Value="White"/> <Setter Property="...

wpf鼠标捕获与控件交互——UIElement.CaptureMouse

应用场景是这样的,我需要拖动一个元素在屏幕上移动,注册了被移动元素的MouseMove事件,但是当鼠标移到被移动元素的外面时,移动失效,且鼠标的手势变成了普通的箭头形状,于是就找到了以下的解决方案。 本例实现了一个鼠标控制控件移动的简单例子,配合鼠标捕获达成预想效果: 1.新建一个wpf应用程序,为了演示效果,xaml简单修改如下:共有两个圆(绿、黄),下...

WPF RadioButton 绑定枚举

定义枚举类型 public enum CoordinateEnum { X=0,Y,Z,RX,RY,RZ } 定义枚举转换Convert public class EnumConvert : IValueConverter { public object Convert(object value,...