WPF 和 UWP 中,不用设置 From 或 To,Storyboard 即拥有更灵活的动画控制

WPF 和 UWP 中,不用设置 From 或 To,Storyboard 即拥有更灵活的动画控制

发布于 2017-10-26 04:55 更新于 2018-02-19 22:41

无论是 WPF 还是 UWP 开发,如果用 StoryboardAnimation 做动画,我们多数时候都会设置 FromTo 属性,用于从起始值动画到目标值。然而动画并不总是可以静态地指定这些值,因为更多的时候动画的起始值和目标值取决于当前 UI 的状态。

本文中,我将将尽量避免设置 FromTo 值,让动画可以随时中断并重新开始,而中途不会出现突兀的变化。


本文涉及到的代码均在 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="TranslateStoryboard" 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 中指定 FromTo 是一个艰难的行为。我们只好在 .xaml.cs 文件中指定。

WPF

在 WPF 中,如果我们没有指定动画的 From,那么动画将从当前值开始;如果我们没有指定动画的 To,那么动画将到当前值结束。从这个角度上说,似乎不设置 FromTo 将导致动画保持在当前值不变,不会有动画效果。

但是,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);
    }
}

总结

  1. 在 WPF 中,可以不通过 FromTo 来指定动画的起始值和终止值;但如果真的不指定 FromTo,需要提前播放一次动画以确保动画能保持住元素状态;
  2. 在 WPF 中,如果没有指定 FromTo,那么动画结束后依然能直接为元素属性复制,且会立刻生效(正常情况下需要先清除动画);
  3. 在 UWP 中,必须指定动画的 To 才能按照期望播放到目标值。

本文会经常更新,请阅读原文: https://walterlv.com/post/using-storyboard-without-from-or-to.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 (walter.lv@qq.com)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏格子的个人博客

Markdown语法学习记录

鉴于每次写博客,写文章的时候,总是要重复去查询Markdown的相关语法,这种闹心的感觉我再也不要了。

11720
来自专栏GIS讲堂

Geoserver2.11矢量切片与OL3中的调用展示

GIS的底图一直使用金字塔技术进行切图,使用户能够快速访问指定级别的地图或者影像。但是切图本身是一张图片,无法进行交互。于是又引入了矢量图层用来显示矢量点线面,...

23430
来自专栏非典型技术宅

iOS动画系列之五:基础动画之缩放篇&旋转篇Swift+OC1. 思路和最终成果2. 抽取公共方法3. 懒加载Layer4. 添加动画

18110
来自专栏瞎说开发那些事

RPA与Excel(DataTable)

方法: 直接调用invokeCode,入参为已定义好的DataTable,出参为去重后的DataTable,代码如下

14320
来自专栏游戏开发那些事

【Unity游戏开发】浅谈 NGUI 中的 UIRoot、UIPanel、UICamera 组件

马三最近换到了一家新的公司撸码,新的公司 UI 部分采用的是 NGUI 插件,而之前的公司用的一直是 Unity 自带的 UGUI,因此马三利用业余时间学习了一...

20120
来自专栏AndroidTv

【Android】属性动画的使用理解

属性动画的教程网上已经特别多了,本篇也不打算再去各种详解知识点,主要就是记录题主学习属性动画时的碰到的一些困惑,以及后来自己的理解。如果有人也碰到相似的问题,正...

30530
来自专栏前端新视界

Vue.js 系列教程 5:动画

原文:intro-to-vue-5-animations 译者:nzbin 译者的话:经过两周的努力,终于完成了这个系列的翻译,由于时间因素及个人水平有限,并没...

61970
来自专栏DeveWork

jQuery仿极客公园火箭发射“返回顶部”效果(优化篇)

承接上一篇《jQuery仿极客公园火箭发射“返回顶部”效果(初始篇)》,本文将对前一篇的代码进行优化。还是转载自andyliu: 先给出个演示Demo:演示地址...

23260
来自专栏24K纯开源

用Qt写软件系列三:一个简单的系统工具之界面美化

前言      在上一篇中,我们基本上完成了主要功能的实现,剩下的一些导出、进程子模块信息等功能,留到后面再来慢慢实现。这一篇来讲述如何对主界面进行个性化的定制...

67570
来自专栏一“技”之长

设计iOS中随系统键盘弹收和内容文字长度自适应高度的文本框

    文本输入框是多数与社交相关的app中不可或缺的一个控件,这些文本输入框应该具备如下的功能:

11220

扫码关注云+社区

领取腾讯云代金券