wpf动画——缓动动画Animation Easing(2)
在wpf或者silverlight中,经常用到Storyboard来完成一些动画的效果,本例将说明使用缓动函数关联动画 Animation Easing的方法:
1.新建一个wpf应用程序(silverlight亦可),xaml简单修改布局如下:
<Window x:Class="WpfApplication51.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid Name="root" Loaded="root_Loaded"> </Grid> </Window>
对应我们添加了一个Grid的Loaded事件处理:
private void root_Loaded(object sender, RoutedEventArgs e) { }
然后我们准备一个故事布景canvas,一个圆elipse,几条准线line,用来描述一个缓动动画过程:
Canvas canvas = new Canvas() { Height = 300, Width = 400, Background = new SolidColorBrush(Colors.Silver), }; Ellipse e1 = new Ellipse() { Height = 20, Width = 20, Fill = new SolidColorBrush(Colors.Green), RenderTransform = new TranslateTransform(-10, -10) }; Line l1 = new Line() { Stroke = new SolidColorBrush(Colors.Gray), X1 = 0, Y1 = 100, X2 = 400, Y2 = 100 }; Line l2 = new Line() { Stroke = new SolidColorBrush(Colors.Gray), X1 = 0, Y1 = 200, X2 = 400, Y2 = 200 }; private void root_Loaded(object sender, RoutedEventArgs e) { this.root.Children.Add(this.canvas); this.canvas.Children.Add(this.l1); this.canvas.Children.Add(this.l2); this.canvas.Children.Add(this.e1); this.e1.SetValue(Canvas.TopProperty, 200.0); this.e1.SetValue(Canvas.LeftProperty, 50.0); }
这里我们让动画的主角-圆,Transform变换了一下,纵横坐标都偏移了一个半径距离,目的是让圆的圆心位置在原点位置,
F5运行一下,得到一个开幕:
2.接下来我们让这个圆动起来,
首先说明一下,一个动画故事StoryBoard可以由很多的关键帧集合DoubleAnimationUsingKeyFrames组成,
每个DoubleAnimationUsingKeyFrames关键帧集合是由很多KeyFrame帧组成,
每个KeyFrame帧记录了时间点keytime,目标值 value,以及是否会用到的EasingFunction缓动处理函数,
“工欲善其事,必先利其器”,so,先写两个方法用来添加丰富我们的动画吧:
/// <summary> /// story故事版增加key关键帧集 /// </summary> /// <param name="sb">story故事版</param> /// <param name="ks">key关键帧集</param> /// <param name="dobj">动画目标</param> /// <param name="property">动画属性</param> void StoryAddKey(Storyboard sb, DoubleAnimationUsingKeyFrames ks, DependencyObject dobj, PropertyPath property) { sb.Children.Add(ks); Storyboard.SetTarget(ks, dobj); Storyboard.SetTargetProperty(ks, property); } /// <summary> /// key关键帧集增加帧frame /// </summary> /// <param name="kfs">关键帧集</param> /// <param name="ms">时间点</param> /// <param name="value">数值</param> /// <param name="efun">缓动处理</param> void KeyAddFrame(DoubleAnimationUsingKeyFrames kfs, double ms, double value, EasingFunctionBase efun) { EasingDoubleKeyFrame kf = new EasingDoubleKeyFrame(); kf.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(ms)); kf.Value = value; kf.EasingFunction = efun; kfs.KeyFrames.Add(kf); } /// <summary> /// property属性链 /// </summary> DependencyProperty[] propertyChain = new DependencyProperty[] { Canvas.TopProperty, Canvas.LeftProperty };
这里的属性链,在处理动画目标属性的时候,可以很方便的选择要处理的属性,
然后用我们建的方法去添加一个动画吧,
动画的过程是让圆在一个二维坐标系中移动:
y纵轴方向:让圆从下面的准线移动到上面的准线;
x横轴方向:让圆水平移动;
Storyboard sb = new Storyboard()
{
FillBehavior = FillBehavior.HoldEnd
};
private void root_Loaded(object sender, RoutedEventArgs e) { this.root.Children.Add(this.canvas); this.canvas.Children.Add(this.l1); this.canvas.Children.Add(this.l2); this.canvas.Children.Add(this.e1); this.e1.SetValue(Canvas.TopProperty, 200.0); this.e1.SetValue(Canvas.LeftProperty, 50.0); //纵坐标动画 DoubleAnimationUsingKeyFrames ks1e1 = new DoubleAnimationUsingKeyFrames(); this.KeyAddFrame(ks1e1, 0.0, 200.0, null); this.KeyAddFrame(ks1e1, 2.0, 100.0, new BackEase() { EasingMode = EasingMode.EaseOut, Amplitude = 1.0 }); this.StoryAddKey(sb, ks1e1, this.e1, new PropertyPath("(0)", this.propertyChain)); //横坐标动画 DoubleAnimationUsingKeyFrames ks2e1 = new DoubleAnimationUsingKeyFrames(); this.KeyAddFrame(ks2e1, 0.0, 50.0, null); this.KeyAddFrame(ks2e1, 2.0, 350.0, null); this.StoryAddKey(sb, ks2e1, this.e1, new PropertyPath("(1)", this.propertyChain)); sb.Begin(); }
这里我们使用了缓动动画中的一种BackEase,设置模式为EaseOut(动画末尾处理),当然你也可以去show其他几种模式:
EaseIn:动画初时处理;
EaseInOut:动画初时及末尾均处理;
这里还设置了Amplitude,其默认值就为1.0,代表缓动动画的幅度,
F5运行一下,发现圆已经按照我们预想的那样去移动,
接下来我们试着把圆移动的轨迹绘制出来,更加直观的观察缓动的处理过程。
3.绘制的方法,添加的代码如下:
DrawingVisual drawingVisual = new DrawingVisual(); DrawingContext drawingContext; Point pe1 = new Point(50.0, 200.0); Image image = new Image() { Width = 400, Height = 300 }; private void root_Loaded(object sender, RoutedEventArgs e) { this.root.Children.Add(this.canvas); this.canvas.Children.Add(this.l1); this.canvas.Children.Add(this.l2); this.canvas.Children.Add(this.e1); this.e1.SetValue(Canvas.TopProperty, 200.0); this.e1.SetValue(Canvas.LeftProperty, 50.0); //纵坐标动画 DoubleAnimationUsingKeyFrames ks1e1 = new DoubleAnimationUsingKeyFrames(); this.KeyAddFrame(ks1e1, 0.0, 200.0, null); this.KeyAddFrame(ks1e1, 2.0, 100.0, new BackEase() { EasingMode = EasingMode.EaseOut, Amplitude = 1.0 }); this.StoryAddKey(sb, ks1e1, this.e1, new PropertyPath("(0)", this.propertyChain)); //横坐标动画 DoubleAnimationUsingKeyFrames ks2e1 = new DoubleAnimationUsingKeyFrames(); this.KeyAddFrame(ks2e1, 0.0, 50.0, null); this.KeyAddFrame(ks2e1, 2.0, 350.0, null); this.StoryAddKey(sb, ks2e1, this.e1, new PropertyPath("(1)", this.propertyChain)); this.canvas.LayoutUpdated += new EventHandler(canvas_LayoutUpdated); drawingContext = drawingVisual.RenderOpen(); sb.Completed += new EventHandler(sb_Completed); sb.Begin(); } void sb_Completed(object sender, EventArgs e) { drawingContext.Close(); RenderTargetBitmap composeImage = new RenderTargetBitmap(400, 300, 0, 0, PixelFormats.Pbgra32); composeImage.Render(drawingVisual); this.image.Source = composeImage; this.canvas.LayoutUpdated -= canvas_LayoutUpdated; this.canvas.Children.Add(image); } void canvas_LayoutUpdated(object sender, EventArgs e) { Point p1 = new Point(Convert.ToDouble(this.e1.GetValue(Canvas.LeftProperty)), Convert.ToDouble(this.e1.GetValue(Canvas.TopProperty))); drawingContext.DrawLine(new Pen(new SolidColorBrush(Colors.Green), 2.0), pe1, p1); pe1 = p1; }
这里用到了canvas的内部元素布局改变的LayoutUpdated事件,每当圆移动改变自己坐标的时候,就记录下坐标,移动到下一个坐标时,连接两个坐标的连线即移动的路径,
这里的绘制使用drawingContext.DrawLine绘制路径线段,动画结束时将绘制得到的DrawingVisual结果放到一个与canvas舞台大小相同的image中用以呈现,
F5运行一下,结果如下图:
4.试着修改缓冲动画的种类,以及幅度Amplitude的值,亦可多show几个圆同时移动,来观察他们的区别,以下给出完整的参考代码(两个圆的轨迹比较):
/// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } #region Canvas canvas = new Canvas() { Height = 300, Width = 400, Background = new SolidColorBrush(Colors.Silver), }; Ellipse e1 = new Ellipse() { Height = 20, Width = 20, Fill = new SolidColorBrush(Colors.Green), RenderTransform = new TranslateTransform(-10, -10) }; Ellipse e2 = new Ellipse() { Height = 20, Width = 20, Fill = new SolidColorBrush(Colors.Yellow), RenderTransform = new TranslateTransform(-10, -10) }; Line l1 = new Line() { Stroke = new SolidColorBrush(Colors.Gray), X1 = 0, Y1 = 100, X2 = 400, Y2 = 100 }; Line l2 = new Line() { Stroke = new SolidColorBrush(Colors.Gray), X1 = 0, Y1 = 200, X2 = 400, Y2 = 200 }; Storyboard sb = new Storyboard() { FillBehavior = FillBehavior.HoldEnd }; DrawingVisual drawingVisual = new DrawingVisual(); DrawingContext drawingContext; Point pe1 = new Point(50.0, 200.0); Point pe2 = new Point(50.0, 200.0); Image image = new Image() { Width = 400, Height = 300 }; #endregion #region /// <summary> /// story故事版增加key关键帧集 /// </summary> /// <param name="sb">story故事版</param> /// <param name="ks">key关键帧集</param> /// <param name="dobj">动画目标</param> /// <param name="property">动画属性</param> void StoryAddKey(Storyboard sb, DoubleAnimationUsingKeyFrames ks, DependencyObject dobj, PropertyPath property) { sb.Children.Add(ks); Storyboard.SetTarget(ks, dobj); Storyboard.SetTargetProperty(ks, property); } /// <summary> /// key关键帧集增加帧frame /// </summary> /// <param name="kfs">关键帧集</param> /// <param name="ms">时间点</param> /// <param name="value">数值</param> /// <param name="efun">缓动处理</param> void KeyAddFrame(DoubleAnimationUsingKeyFrames kfs, double ms, double value, EasingFunctionBase efun) { EasingDoubleKeyFrame kf = new EasingDoubleKeyFrame(); kf.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(ms)); kf.Value = value; kf.EasingFunction = efun; kfs.KeyFrames.Add(kf); } /// <summary> /// property属性链 /// </summary> DependencyProperty[] propertyChain = new DependencyProperty[] { Canvas.TopProperty, Canvas.LeftProperty }; #endregion private void root_Loaded(object sender, RoutedEventArgs e) { this.root.Children.Add(this.canvas); this.canvas.Children.Add(this.l1); this.canvas.Children.Add(this.l2); this.canvas.Children.Add(this.e1); this.e1.SetValue(Canvas.TopProperty, 200.0); this.e1.SetValue(Canvas.LeftProperty, 50.0); this.canvas.Children.Add(this.e2); this.e2.SetValue(Canvas.TopProperty, 200.0); this.e2.SetValue(Canvas.LeftProperty, 50.0); #region e1 //纵坐标动画 DoubleAnimationUsingKeyFrames ks1e1 = new DoubleAnimationUsingKeyFrames(); this.KeyAddFrame(ks1e1, 0.0, 200.0, null); this.KeyAddFrame(ks1e1, 2.0, 100.0, new BackEase() { EasingMode = EasingMode.EaseOut, Amplitude = 1.0 }); this.StoryAddKey(sb, ks1e1, this.e1, new PropertyPath("(0)", this.propertyChain)); //横坐标动画 DoubleAnimationUsingKeyFrames ks2e1 = new DoubleAnimationUsingKeyFrames(); this.KeyAddFrame(ks2e1, 0.0, 50.0, null); this.KeyAddFrame(ks2e1, 2.0, 350.0, null); this.StoryAddKey(sb, ks2e1, this.e1, new PropertyPath("(1)", this.propertyChain)); #endregion #region e2 //纵坐标动画 DoubleAnimationUsingKeyFrames ks1e2 = new DoubleAnimationUsingKeyFrames(); this.KeyAddFrame(ks1e2, 0.0, 200.0, null); this.KeyAddFrame(ks1e2, 2.0, 100.0, null); this.StoryAddKey(sb, ks1e2, this.e2, new PropertyPath("(0)", this.propertyChain)); //横坐标动画 DoubleAnimationUsingKeyFrames ks2e2 = new DoubleAnimationUsingKeyFrames(); this.KeyAddFrame(ks2e2, 0.0, 50.0, null); this.KeyAddFrame(ks2e2, 2.0, 350.0, null); this.StoryAddKey(sb, ks2e2, this.e2, new PropertyPath("(1)", this.propertyChain)); #endregion this.canvas.LayoutUpdated += new EventHandler(canvas_LayoutUpdated); drawingContext = drawingVisual.RenderOpen(); sb.Completed += new EventHandler(sb_Completed); sb.Begin(); } void sb_Completed(object sender, EventArgs e) { drawingContext.Close(); RenderTargetBitmap composeImage = new RenderTargetBitmap(400, 300, 0, 0, PixelFormats.Pbgra32); composeImage.Render(drawingVisual); this.image.Source = composeImage; this.canvas.LayoutUpdated -= canvas_LayoutUpdated; this.canvas.Children.Add(image); } void canvas_LayoutUpdated(object sender, EventArgs e) { #region e1 Point p1 = new Point(Convert.ToDouble(this.e1.GetValue(Canvas.LeftProperty)), Convert.ToDouble(this.e1.GetValue(Canvas.TopProperty))); drawingContext.DrawLine(new Pen(new SolidColorBrush(Colors.Green), 2.0), pe1, p1); pe1 = p1; #endregion #region e2 Point p2 = new Point(Convert.ToDouble(this.e2.GetValue(Canvas.LeftProperty)), Convert.ToDouble(this.e2.GetValue(Canvas.TopProperty))); drawingContext.DrawLine(new Pen(new SolidColorBrush(Colors.Yellow), 2.0), pe2, p2); pe2 = p2; #endregion } }
运行结果如下: