需求
在提升应用程序的用户体验上,动画(Animation)是一个不得不说的。WPF中有非常丰富的动画实现资源,主要通过故事板(Storyboard)上进行“表演”。本文通过模仿PPT中的部分动画(滑入滑出和淡入淡出结合)实现登录页、主页、房间页、记录页、设置页之间的切换。
首先上效果:
环境
Windows 10
Visual Studio 2019
.Net Framework 4.7.2
设计
UI设计:
和上篇博客相同。
功能设计:
1.程序进入时使用淡入的动画效果。
2.其他页面的切换分别以不同的方向滑入。
实现
1.自定义给故事板Storyboard添加动画的扩展方法
分别添加淡入、淡出、左边滑入/出、右边滑入/出、上方滑入/出、下方滑入/出
/// <summary>
/// 给 <see cref="Storyboard"/> 添加动画的扩展类
/// </summary>
public static class StoryboardExtensions
{
#region 左边滑入滑出动画 Sliding To/From Left
/// <summary>
/// 添加一个从左边滑入的动画到故事板 <see cref="Storyboard"/> 上
/// </summary>
/// <param name="storyboard">承载动画的故事板</param>
/// <param name="seconds">动画时长</param>
/// <param name="offset">从左边开始的距离</param>
/// <param name="decelerationRatio">减速率</param>
/// <param name="keepMargin">动画期间是否保持元素的宽度相同</param>
public static void AddSlideFromLeft(this Storyboard storyboard, float seconds, double offset, float decelerationRatio = 0.9f, bool keepMargin = true)
{
// 创建一个左侧边缘滑入的动画
var animation = new ThicknessAnimation
{
Duration = new Duration(TimeSpan.FromSeconds(seconds)),
From = new Thickness(-offset, 0, keepMargin ? offset : 0, 0),
To = new Thickness(0),
DecelerationRatio = decelerationRatio
};
// 设置动画目标属性的名称 Margin
Storyboard.SetTargetProperty(animation, new PropertyPath("Margin"));
// 将动画添加到当前故事板上
storyboard.Children.Add(animation);
}
/// <summary>
/// 添加一个从左边滑出的动画到故事板(Storyboard)上
/// </summary>
/// <param name="storyboard">承载动画的故事板</param>
/// <param name="seconds">动画时长</param>
/// <param name="offset">从左边开始的距离</param>
/// <param name="decelerationRatio">减速率</param>
/// <param name="keepMargin">动画期间是否保持元素的宽度相同</param>
public static void AddSlideToLeft(this Storyboard storyboard, float seconds, double offset, float decelerationRatio = 0.9f, bool keepMargin = true)
{
// 创建一个左侧边缘滑出的动画
var animation = new ThicknessAnimation
{
Duration = new Duration(TimeSpan.FromSeconds(seconds)),
From = new Thickness(0),
To = new Thickness(-offset, 0, keepMargin ? offset : 0, 0),
DecelerationRatio = decelerationRatio
};
// 设置动画目标属性的名称 Margin
Storyboard.SetTargetProperty(animation, new PropertyPath("Margin"));
// 将动画添加到当前故事板上
storyboard.Children.Add(animation);
}
#endregion
#region 右边滑入滑出动画 Sliding To/From Right
/// <summary>
/// 添加一个从右边滑入的动画到故事板 <see cref="Storyboard"/> 上
/// </summary>
/// <param name="storyboard">承载动画的故事板</param>
/// <param name="seconds">动画时长</param>
/// <param name="offset">从左边开始的距离</param>
/// <param name="decelerationRatio">减速率</param>
/// <param name="keepMargin">动画期间是否保持元素的宽度相同</param>
public static void AddSlideFromRight(this Storyboard storyboard, float seconds, double offset, float decelerationRatio = 0.9f, bool keepMargin = true)
{
// 创建一个右侧边缘滑入的动画
var animation = new ThicknessAnimation
{
Duration = new Duration(TimeSpan.FromSeconds(seconds)),
From = new Thickness(keepMargin ? offset : 0, 0, -offset, 0),
To = new Thickness(0),
DecelerationRatio = decelerationRatio
};
// 设置动画目标属性的名称 Margin
Storyboard.SetTargetProperty(animation, new PropertyPath("Margin"));
// 将动画添加到当前故事板上
storyboard.Children.Add(animation);
}
/// <summary>
/// 添加一个从右边滑出的动画到故事板 <see cref="Storyboard"/> 上
/// </summary>
/// <param name="storyboard">承载动画的故事板</param>
/// <param name="seconds">动画时长</param>
/// <param name="offset">从左边开始的距离</param>
/// <param name="decelerationRatio">减速率</param>
/// <param name="keepMargin">动画期间是否保持元素的宽度相同</param>
public static void AddSlideToRight(this Storyboard storyboard, float seconds, double offset, float decelerationRatio = 0.9f, bool keepMargin = true)
{
// 创建一个右侧边缘滑出的动画
var animation = new ThicknessAnimation
{
Duration = new Duration(TimeSpan.FromSeconds(seconds)),
From = new Thickness(0),
To = new Thickness(keepMargin ? offset : 0, 0, -offset, 0),
DecelerationRatio = decelerationRatio
};
// 设置动画目标属性的名称 Margin
Storyboard.SetTargetProperty(animation, new PropertyPath("Margin"));
// 将动画添加到当前故事板上
storyboard.Children.Add(animation);
}
#endregion
#region 顶部滑入滑出动画 Sliding To/From Top
/// <summary>
/// 添加一个从顶部滑入的动画到故事板 <see cref="Storyboard"/> 上
/// </summary>
/// <param name="storyboard">承载动画的故事板</param>
/// <param name="seconds">动画时长</param>
/// <param name="offset">从左边开始的距离</param>
/// <param name="decelerationRatio">减速率</param>
/// <param name="keepMargin">动画期间是否保持元素的宽度相同</param>
public static void AddSlideFromTop(this Storyboard storyboard, float seconds, double offset, float decelerationRatio = 0.9f, bool keepMargin = true)
{
// 创建一个顶部边缘滑入的动画
var animation = new ThicknessAnimation
{
Duration = new Duration(TimeSpan.FromSeconds(seconds)),
From = new Thickness(0, -offset, 0, keepMargin ? offset : 0),
To = new Thickness(0),
DecelerationRatio = decelerationRatio
};
// 设置动画目标属性的名称 Margin
Storyboard.SetTargetProperty(animation, new PropertyPath("Margin"));
// 将动画添加到当前故事板上
storyboard.Children.Add(animation);
}
/// <summary>
/// 添加一个从顶部滑出的动画到故事板 <see cref="Storyboard"/> 上
/// </summary>
/// <param name="storyboard">承载动画的故事板</param>
/// <param name="seconds">动画时长</param>
/// <param name="offset">从左边开始的距离</param>
/// <param name="decelerationRatio">减速率</param>
/// <param name="keepMargin">动画期间是否保持元素的宽度相同</param>
public static void AddSlideToTop(this Storyboard storyboard, float seconds, double offset, float decelerationRatio = 0.9f, bool keepMargin = true)
{
// 创建一个顶部边缘滑出的动画
var animation = new ThicknessAnimation
{
Duration = new Duration(TimeSpan.FromSeconds(seconds)),
From = new Thickness(0),
To = new Thickness(0, -offset, 0, keepMargin ? offset : 0),
DecelerationRatio = decelerationRatio
};
// 设置动画目标属性的名称 Margin
Storyboard.SetTargetProperty(animation, new PropertyPath("Margin"));
// 将动画添加到当前故事板上
storyboard.Children.Add(animation);
}
#endregion
#region 底部滑入滑出动画 Sliding To/From Bottom
/// <summary>
/// 添加一个从底部滑入的动画到故事板 <see cref="Storyboard"/> 上
/// </summary>
/// <param name="storyboard">承载动画的故事板</param>
/// <param name="seconds">动画时长</param>
/// <param name="offset">从左边开始的距离</param>
/// <param name="decelerationRatio">减速率</param>
/// <param name="keepMargin">动画期间是否保持元素的宽度相同</param>
public static void AddSlideFromBottom(this Storyboard storyboard, float seconds, double offset, float decelerationRatio = 0.9f, bool keepMargin = true)
{
// 创建一个顶部边缘滑入的动画
var animation = new ThicknessAnimation
{
Duration = new Duration(TimeSpan.FromSeconds(seconds)),
From = new Thickness(0, keepMargin ? offset : 0, 0, -offset),
To = new Thickness(0),
DecelerationRatio = decelerationRatio
};
// 设置动画目标属性的名称 Margin
Storyboard.SetTargetProperty(animation, new PropertyPath("Margin"));
// 将动画添加到当前故事板上
storyboard.Children.Add(animation);
}
/// <summary>
/// 添加一个从底部滑出的动画到故事板 <see cref="Storyboard"/> 上
/// </summary>
/// <param name="storyboard">承载动画的故事板</param>
/// <param name="seconds">动画时长</param>
/// <param name="offset">从左边开始的距离</param>
/// <param name="decelerationRatio">减速率</param>
/// <param name="keepMargin">动画期间是否保持元素的宽度相同</param>
public static void AddSlideToBottom(this Storyboard storyboard, float seconds, double offset, float decelerationRatio = 0.9f, bool keepMargin = true)
{
// 创建一个底部边缘滑出的动画
var animation = new ThicknessAnimation
{
Duration = new Duration(TimeSpan.FromSeconds(seconds)),
From = new Thickness(0),
To = new Thickness(0, keepMargin ? offset : 0, 0, -offset),
DecelerationRatio = decelerationRatio
};
// 设置动画目标属性的名称 Margin
Storyboard.SetTargetProperty(animation, new PropertyPath("Margin"));
// 将动画添加到当前故事板上
storyboard.Children.Add(animation);
}
#endregion
#region 淡入淡出效果 Fade In/Out
/// <summary>
/// 添加一个淡入的动画到故事板 <see cref="Storyboard"/> 上
/// </summary>
/// <param name="storyboard">承载动画的故事板</param>
/// <param name="seconds">动画时长</param>
public static void AddFadeIn(this Storyboard storyboard, float seconds)
{
// 创建一个渐变出现的动画
var animation = new DoubleAnimation
{
Duration = new Duration(TimeSpan.FromSeconds(seconds)),
From = 0,
To = 1,
};
// 设置动画目标属性的名称 Opacity
Storyboard.SetTargetProperty(animation, new PropertyPath("Opacity"));
// 将动画添加到当前故事板上
storyboard.Children.Add(animation);
}
/// <summary>
/// 添加一个淡出的动画到故事板 <see cref="Storyboard"/> 上
/// </summary>
/// <param name="storyboard">承载动画的故事板</param>
/// <param name="seconds">动画时长</param>
public static void AddFadeOut(this Storyboard storyboard, float seconds)
{
// 创建一个渐变消失的动画
var animation = new DoubleAnimation
{
Duration = new Duration(TimeSpan.FromSeconds(seconds)),
From = 1,
To = 0,
};
// 设置动画目标属性的名称 Opacity
Storyboard.SetTargetProperty(animation, new PropertyPath("Opacity"));
// 将动画添加到当前故事板上
storyboard.Children.Add(animation);
}
#endregion
}
2.自定义给继承自FrameworkElement的所有元素添加对应动画的扩展方法
(1)定义一个确定滑动方向的枚举类型
/// <summary>
/// 动画滑动方法
/// </summary>
public enum AnimationSlideInDirection
{
/// <summary>
/// 无方向动画
/// </summary>
None = 0,
/// <summary>
/// 左测滑入
/// </summary>
Left = 1,
/// <summary>
/// 右测滑入
/// </summary>
Right = 2,
/// <summary>
/// 顶部滑入
/// </summary>
Top = 3,
/// <summary>
/// 底部滑入
/// </summary>
Bottom = 4
}
(2)根据元素设定的动画方向,添加元素对应动画的扩展方法
/// <summary>
/// 给 <see cref="FrameworkElement"/> 添加动画的扩展类
/// Helpers to animation framework elements in specific ways
/// </summary>
public static class FrameworkElementAnimationExtensions
{
/// <summary>
/// 滑入动画
/// </summary>
/// <param name="element">动画元素</param>
/// <param name="direction">划动的方向</param>
/// <param name="firstLoad">是否第一次加载</param>
/// <param name="seconds">动画时长</param>
/// <param name="keepMargin">动画期间是否保持元素的宽度相同</param>
/// <param name="size">动画的宽度/高度。如果未指定,则使用元素大小</param>
/// <returns></returns>
public static async Task SlideAndFadeInAsync(this FrameworkElement element, AnimationSlideInDirection direction, bool firstLoad, float seconds = 0.2f, bool keepMargin = true, int size = 0)
{
// 创建一个故事板
var sb = new Storyboard();
// 添加一个动画:滑入
switch (direction)
{
// 左侧滑入动画
case AnimationSlideInDirection.Left:
sb.AddSlideFromLeft(seconds, size == 0 ? element.ActualWidth : size, keepMargin: keepMargin);
break;
// 右侧滑入动画
case AnimationSlideInDirection.Right:
sb.AddSlideFromRight(seconds, size == 0 ? element.ActualWidth : size, keepMargin: keepMargin);
break;
// 顶部滑入动画
case AnimationSlideInDirection.Top:
sb.AddSlideFromTop(seconds, size == 0 ? element.ActualHeight : size, keepMargin: keepMargin);
break;
// 底部滑入动画
case AnimationSlideInDirection.Bottom:
sb.AddSlideFromBottom(seconds, size == 0 ? element.ActualHeight : size, keepMargin: keepMargin);
break;
default:
break;
}
// 添加一个动画:淡入
sb.AddFadeIn(seconds);
// 开始动画
sb.Begin(element);
// 展示页面
if (seconds != 0 || firstLoad)
element.Visibility = Visibility.Visible;
// 等待结束
await Task.Delay((int)(seconds * 1000));
}
/// <summary>
/// 滑出动画
/// </summary>
/// <param name="element">动画元素</param>
/// <param name="direction">划动的方向</param>
/// <param name="size">是否第一次加载</param>
/// <param name="seconds">动画时长</param>
/// <param name="keepMargin">动画的宽度/高度。如果未指定,则使用元素大小</param>
/// <returns></returns>
public static async Task SlideAndFadeOutAsync(this FrameworkElement element, AnimationSlideInDirection direction, float seconds = 0.2f, bool keepMargin = true, int size = 0)
{
// 创建一个故事板
var sb = new Storyboard();
// 添加一个动画:滑出
switch (direction)
{
// 左侧滑入动画
case AnimationSlideInDirection.Left:
sb.AddSlideToLeft(seconds, size == 0 ? element.ActualWidth : size, keepMargin: keepMargin);
break;
// 右侧滑入动画
case AnimationSlideInDirection.Right:
sb.AddSlideToRight(seconds, size == 0 ? element.ActualWidth : size, keepMargin: keepMargin);
break;
// 顶部滑入动画
case AnimationSlideInDirection.Top:
sb.AddSlideToTop(seconds, size == 0 ? element.ActualHeight : size, keepMargin: keepMargin);
break;
// 底部滑入动画
case AnimationSlideInDirection.Bottom:
sb.AddSlideToBottom(seconds, size == 0 ? element.ActualHeight : size, keepMargin: keepMargin);
break;
default:
break;
}
// 添加一个动画:淡出
sb.AddFadeOut(seconds);
// 开始动画
sb.Begin(element);
// 仅当我们正在设置动画时才使页面可见
if (seconds != 0)
element.Visibility = Visibility.Visible;
// 等待结束
await Task.Delay((int)(seconds * 1000));
// 隐藏上一个元素
if (element.Opacity == 0)
element.Visibility = Visibility.Hidden;
}
}
3.自定义具备动画能力页面(Page)基类,该类实现页面加载和卸载时以动画的方式呈现
public class AnimationPageBaseView :Page
{
#region 公共属性
/// <summary>
/// 加载页面动画方向
/// </summary>
public AnimationSlideInDirection PageLoadAnimationDirection { get; set; } = AnimationSlideInDirection.None;
/// <summary>
/// 卸载页面动画方向
/// </summary>
public AnimationSlideInDirection PageUnloadAnimationDirection { get; set; } = AnimationSlideInDirection.None;
/// <summary>
/// 标识在加载时,是否需要使用动画退出
/// 用于将页面移动到另外的一个Frame容器上
/// </summary>
public bool ShouldAnimationOut { get; set; }
/// <summary>
/// 滑动时间
/// </summary>
public float SlideSeconds { get; set; } = 0.5f;
#endregion
#region 构造函数
/// <summary>
/// 默认构造函数
/// </summary>
public AnimationPageBaseView()
{
// 让设计时,不去制作动画(不添加这个,设计界面会弹出“NullReferenceException”)
if (DesignerProperties.GetIsInDesignMode(this))
{ return; }
// 如果需要以动画的方式进入,首先将该页影藏
if (PageLoadAnimationDirection != AnimationSlideInDirection.None)
{ Visibility = Visibility.Collapsed; }
// 监听页面加载
Loaded += BasePage_LoadedAsync;
Unloaded += BasePage_UnLoadedAsync;
}
#endregion
#region 动画加载、卸载 Animation Load / Unload
/// <summary>
/// 一旦页面加载,执行必要的动画
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void BasePage_LoadedAsync(object sender, System.Windows.RoutedEventArgs e)
{
await AnimateInAsync();
}
/// <summary>
/// 页面卸载时,根据设置是否执行动画
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void BasePage_UnLoadedAsync(object sender, RoutedEventArgs e)
{
if (ShouldAnimationOut)
{
await AnimateOutAsync();
}
}
/// <summary>
/// 以动画的方式进入
/// </summary>
/// <returns></returns>
public async Task AnimateInAsync()
{
// 开始动画
await this.SlideAndFadeInAsync(PageLoadAnimationDirection, false, SlideSeconds, size: (int)Application.Current.MainWindow.Width);
}
/// <summary>
/// 以动画的方式退出
/// </summary>
/// <returns></returns>
public async Task AnimateOutAsync()
{
// 开始动画
await this.SlideAndFadeOutAsync(PageUnloadAnimationDirection, SlideSeconds);
}
#endregion
}
4.应用具备动画能力的页面(AnimationPageBaseView),修改页面的基类,并设置动画属性
注意:在XAML文件中,需要修改原来Page的根节点,修改为:local:AnimationPageBaseView。
/// <summary>
/// LoginView.xaml 的交互逻辑
/// </summary>
public partial class LoginView : AnimationPageBaseView
{
public LoginView()
{
InitializeComponent();
PageLoadAnimationDirection = Util.Animation.AnimationSlideInDirection.None;
}
}
/// <summary>
/// HomeView.xaml 的交互逻辑
/// </summary>
public partial class HomeView : AnimationPageBaseView
{
public HomeView()
{
InitializeComponent();
PageLoadAnimationDirection = Util.Animation.AnimationSlideInDirection.Bottom;
}
}
/// <summary>
/// ChamberView.xaml 的交互逻辑
/// </summary>
public partial class ChamberView : AnimationPageBaseView
{
public ChamberView()
{
InitializeComponent();
PageLoadAnimationDirection = Util.Animation.AnimationSlideInDirection.Left;
}
}
/// <summary>
/// RecordView.xaml 的交互逻辑
/// </summary>
public partial class RecordView : AnimationPageBaseView
{
public RecordView()
{
InitializeComponent();
PageLoadAnimationDirection = Util.Animation.AnimationSlideInDirection.Right;
}
}
/// <summary>
/// SettingsView.xaml 的交互逻辑
/// </summary>
public partial class SettingsView : AnimationPageBaseView
{
public SettingsView()
{
InitializeComponent();
PageLoadAnimationDirection = Util.Animation.AnimationSlideInDirection.Top;
}
}
Over
每次记录一小步...点点滴滴人生路...