WPF 和 UWP 中,不用设置 From 或 To,Storyboard 即拥有更灵活的动画控制
无论是 WPF 还是 UWP 开发,如果用 Storyboard 和 Animation 做动画,我们多数时候都会设置 From 和 To 属性,用于从起始值动画到目标值。然而动画并不总是可以静态地指定这些值,因为更多的时候动画的起始值和目标值取决于当前 UI 的状态。
本文中,我将将尽量避免设置 From 和 To 值,让动画可以随时中断并重新开始,而中途不会出现突兀的变化。
本文涉及到的代码均在 GitHub 上以 MIT License 开源:walterlv/sharing-demo at demo/storyboard-without-using-from-or-to。
预览效果
下面是本文期望实现的基本效果:
在 WPF 中的动画效果

在 UWP 中的动画效果

预备代码
为了让读者能够最快速地搭建一个可供试验的 DEMO,我这里贴出界面部分核心代码。
XAML 是这样的(这里的 XAML,WPF 和 UWP 完全一样,可以互相使用而不用修改任何代码):
- 布局部分
<Grid Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Content="平移至随机位置" Click="BeginStoryboard_Click"/>
<Button Grid.Row="0" Grid.Column="1" Content="从随机位置平移" Click="BeginStoryboard2_Click"/>
<Button Grid.Row="0" Grid.Column="2" Content="暂停" Click="PauseStoryboard_Click"/>
<Canvas x:Name="DisplayCanvas" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2">
<Rectangle x:Name="DisplayShape" Fill="ForestGreen" Width="120" Height="40">
<UIElement.RenderTransform>
<TranslateTransform x:Name="TranslateTransform" X="0" Y="0"/>
</UIElement.RenderTransform>
</Rectangle>
</Canvas>
</Grid>
- 资源部分
<Page.Resources>
<CircleEase x:Key="EasingFunction.Translate" EasingMode="EaseOut"/>
<!-- 为了方便使用,在 UWP 中加上了 x:Name;WPF 代码请删除 x:Name -->
<Storyboard x:Name="TranlateStoryboard" x:Key="Storyboard.Translate">
<DoubleAnimation Storyboard.TargetName="TranslateTransform" Storyboard.TargetProperty="X" EasingFunction="{StaticResource EasingFunction.Translate}"/>
<DoubleAnimation Storyboard.TargetName="TranslateTransform" Storyboard.TargetProperty="Y" EasingFunction="{StaticResource EasingFunction.Translate}"/>
</Storyboard>
</Page.Resources>
.xaml.cs 文件中预备一些属性和字段方便使用:
#if !WINDOWS_UWP
// 因为 WPF 不能在资源中指定 x:Name,所以需要在后台代码中手动查找动画资源。
private Storyboard TranslateStoryboard => (Storyboard)FindResource("Storyboard.Translate");
#endif
private DoubleAnimation TranslateXAnimation => (DoubleAnimation) TranslateStoryboard.Children[0];
private DoubleAnimation TranslateYAnimation => (DoubleAnimation) TranslateStoryboard.Children[1];
private readonly Random _random = new Random(DateTime.Now.Ticks.GetHashCode());
private Point NextRandomPosition()
{
var areaX = (int) Math.Round(DisplayCanvas.ActualWidth - DisplayShape.ActualWidth);
var areaY = (int) Math.Round(DisplayCanvas.ActualHeight - DisplayShape.ActualHeight);
return new Point(_random.Next(areaX) + 1, _random.Next(areaY) + 1);
}
探索动画
由于我们期望元素从当前所在的位置开始动画,到我们指定的另一个随机位置,所以直接在 XAML 中指定 From 和 To 是一个艰难的行为。我们只好在 .xaml.cs 文件中指定。
WPF
在 WPF 中,如果我们没有指定动画的 From,那么动画将从当前值开始;如果我们没有指定动画的 To,那么动画将到当前值结束。从这个角度上说,似乎不设置 From 和 To 将导致动画保持在当前值不变,不会有动画效果。
但是,WPF 允许在动画进行中修改动画参数,于是我们可以直接开始动画,然后再动画进行中修改元素属性到目标值。
也就是说,可以这么写:
private void BeginStoryboard_Click(object sender, RoutedEventArgs e)
{
TranslateStoryboard.Begin();
var nextPosition = NextRandomPosition();
TranslateTransform.X = nextPosition.X;
TranslateTransform.Y = nextPosition.Y;
}
快速点击这个按钮看看,你会发现每次点击都可以立即从当前位置开始向新的目标位置动画。

不过你应该注意到了一个坑——第一次并没有播放动画,而是直接跳到了目标位置;这是因为动画还没有保持住元素的位置。我们需要在初始化的时候播放一次动画;
private void OnLoaded(object sender, RoutedEventArgs e)
{
TranslateStoryboard.Begin();
TranslateStoryboard.Stop();
}
这样就解决了第一次动画不播放的问题。
现在,我们加上暂停按钮:
private void PauseStoryboard_Click(object sender, RoutedEventArgs e)
{
TranslateStoryboard.Pause();
}
即便是中途有暂停,依然能够继续让动画朝新的目标位置动画。

如果我们希望动画从一个新的起点开始,而不是从当前状态开始,则只需要在动画开始之前设置元素的位置即可:
private void BeginStoryboard2_Click(object sender, RoutedEventArgs e)
{
MoveToRandomPosition();
TranslateStoryboard.Begin();
MoveToRandomPosition();
void MoveToRandomPosition()
{
var nextPosition = NextRandomPosition();
TranslateTransform.X = nextPosition.X;
TranslateTransform.Y = nextPosition.Y;
}
}
UWP
UWP 的情况就不如 WPF 那么灵活了。在 UWP 中,如果不给动画指定 To 值,那么动画根本就会直接朝 0 位置执行。
于是在动画执行之前,设置动画的 To 值不可避免:
private void BeginStoryboard_Click(object sender, RoutedEventArgs e)
{
AnimateToRandomPosition();
TranslateStoryboard.Begin();
void Uwp_AnimateToRandomPosition()
{
var nextPosition = NextRandomPosition();
TranslateXAnimation.To = nextPosition.X;
TranslateYAnimation.To = nextPosition.Y;
}
}
在这样的写法下,灵活性与 WPF 相当,但 WPF 中支持在动画没有播放的时候随时设置元素位置,而这种方式则不行(其值会被动画保持)。
完整的后台代码
public partial class StoryboardPage : Page
{
public StoryboardPage()
{
InitializeComponent();
Loaded += OnLoaded;
}
#if !WINDOWS_UWP
private Storyboard TranslateStoryboard => (Storyboard)FindResource("Storyboard.Translate");
#endif
private DoubleAnimation TranslateXAnimation => (DoubleAnimation) TranslateStoryboard.Children[0];
private DoubleAnimation TranslateYAnimation => (DoubleAnimation) TranslateStoryboard.Children[1];
private readonly Random _random = new Random(DateTime.Now.Ticks.GetHashCode());
private void OnLoaded(object sender, RoutedEventArgs e)
{
Loaded -= OnLoaded;
TranslateStoryboard.Begin();
TranslateStoryboard.Stop();
}
private void BeginStoryboard_Click(object sender, RoutedEventArgs e)
{
Uwp_AnimateToRandomPosition();
TranslateStoryboard.Begin();
MoveToRandomPosition();
}
private void BeginStoryboard2_Click(object sender, RoutedEventArgs e)
{
MoveToRandomPosition();
Uwp_AnimateToRandomPosition();
TranslateStoryboard.Begin();
MoveToRandomPosition();
}
private void PauseStoryboard_Click(object sender, RoutedEventArgs e)
{
TranslateStoryboard.Pause();
}
[Conditional("WINDOWS_UWP")]
private void Uwp_AnimateToRandomPosition()
{
var nextPosition = NextRandomPosition();
TranslateXAnimation.To = nextPosition.X;
TranslateYAnimation.To = nextPosition.Y;
}
[Conditional("WPF")]
private void MoveToRandomPosition()
{
var nextPosition = NextRandomPosition();
TranslateTransform.X = nextPosition.X;
TranslateTransform.Y = nextPosition.Y;
}
private Point NextRandomPosition()
{
var areaX = (int) Math.Round(DisplayCanvas.ActualWidth - DisplayShape.ActualWidth);
var areaY = (int) Math.Round(DisplayCanvas.ActualHeight - DisplayShape.ActualHeight);
return new Point(_random.Next(areaX) + 1, _random.Next(areaY) + 1);
}
}
总结
- 在 WPF 中,可以不通过
From和To来指定动画的起始值和终止值;但如果真的不指定From和To,需要提前播放一次动画以确保动画能保持住元素状态; - 在 WPF 中,如果没有指定
From和To,那么动画结束后依然能直接为元素属性复制,且会立刻生效(正常情况下需要先清除动画); - 在 UWP 中,必须指定动画的
To才能按照期望播放到目标值。
WPF 和 UWP 中,不用设置 From 或 To,Storyboard 即拥有更灵活的动画控制的更多相关文章
- WPF MVVM模式中,通过命令实现窗体拖动、跳转以及显隐控制
原文:WPF MVVM模式中,通过命令实现窗体拖动.跳转以及显隐控制 在WPF中使用MVVM模式,可以让我们的程序实现界面与功能的分离,方便开发,易于维护.但是,很多初学者会在使用MVVM的过程中遇到 ...
- 将 WPF、UWP 以及其他各种类型的旧 csproj 迁移成基于 Microsoft.NET.Sdk 的新 csproj
原文 将 WPF.UWP 以及其他各种类型的旧 csproj 迁移成基于 Microsoft.NET.Sdk 的新 csproj 写过 .NET Standard 类库或者 .NET Core 程序的 ...
- 年度巨献-WPF项目开发过程中WPF小知识点汇总(原创+摘抄)
WPF中Style的使用 Styel在英文中解释为”样式“,在Web开发中,css为层叠样式表,自从.net3.0推出WPF以来,WPF也有样式一说,通过设置样式,使其WPF控件外观更加美化同时减少了 ...
- [UWP小白日记-11]在UWP中使用Entity Framework Core(Entity Framework 7)操作SQLite数据库(一)
前言 本文中,您将创建一个通用应用程序(UWP),使用Entity Framework Core(Entity Framework 7)框架在SQLite数据库上执行基本的数据访问. 准备: Enti ...
- UWP: 在 UWP 中使用 Entity Framework Core 操作 SQLite 数据库
在应用中使用 SQLite 数据库来存储数据是相当常见的.在 UWP 平台中要使用 SQLite,一般会使用 SQLite for Universal Windows Platform 和 SQLit ...
- 在 UWP 中实现 Expander 控件
WPF 中的 Expander 控件在 Windows 10 SDK 中并不提供,本文主要说明,如何在 UWP 中创建这样一个控件.其效果如下图: 首先,分析该控件需要的一些特性,它应该至少包括如下三 ...
- 优化 UWP 中图片的内存占用
跟图片打交道的 UWP 应用或多或少都会遇到图片带来的性能问题,就算不主要处理图片,做个论坛做个新闻客户端都涉及到大量图片.一个帖子.一篇文章里多半都是些高清大图,这些图片一张即可占用程序 1~2M ...
- 【UWP】在 UWP 中使用 Exceptionless 进行遥测
2020年1月17日更新: nightly build 版本已发布 https://www.myget.org/feed/exceptionless/package/nuget/Exceptionle ...
- UWP中实现自定义标题栏
UWP中实现自定义标题栏 0x00 起因 在UWP开发中,有时候我们希望实现自定义标题栏,例如在标题栏中加入搜索框.按钮之类的控件.搜了下资料居然在一个日文网站找到了一篇介绍这个主题的文章: http ...
随机推荐
- 面试笔试总结(二)之 C++基础
上节,一定要写出基于引用计数的智能指针 明白单例模式 会写出代码 复习: 1- 2- 推荐leveldb ....是c++的写代码很规范的地方?比如智能指针在这里... 对类进行改造 可以改成Sing ...
- CountDownLatch详解
功能描述 一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待. 常见用法 多个人等一个信号后继续执行操作.例如5个运动员,等一个发令员的枪响. 一个人等多个人的信号. ...
- LeetCode第[73]题(Java):Set Matrix Zeroes(矩阵置0)
题目:矩阵置0 难度:Easy 题目内容: Given a m x n matrix, if an element is 0, set its entire row and column to 0. ...
- 解决VS2015中出现类似于error C4996: 'scanf': This function or variable may be unsafe的安全检查错误
用习惯了VS老版本的人当刚使用VS2013的时候可能总遇到类似于这样的错误: error C4996: 'scanf': This function or variable may be unsafe ...
- 织梦dedecms获取当前内容页栏目id号的方法
一,可在内容模板中直接这样写{dede:field.typeid/} 可显示本栏目的id 二,也可这样写 {dede:type}[field:ID /]{/dede:type} . 三, 如果是在{ ...
- Sql Server 2008 清理日志文件
Use LingZhong Select NAME,size From sys.database_files ALTER DATABASE LingZhong SET RECOVERY SIMPLE ...
- 解决无法启动mysql服务错误1069
之前在服务器上修改了my.ini文件 mysql就一直无法启动 后来把my.ini改回原来的,还是无法启动并报1069错误 在网上查了一下,基本上都是说修改mysql密码,再重新启动,试了一下没作用, ...
- MVC,MVVM,MVP等设计模式的分析
从Script到Code Blocks.Code Behind到MVC.MVP.MVVM 三个模式按照大致的历史进程将这些概念进行划分: Script Code Blocks.Code Behind ...
- Python之路day12 web 前端(HTML+ css)
HTML文档 文档树: Doctype Doctype告诉浏览器使用什么样的html或xhtml规范来解析html文档 有和无的区别 BackCompat:标准兼容模式未开启(或叫怪异模式[Quirk ...
- CSS布局 — 圣杯布局与双飞翼布局
圣杯布局 实现原理 html代码中,middle部分首先要放在container的最前部分,然后是left,right 将三者都设置 float:left, position:relative (因为 ...