在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 } }
運行結果如下: