在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的附加属性。