WPF 自定义页面动画(入)的应用——室内监控可视化

需求

在提升应用程序的用户体验上,动画(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

每次记录一小步...点点滴滴人生路...

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值