WPF/WP/Silverlight/Metro App代码创建动画的思路

在2010年之前,我都是用Blend创建动画,添加触发器实现自动动画,后来写成代码创建的方式。如今Blend已经集成到Visual Studio安装镜像中了,最新的VS2015安装,Blend的操作界面已经十分接近VS,难怪有人吐槽Win10 Insider Preview(10025之前版本)的图标设计都是程序员搞出来的——这靠近VS的界面是怎么回事,不是应该更接近于Photoshop、Flash什么的吗?

如果你们公司没有大牛设计师,估计是不太可能使用Blend的。

这是一个点击Show或者Hide按钮使页面中蓝色Grid淡出或者淡入显示的例子。创建两个Storyboard,在2秒位置分别设置蓝色Grid的Opacity为100%和0%。然后在按钮的Click事件处理代码中找到Storyboard资源并执行。

        private void Show_Click(object sender, RoutedEventArgs e)
        {
            ((Storyboard)(this.Resources["ShowGrid"])).Begin();
        }

        private void Hide_Click(object sender, RoutedEventArgs e)
        {
            ((Storyboard)(this.Resources["HideGrid"])).Begin();
        }

作为码农的封装癖好,为了更方便的调用代码动画,我写了UIElement的扩展方法,来扩充界面元素的动画调用。

grid.PlayFadeMoveAnimation(TimeSpan.FromMilliseconds(800), destOpacity: 1, fromX: 50, destX: 0,easingFunctionForMove: new CircleEase())
    .Completed += (ss, se) =>{/*动画播放完成*/};

PlayFadeMoveAnimation扩展方法就是播放淡入淡出并且做移动动画的效果。当时受到了JQuery的“连续方法调用”的影响,这个方法会返回个Storyboard。

上面代码的解释是grid立即播放动画效果:在800ms里,使grid的Opacity变成1(淡入效果),并且水平坐标从50移动到0(右侧50距离开始移动到原始位置),移动时候使用默认的CircleEase函数效果。

例子

1.闪烁

grid.PlayTwinklingAnimation(new[] { new TimeSpan(0, 0, 1), new TimeSpan(0, 0, 1) }, new[] { 0.1, 1.0 }, RepeatBehavior.Forever);

2.淡入淡出

grid.PlayFadeAnimation(TimeSpan.FromMilliseconds(500), 1);

grid.PlayFadeAnimation(TimeSpan.FromMilliseconds(500), 0);

3.淡入淡出平移

grid.PlayFadeMoveAnimation(TimeSpan.FromMilliseconds(800), destOpacity: 1, fromX: 50, destX: 0, easingFunctionForMove: new CircleEase());

grid.PlayFadeMoveAnimation(TimeSpan.FromMilliseconds(800), destOpacity: 0, fromX: 0, destX: 50);

4.平移缩放

grid.PlayMoveAnimation(TimeSpan.FromMilliseconds(500), destX: 0, destY: 0, scaleX: 1, scaleY: 1);

grid.PlayMoveAnimation(TimeSpan.FromMilliseconds(500), destX: 500, destY: 500, scaleX: 0.2, scaleY: 0.2);

下面是实现代码:

  1 using System;
  2 using Windows.UI.Xaml;
  3 using Windows.UI.Xaml.Media;
  4 using Windows.UI.Xaml.Media.Animation;
  5 
  6 public static class AnimationHelper
  7 {
  8     #region Animation
  9 
 10     private const double DoubleEpsilon = 0.001;
 11 
 12     /// <summary>
 13     /// 播放闪烁动画
 14     /// </summary>
 15     /// <param name="uiElement">作用UI元素</param>
 16     /// <param name="timeSpanValues">播放时间</param>
 17     /// <param name="opacityValues">透明类清单</param>
 18     /// <param name="repeatBehavior">重复次数</param>
 19     /// <param name="autoReserse">是否翻转播放</param>
 20     /// <returns>当成功播放动画时会返回一个Storyboard对象</returns>
 21     public static Storyboard PlayTwinklingAnimation(
 22         this UIElement uiElement,
 23         TimeSpan[] timeSpanValues,
 24         double[] opacityValues,
 25         RepeatBehavior repeatBehavior,
 26         bool autoReserse = false)
 27     {
 28         if (timeSpanValues == null || opacityValues == null
 29             || timeSpanValues.Length == 0
 30             || timeSpanValues.Length != opacityValues.Length)
 31         {
 32             return null;
 33         }
 34 
 35         var animation = new DoubleAnimationUsingKeyFrames();
 36         for (int i = 0; i < timeSpanValues.Length; i++)
 37         {
 38             var keyframe = new SplineDoubleKeyFrame { KeyTime = timeSpanValues[i], Value = opacityValues[i] };
 39             animation.KeyFrames.Add(keyframe);
 40         }
 41 
 42         animation.RepeatBehavior = repeatBehavior;
 43         animation.AutoReverse = autoReserse;
 44 
 45         Storyboard.SetTarget(animation, uiElement);
 46         Storyboard.SetTargetProperty(animation, "Opacity");
 47 
 48         var storyboard = new Storyboard();
 49         storyboard.Children.Add(animation);
 50         storyboard.Begin();
 51 
 52         return storyboard;
 53     }
 54 
 55 
 56     /// <summary>
 57     /// 播放淡入淡出动画
 58     /// </summary>
 59     /// <param name="uiElement">作用UI元素</param>
 60     /// <param name="timeSpan">动画时长</param>
 61     /// <param name="destOpacity">目标的Opacity</param>
 62     /// <param name="changeVisibility">是否允许改变UI元素的Visibility属性,建议“是”</param>
 63     /// <param name="easingFunction">easingFunction</param>
 64     /// <returns>当成功播放动画时会返回一个Storyboard对象</returns>
 65     public static Storyboard PlayFadeAnimation(
 66         this UIElement uiElement,
 67         TimeSpan timeSpan,
 68         double destOpacity,
 69         bool changeVisibility = true,
 70         EasingFunctionBase easingFunction = null)
 71     {
 72         if (changeVisibility)
 73         {
 74             if (uiElement.Visibility == Visibility.Collapsed &&
 75                 destOpacity < DoubleEpsilon)
 76             {
 77                 uiElement.Opacity = 0;
 78                 return null;
 79             }
 80 
 81             if (Math.Abs(uiElement.Opacity - destOpacity) < DoubleEpsilon)
 82             {
 83                 uiElement.Visibility = destOpacity < DoubleEpsilon ? Visibility.Collapsed : Visibility.Visible;
 84                 return null;
 85             }
 86         }
 87         else if (Math.Abs(uiElement.Opacity - destOpacity) < DoubleEpsilon)
 88         {
 89             return null;
 90         }
 91 
 92         if (changeVisibility &&
 93             destOpacity > DoubleEpsilon &&
 94             uiElement.Visibility != Visibility.Visible)
 95         {
 96             uiElement.Opacity = 0;
 97             uiElement.Visibility = Visibility.Visible;
 98         }
 99 
100         var animation = new DoubleAnimation
101         {
102             From = uiElement.Opacity,
103             To = destOpacity,
104             Duration = new Duration(timeSpan)
105         };
106 
107         if (easingFunction != null)
108         {
109             animation.EasingFunction = easingFunction;
110         }
111 
112         Storyboard.SetTarget(animation, uiElement);
113         Storyboard.SetTargetProperty(animation, "Opacity");
114 
115         animation.Completed += (sender, e) =>
116         {
117             uiElement.Opacity = destOpacity;
118 
119             if (changeVisibility)
120             {
121                 if (destOpacity < DoubleEpsilon)
122                 {
123                     uiElement.Visibility = Visibility.Collapsed;
124                 }
125             }
126         };
127 
128         var storyboard = new Storyboard();
129         storyboard.Children.Add(animation);
130         storyboard.FillBehavior = FillBehavior.HoldEnd;
131         storyboard.Begin();
132 
133         return storyboard;
134     }
135 
136 
137     /// <summary>
138     /// 播放移动动画(注:参数destX、destY、scaleX和scaleY至少指定一个)
139     /// </summary>
140     /// <param name="frameworkElement">作用UI元素</param>
141     /// <param name="timeSpan">动画时长</param>
142     /// <param name="destX">目标的坐标X</param>
143     /// <param name="destY">目标的坐标Y</param>
144     /// <param name="scaleX">目标的缩放X</param>
145     /// <param name="scaleY">目标的缩放Y</param>
146     /// <param name="centerX">目标的缩放中心点X</param>
147     /// <param name="centerY">目标的缩放中心点Y</param>
148     /// <param name="easingFunction">EasingFunction</param>
149     /// <returns>当成功播放动画时会返回一个Storyboard对象</returns>
150     public static Storyboard PlayMoveAnimation(
151         this FrameworkElement frameworkElement,
152         TimeSpan timeSpan,
153         double destX = double.NaN,
154         double destY = double.NaN,
155         double scaleX = double.NaN,
156         double scaleY = double.NaN,
157         double centerX = double.NaN,
158         double centerY = double.NaN,
159         EasingFunctionBase easingFunction = null)
160     {
161         if (double.IsNaN(destX) && double.IsNaN(destY) && double.IsNaN(scaleX) && double.IsNaN(scaleY))
162         {
163             throw new ArgumentException("destX destY scaleX scaleX");
164         }
165 
166         var storyboard = new Storyboard();
167 
168         var translateTransform = frameworkElement.GetTranform<TranslateTransform>();
169         if (!double.IsNaN(destX))
170         {
171             if (Math.Abs(translateTransform.X - destX) > DoubleEpsilon)
172             {
173                 var animation = new DoubleAnimation
174                 {
175                     From = translateTransform.X,
176                     To = destX,
177                     Duration = new Duration(timeSpan),
178                     EasingFunction = easingFunction
179                 };
180 
181                 Storyboard.SetTarget(animation, translateTransform);
182                 Storyboard.SetTargetProperty(animation, "X");
183                 storyboard.Children.Add(animation);
184             }
185         }
186 
187         if (!double.IsNaN(destY))
188         {
189             if (Math.Abs(translateTransform.Y - destY) > DoubleEpsilon)
190             {
191                 var animation = new DoubleAnimation
192                 {
193                     From = translateTransform.Y,
194                     To = destY,
195                     Duration = new Duration(timeSpan),
196                     EasingFunction = easingFunction
197                 };
198 
199                 Storyboard.SetTarget(animation, translateTransform);
200                 Storyboard.SetTargetProperty(animation, "Y");
201                 storyboard.Children.Add(animation);
202             }
203         }
204 
205         var scaleTransform = frameworkElement.GetTranform<ScaleTransform>();
206         if (!double.IsNaN(centerX)) scaleTransform.CenterX = centerX;
207         if (!double.IsNaN(centerY)) scaleTransform.CenterX = centerY;
208         if (!double.IsNaN(scaleX))
209         {
210             if (Math.Abs(scaleTransform.ScaleX - scaleX) > DoubleEpsilon)
211             {
212                 var animation = new DoubleAnimation
213                 {
214                     From = scaleTransform.ScaleX,
215                     To = scaleX,
216                     Duration = new Duration(timeSpan),
217                     EasingFunction = easingFunction
218                 };
219 
220                 Storyboard.SetTarget(animation, scaleTransform);
221                 Storyboard.SetTargetProperty(animation, "ScaleX");
222                 storyboard.Children.Add(animation);
223             }
224         }
225 
226         if (!double.IsNaN(scaleY))
227         {
228             if (Math.Abs(scaleTransform.ScaleY - scaleY) > DoubleEpsilon)
229             {
230                 var animation = new DoubleAnimation
231                 {
232                     From = scaleTransform.ScaleY,
233                     To = scaleY,
234                     Duration = new Duration(timeSpan),
235                     EasingFunction = easingFunction
236                 };
237 
238                 Storyboard.SetTarget(animation, scaleTransform);
239                 Storyboard.SetTargetProperty(animation, "ScaleY");
240                 storyboard.Children.Add(animation);
241             }
242         }
243 
244         if (storyboard.Children.Count > 0)
245         {
246             storyboard.Begin();
247             return storyboard;
248         }
249 
250         return null;
251     }
252 
253     /// <summary>
254     /// 播放一个包含透明度和移动变化的动画
255     /// </summary>
256     /// <param name="uiElement">作用UI元素</param>
257     /// <param name="timeSpan">动画时长</param>
258     /// <param name="destOpacity">目标Opacity</param>
259     /// <param name="changeVisibility">是否允许改变UI元素的Visibility属性,建议“是”</param>
260     /// <param name="fromX">起始坐标X</param>
261     /// <param name="fromY">起始坐标Y</param>
262     /// <param name="destX">目标的坐标X</param>
263     /// <param name="destY">目标的坐标Y</param>
264     /// <param name="easingFunctionForFade">EasingFunction</param>
265     /// <param name="easingFunctionForMove">EasingFunction</param>
266     /// <returns>当成功播放动画时会返回一个Storyboard对象</returns>
267     public static Storyboard PlayFadeMoveAnimation(
268         this UIElement uiElement,
269         TimeSpan timeSpan,
270         double destOpacity,
271         bool changeVisibility = true,
272         double fromX = double.NaN,
273         double fromY = double.NaN,
274         double destX = double.NaN,
275         double destY = double.NaN,
276         EasingFunctionBase easingFunctionForFade = null,
277         EasingFunctionBase easingFunctionForMove = null
278         )
279     {
280         var storyboard = new Storyboard { FillBehavior = FillBehavior.HoldEnd };
281 
282         if (changeVisibility &&
283             destOpacity > DoubleEpsilon &&
284             uiElement.Visibility != Visibility.Visible)
285         {
286             uiElement.Opacity = 0;
287             uiElement.Visibility = Visibility.Visible;
288         }
289         var fadeAnimation = new DoubleAnimation
290         {
291             From = uiElement.Opacity,
292             To = destOpacity,
293             Duration = new Duration(timeSpan)
294         };
295         if (easingFunctionForFade != null)
296         {
297             fadeAnimation.EasingFunction = easingFunctionForFade;
298         }
299         Storyboard.SetTarget(fadeAnimation, uiElement);
300         Storyboard.SetTargetProperty(fadeAnimation, "Opacity");
301         fadeAnimation.Completed += (sender, e) =>
302         {
303             uiElement.Opacity = destOpacity;
304 
305             if (changeVisibility)
306             {
307                 if (destOpacity < DoubleEpsilon)
308                 {
309                     uiElement.Visibility = Visibility.Collapsed;
310                 }
311             }
312         };
313         storyboard.Children.Add(fadeAnimation);
314 
315         if (!double.IsNaN(destX) || !double.IsNaN(destY))
316         {
317             var translateTransform = uiElement.GetTranform<TranslateTransform>();
318             if (!double.IsNaN(destX))
319             {
320                 var x = double.IsNaN(fromX) ? translateTransform.X : fromX;
321                 if (Math.Abs(x - destX) > DoubleEpsilon)
322                 {
323                     var animation = new DoubleAnimation
324                     {
325                         From = x,
326                         To = destX,
327                         Duration = new Duration(timeSpan),
328                         EasingFunction = easingFunctionForMove
329                     };
330 
331                     Storyboard.SetTarget(animation, translateTransform);
332                     Storyboard.SetTargetProperty(animation, "X");
333                     storyboard.Children.Add(animation);
334                 }
335             }
336             if (!double.IsNaN(destY))
337             {
338                 var y = double.IsNaN(fromY) ? translateTransform.X : fromY;
339                 if (Math.Abs(y - destY) > DoubleEpsilon)
340                 {
341                     var animation = new DoubleAnimation
342                     {
343                         From = y,
344                         To = destY,
345                         Duration = new Duration(timeSpan),
346                         EasingFunction = easingFunctionForMove
347                     };
348 
349                     Storyboard.SetTarget(animation, translateTransform);
350                     Storyboard.SetTargetProperty(animation, "Y");
351                     storyboard.Children.Add(animation);
352                 }
353             }
354         }
355 
356         if (storyboard.Children.Count > 0)
357         {
358             storyboard.Begin();
359             return storyboard;
360         }
361 
362         return null;
363     }
364 
365     #endregion
366 
367     #region Transform helper
368 
369     /// <summary>
370     /// 获得或创建一个新的Transform对象
371     /// </summary>
372     /// <typeparam name="T">指定一个Transform类型</typeparam>
373     /// <param name="uiElement">UI元素</param>
374     /// <returns>一个Transform对象</returns>
375     public static T GetTranform<T>(this UIElement uiElement)
376         where T : Transform, new()
377     {
378         if (uiElement.RenderTransform == null)
379         {
380             var newTransformGroup = new TransformGroup();
381             var newTransfrom = new T();
382             newTransformGroup.Children.Add(newTransfrom);
383 
384             uiElement.RenderTransform = newTransformGroup;
385             return newTransfrom;
386         }
387 
388         var transformGroup = uiElement.RenderTransform as TransformGroup;
389         if (transformGroup != null)
390         {
391             if (transformGroup is T)
392             {
393                 return transformGroup as T;
394             }
395 
396             var r = GetTranform<T>(transformGroup);
397             if (r != null)
398             {
399                 return r;
400             }
401 
402             var newTransfrom = new T();
403             transformGroup.Children.Add(newTransfrom);
404             return newTransfrom;
405         }
406 
407         var transform = uiElement.RenderTransform as T;
408         if (transform != null)
409         {
410             return transform;
411         }
412 
413         var newTransformGroup1 = new TransformGroup();
414         var newTransfrom1 = new T();
415 
416         //如果原来不是MatrixTransform矩阵,则加入
417         var matrixTransform = uiElement.RenderTransform as MatrixTransform;
418         if (matrixTransform == null)
419         {
420             newTransformGroup1.Children.Add(uiElement.RenderTransform);
421         }
422 
423         newTransformGroup1.Children.Add(newTransfrom1);
424         uiElement.RenderTransform = newTransformGroup1;
425         return newTransfrom1;
426     }
427 
428     private static T GetTranform<T>(TransformGroup transformGroup)
429         where T : Transform, new()
430     {
431         foreach (var child in transformGroup.Children)
432         {
433             if (child is T)
434             {
435                 return (T)child;
436             }
437 
438             var group1 = child as TransformGroup;
439             if (group1 != null)
440             {
441                 var r = GetTranform<T>(group1);
442                 if (r != null)
443                 {
444                     return r;
445                 }
446             }
447         }
448 
449         return null;
450     }
451 
452     #endregion
453 }

需要特别说一下的是:在Win10 UAP开发中,如Grid这中的界面元素都默认使用了2D的仿射矩阵变换动画,通过修改3*3的矩阵数值就能够表达平移、旋转、缩放。2010年在某家公司做一个WPF地图平面功能时就是直接使用的Matrix实现。因此MatrixTransform不能和其他Transform一起使用。

解释一下375行的GetTranform<T>方法,它返回或为uiElement创建指定的Transform。当uiElement的RenderTransform属性为空时候,创建TransformGroup,然后将指定的Transform添加到TransformGroup里,使UIElement的RenderTransform为TransformGroup。

在动画代码里,平移使用TranslateTransform效果,透明度是修改Opacity属性,Opacity为0或者为1时候修改Visibility属性。当然可以在调用PlayXXXAnimation方法时候设置changeVisibility为false来达到不修改Visibility的目的。

EasingFunctionBase是缓动动画效果,就是Bland里不同的动画曲线函数。

最后可根据情况丰富自已的动画库。比如模仿按钮被按下的动画效果,然后写成bool类型的FramewrokElement的附加属性。

原文地址:https://www.cnblogs.com/Bob-wei/p/4535210.html