流畅和命令式动画






4.96/5 (85投票s)
使用与基于帧的动画片段良好集成的流畅API轻松创建动画。
- 下载 WpfSample - 发布版 - 197.3 KB
- 下载 WinFormsSample - 发布版 - 24.1 KB
- 下载完整源代码 - 946.2 KB
- 下载适用于 Windows 应用商店应用的库
旧链接 - 不确定它们是否都有效
查看 Cyberminds57 动画 - 外部资源,需要 Silverlight 5
查看使用此库制作的射击游戏 - 外部资源,需要 Silverlight 5。您可以在 Shoot'em Up .NET[^] 查看如何使用此库编写游戏的教程。
您还可以通过 CodePlex 为此库做出贡献。
示例
AnimationBuilder.
BeginSequence().
BeginParallel().
Walk(pfzCharacter, LookDirection.Down, 200).
Walk(sboCharacter, LookDirection.Down, 220).
EndParallel().
Say(1.5, "Hi, my name is Paulo Zemek.", HorizontalAlignment.Right, pfzCharacter).
Say(1.5, "Hi, my name is Sébastien Boudreau.", HorizontalAlignment.Left, sboCharacter).
Say(1.5, "We are aliens.", HorizontalAlignment.Center, pfzCharacter, sboCharacter).
BeginParallel().
Walk(pfzCharacter, LookDirection.Left, -pfzCharacter.Width).
Walk(sboCharacter, LookDirection.Right, canvas.ActualWidth).
EndParallel().
Add(() => { pfzCharacter.Top = 500; pfzCharacter.Left = 200; }).
Walk(pfzCharacter, LookDirection.Up, 300).
Say(3, "Humm... it seems I teleported myself again.\r\nWhy was I walking after all?", HorizontalAlignment.Right, pfzCharacter).
Add(_RandomMoves(pfzCharacter)). // Random moves is frame-based.
Range(1.0, 0.0, 1, (value) => pfzCharacter.Opacity = value).
EndSequence();
点击此处查看该特定动画运行(需要 Silverlight 5)。
或点击下一张图片查看更完整的示例(与下载示例中的相同 [也需要 Silverlight 5])
简介
在撰写完文章 Fluent Method and Type Builder 后,我开始思考游戏和动画,我在想是否有可能创建一些东西来将基于帧和基于时间的动画集成到一个易于使用的 API 中。
我必须说,对于复杂动画,我绝对更喜欢基于帧的动画,因为在每一帧我都可以检查状态并决定“下一步”是什么。我不需要处理复杂的计算来决定如果一整秒过去了该怎么做,因为我可以在内存中逐帧执行直到我到达正确的帧。这大大简化了代码,但在某些情况下,基于时间的动画要好得多(因为我可以通过一次计算拥有任意数量的“中间帧”),但这正是我在集成两种动画时遇到的问题。
我最初想创建一个东西,让在 WPF 的 Storyboards
中使用基于帧的动画变得更容易,但我最终做了一些完全不同的事情,我认为这更符合我对动画的理解。
事实上,我认为 WPF 中的动画存在以下问题:
- 它们是 WPF 特有的。是的,我谈论的是 WPF 的
Storyboard
,但是如果我只想在 Windows Forms 应用程序甚至在“处理动画”但通过 TCP 将结果发送给客户端的服务器应用程序中使用动画功能呢? - 它们对我来说太冗长了。我更喜欢简单的动画,如果可能的话,一行代码就能搞定。
- 我认为每个动画都需要有一个开始时间才能紧接在另一个动画之后开始是很糟糕的。这迫使开发人员在之前的动画需要更长的时间时重新编写所有开始时间。此外,所有持续时间在创建动画时都必须已知,如果我们的动画作为序列的一部分需要等待用户输入,这可能是不可能实现的。
Timeline
和AnimationTimeline
类已经承担了太多的职责,这使得它们难以正确继承和实现;AnimationTimeline
期望生成值以影响单个属性(该属性必须是DependencyProperty
)。
我想要的是(也是我将在本文中介绍的)一个动画框架:
- 能够动画化基于帧和基于时间的动画;
- 能够说明哪些动画并行运行,哪些动画按序列运行,因此您无需知道每个动画的开始时间;
- 能够混合基于帧和基于时间的动画(请注意,这与简单地支持两种类型不同);
- 能够以命令式或声明式方式工作(也可以混合使用);
- 能够通过单个动画动画化所需的任意数量的属性;
- 可用于其他类型的应用程序(如服务器应用程序、Windows Form 应用程序等);
- 通过实现单个接口(
IAnimation
)易于扩展; - 能够通过使用其他动画(如循环、速度修改器和条件)“装饰”动画来改变某些行为。
声明式和命令式动画
如果“声明式”和“命令式”这两个术语不够清楚,我将通过示例展示它们的区别。
声明式
AnimationBuilder.
BeginLoop().
BeginSequence().
Range(20, 100, 1, (value) => button.Height = value).
Range(100, 20, 1, (value) => button.Height = value).
EndSequence().
EndLoop();
命令式
IEnumerable<IAnimation> _Animation()
{
while(true)
{
yield return Range.Create(20, 100, 1, (value) => button.Height = value);
yield return Range.Create(100, 20, 1, (value) => button.Height = value);
}
}
“流畅API”是该库的声明式部分。在其中,我们描述了整个“动画流程”,但我们并没有通过单个指令真正控制动画的执行。我们当然使用指令来创建声明式动画,但动画本身不再受我们的指令控制。有些人认为这更好,因为我们可以轻松看到整个动画流程,但同时这也更受限制,因为这样的流程应该在动画开始之前就已知。
另一方面,“命令式”方法将执行您的代码以了解下一步要做什么。这开启了使用您需要的任何编程指令的可能性,因此您可以检查不同的状态,进行真正复杂的计算,然后返回适当的动画部分,或者您甚至可以使动画逐帧工作(因此,您不需要返回下一个动画,而是执行您需要做的事情并返回 WaitNextFrame()
)。
所以,如果我们将前面的例子这样写,它就可以是基于帧的:
IEnumerable<IAnimation> _Animation()
{
var helper = FrameBasedAnimation.CreateHelper(80);
// this helper will keep the animation at 80 frames per second.
while(true)
{
for(int i=20; i<100; i++)
{
button.Height = value;
yield return helper.WaitNextFrame();
}
yield return Range.Create(100, 20, 1, (value) => button.Height = value);
// Yeah, I am mixing the modes here. The first part of the animation is
// frame-based and the second part uses the Range which is time based.
// But it is not hard to imagine that we can copy the first for and
// make it work from 100 to 20.
}
}
流畅动画构建器 API - 基础
为了理解这个库,我将从“流畅API”开始。它是基础部分,您将用它来创建许多不同且非交互式的动画。即使您以后决定创建基于帧的动画,您仍然可以依靠“流畅API”来创建更小的序列,这些序列是用户操作的结果,所以我没有理由不从它开始。
流畅API 的核心是 AnimationBuilder
类。通过它,您可以创建任何类型的“动画情节提要”。
这里的原则很简单:
- 所有创建支持内部动画片段的动画片段的方法都以
Begin
开头,后跟“动画名称”,并通过调用EndAnimationName
结束此类片段; - 在任何支持内部片段的片段内部,您可以调用
Add()
来提供手动创建的片段或提供命令式动画; - 在任何时候,您都可以调用
Range
来创建从一个值到另一个值的动画; - 支持内部片段的动画片段可以是装饰器(它们在某种程度上改变单个内部动画的行为),也可以是动画组(目前只有两种,一种并行运行动画,一种按序列运行动画)。
即使装饰器只支持一个内部动画,您也可以始终创建并行或顺序组作为其内容,因此您可以在最初只支持一个片段的片段内部运行许多动画。
你被这些术语(比如装饰器)搞糊涂了吗?好的,那么让我们再看看声明式动画的基本示例:
AnimationBuilder.
BeginLoop().
BeginSequence().
Range(20, 100, 1, (value) => button.Height = value).
Range(100, 20, 1, (value) => button.Height = value).
EndSequence().
EndLoop();
BeginLoop()
正在创建一个 Loop
装饰器。Loop
,顾名思义,将循环(或者如果你喜欢,重复)其唯一的内部动画。
但正如你所看到的,它的单个动画是一个序列(由 BeginSequence
调用创建),并且在这个序列中我们有两个 Range
调用。
如果你反向分析这一点,我们可以说:
- 我们有一个片段,它将在一秒钟内使按钮的
Height
变大(从高度20到100); - 我们有一个片段,它将使按钮的
Height
变小; - 然后我们说它们将按序列运行(因此一个接一个);
- 最后,该序列将以无限循环运行。
方法摘要
我想前面的例子足以说明问题了。现在我将介绍库中附带的所有方法并解释它们的作用。
我不会在这里给出真正复杂的例子,但如果您下载示例,您可以看到许多不同的动画及其代码。
方法名称 | 示例用法 + 描述 |
Range | Range(initialValue, finalValue, durationInSeconds, (value) => character.Left = value);
这里展示的动画只试图展示如果两个对象以相同的持续时间但不同的最终值进行动画,会发生什么。它对于与下一个项目进行比较很有用。 |
RangeBySpeed | RangeBySpeed(initialValue, finalValue, speed, (value) => character.Left = value);
// or
RangeBySpeed(() => character.Left, finalValue, speed, (value) => character.Left = value);
这同样是 第二个版本接收一个委托来读取实际值,如果您在声明动画时不知道初始值(在示例中, 这里展示的动画试图通过使用 |
BeginRanges / EndRanges | BeginRanges((value) => circle.Fill = new SolidColorBrush(value), Colors.Black).
To(Colors.Blue, durationInSeconds).
To(Colors.Red, 1).
To(Colors.Green, 1).
To(Colors.Yellow, 1).
To(Colors.Magenta, 1).
Wait(1).
To(Colors.Black, 1).
EndRanges();
BeginRanges/EndRanges 是一种特殊的序列,用于创建许多范围,所有这些范围都将执行相同的委托来更新值。因此,在 |
Add | someAnimationContainer.
Add(animation);
|
BeginLoop / EndLoop | BeginLoop(5).
Range(0, 100, 1, (value) => button.Left = value).
EndLoop();
|
BeginParallel / EndParallel | BeginParallel().
Range(0, 500, 10, (value) => item.Left = value).
Add(someOtherAnimation).
EndParallel();
在一个并行块中,所有添加的动画都将并行运行。重要的是要注意,并行动画片段本身只有当最长寿的内部动画结束时(如果它确实结束了)才被视为结束。 前面的动画只展示了两个并行范围,即使所提供的代码不完整(我从未展示过 someOtherAnimation 是如何声明的)。 |
BeginSequence / EndSequence | BeginSequence().
Add(someAnimation).
Add(someOtherAnimation).
EndSequence();
在我看来,序列是最自然的动画组。你知道你想做很多事情,一个接一个。你不在乎(或者你不知道)每件“事情”需要多长时间,但没问题,序列只是告诉你一个“事情”(动画片段)在另一个之后开始。 前面的动画展示了两个范围将如何按序列播放。此示例仅旨在展示第二个“部分”在第一个完成之后如何开始。 |
BeginAcceleratingStart / EndAcceleratingStart | BeginAcceleratingStart(1).
Range(0, 450, 3, (value) => Canvas.SetLeft(circle, value)).
EndAcceleratingStart();
在许多情况下,动画可能需要缓慢启动,然后达到其正常速度。当然,您可以保持“时间”不变并加速内部动画。但为了简化许多您可能想要“加速启动”的情况,您可以使用 这个装饰器会“欺骗”内部动画,说它经过的时间比实际开始时要少,然后逐渐加速直到达到正常速度。它接收的参数是达到正常速度所需的时间,但那是“内部时间”,这意味着这个时间已经受到这种加速的影响。这样做是为了让您可以要求一秒钟的加速启动,并放入一个只有一秒钟的内部动画。在这种情况下,它会在达到全速时结束(如果使用了外部时间,加速将在一秒钟内完成,但由于内部动画开始时较慢,它会稍后结束)。 |
BeginDeacceleratingEnd / EndDeacceleratingEnd | BeginDeacceleratingEnd(1, 3).
Range(0, 450, 3, (value) => Canvas.SetLeft(circle, value)).
EndDeacceleratingEnd();
这种效果与上一个效果相反,它会使动画的结束减速。如果内部动画大于应该减速的部分,您应该将整个动画持续时间作为第二个参数。不幸的是,在这种情况下,您应该知道动画需要多长时间才能知道它应该在哪里开始减速。 |
BeginTimeMultiplier / EndTimeMultiplier | BeginTimeMultiplier(2).
Range(0, 450, 3, (value) => Canvas.SetLeft(circle, value)).
EndTimeMultiplier();
在此示例中,我们可以说这样的 |
BeginProgressiveTimeMultiplier / EndProgressiveTimeMultiplier | BeginProgressiveTimeMultiplier(1, 0.2, 1.5).
Add(someAnimation).
EndProgressiveTimeMultiplier();
|
BeginRunCondition / EndRunCondition | BeginRunCondition(() => checkBoxRun.IsChecked).
Add(someAnimation).
EndRunCondition();
|
BeginPrematureEndCondition / EndPrematureEndCondition | BeginPrematureEndCondition(() => !walking).
BeginLoop().
Range(0, character.FrameCount - 0.001, 0.4, (value) => character.FrameIndex = (int)value).
EndLoop().
EndPrematureEndCondition();
|
BeginPauseCondition / EndPauseCondition | BeginPauseCondition(() => checkBoxPaused.IsChecked).
BeginLoop().
Range(0, character.FrameCount - 0.001, 0.4, (value) => character.FrameIndex = (int)value).
EndLoop().
EndPauseCondition();
这可能是基于帧的动画中最有用的一个,因为它使得添加暂停支持变得极其简单,而无需在每一帧都验证暂停条件,因此,它对游戏非常有用。 |
BeginTimeLimit / EndTimeLimit | BeginTimeLimit(5).
Add(animationWithUnknownDuration).
EndTimeLimit();
这个装饰器将限制内部动画的运行时间。它在内部用于某些具有多个不同装饰器序列的构造中,所有这些装饰器都作用于同一个动画,因此每个装饰器都被限制在一定时间内。但如果您只是想展示游戏关卡的“几秒钟”,它可能直接有用。 |
BeginDisposeOnEnd / EndDisposeOnEnd | BeginDisposeOnEnd().
Add(animationThatConsumesLotOfResources).
EndDisposeOnEnd();
通常,即使内部动画结束了,它们也会保留在内存中。如果您将它们放入循环中,它们将被重置并再次运行,这很有用。但是,如果您正在创建一个消耗大量资源且不会被重置的动画,您可以将其放入 |
Wait | sequence.Wait(5);
|
BeginSegmentedTime / EndSegmentedTime | BeginSegmentedTime(TimeSpan.FromMilliseconds(15), collisionDetectionDelegate).
Add(animationToBeSegmented).
EndSegmentedTime();
如果您正在使用基于时间的片段(如 因此,要做到这一点,您需要指定测试片段的间隔(通常用于碰撞检测)。每个内部动画将运行到该片段大小,执行给定的委托,并继续前进直到达到实际经过的时间。在值较小的情况下,内部动画会被处理以生成平滑的结果。 |
命令式动画 - 中级
声明式动画必须从一开始就知道它们将要做的一切,即使它们可以通过使用 RunCondition
和 Add
调用来执行任何代码而产生一些交互,但它们仍然受到限制。它们的目的不是为了交互。
另一方面,命令式动画可以使用任何编译器构造,例如 while
、if
,甚至 try/catch/finally
块,更重要的是,它们可以 yield return
其他动画来运行,就像声明式动画一样,因此您可以在命令式动画中创建复杂的逻辑,然后 yield return
可以使用“声明式 API”创建的更简单的片段。
我之前已经介绍过命令式动画,但让我们再看一遍:
IEnumerable<IAnimation> ImperativeAnimation()
{
while(true)
{
yield return AnimationBuilder.Range(20, 100, 1, (value) => button.Height = value);
yield return AnimationBuilder.Range(100, 20, 1, (value) => button.Height = value);
}
}
如您所见,命令式动画的“核心”是使用 yield return
关键字来提供要执行的下一个动画。但是,如果您的代码已经在更新它需要更新的对象,而您只想等待下一帧呢?我最初想到接受 yield return null;
作为 WaitNextFrame()
指令,但我无法保证所有动画都具有相同的帧率。
所以,为了解决这个问题,我创建了 FrameBasedAnimationHelper
,顾名思义,它是一个用于制作基于帧动画的辅助类。创建它时,您应该告诉动画的帧率,当您需要时,您可以使用 yield return helper.WaitNextFrame()
甚至 yield return helper.Wait(someValue)
。
由于它被创建为普通实例而不是 static
,您可以并行运行许多动画,但具有不同的帧率。WPF、Silverlight 或 Windows Forms 触发动画的频率并不重要,该辅助类将使您的动画保持在正确的帧率,补偿丢失的帧并在调用彼此太近时等待。
private static IEnumerable<IAnimation> _RandomMoves(ICharacterImage character)
{
var random = new Random();
var helper = FrameBasedAnimationHelper.CreateByFps(60);
for (int i = 0; i < 60*4; i++)
{
double value = (i / 60.0) + 1;
int x = random.Next(2);
int y = random.Next(2);
if (x == 0)
x = -1;
if (y == 0)
y = -1;
character.Left += x * value;
character.Top += y * value;
yield return helper.WaitNextFrame();
}
}
ImperativeAnimation.AddSubordinatedParallel
您可能已经明白,ImperativeAnimation
的 yield return
将作为序列工作。但是,如果您想添加一个动画并行运行怎么办?
通常的答案是直接将此类动画添加到 AnimationManager
。动画管理器只是并行运行所有添加的动画。但在这种情况下,动画将完全独立。例如,如果实际动画正在 TimeMultiplier
内部运行,则新动画将不会受到其影响。如果实际动画结束,另一个动画将继续运行。
如果这不是你想要的,还有另一种选择。在命令式动画内部,你可以调用 ImperativeAnimation.AddSubordinatedParallel()
。该方法将在相同的容器内,与当前的命令式动画并行添加一个动画。也就是说,如果命令式动画在 TimeMultiplier
内部,那么这个动画也将处于 TimeMultiplier
内部。此外,作为一个从属动画,当命令式动画结束时,这个并行动画也会结束。也就是说:它将并行运行,具有相同的装饰器,但它仍然被视为一个“子”动画,只有在主动画存活时才会存活。
我想给出一个命令式动画的方法摘要,但由于它们可以使用任何 C# 构造,所以这将毫无用处。所以我的摘要是:
- 使用
yield return
非常适合“即时”运行新动画; - 如果您想要基于帧的动画,请创建一个
FrameBasedAnimationHelper
并调用yield return helper.WaitNextFrame()
; - 如果不是 yield 返回一个动画,而是想要初始化一个与当前动画并行运行的动画,请使用
ImperativeAnimation.AddSubordinatedParallel()
。
扩展库 - 高级
我创建这个库时考虑到了可扩展性,这个库中有许多扩展点:您可以创建新的 RangeCalculator
(我提供了用于 int
、double
、Point
和 Color
的范围计算器,但是如果您想要一个用于 Rectangle
或其他数值类型的范围计算器呢?),您可以创建新的 SpeedToTimeCalculator
(这就是 RangeBySpeed
的工作原理,它会先计算时间,然后它将是一个正常的 Range
),您可以通过实现 IAnimation
接口或创建命令式动画来创建新的动画片段。
但是如果你看文章的第一个例子,我正在使用 Walk()
和 Say()
方法,它们不属于这个库。不幸的是,如果不改变 AnimationBuilder
的源代码,真的无法扩展它的类型,但是我们可以为“流畅API”创建扩展方法。这不是我真正认为完美的解决方案,但是整个“流畅API”只是为了让你能够编写看起来像声明式的代码,所以为了这个目的,不要害怕创建扩展方法。
那么,让我们探讨如何以多种不同方式扩展这个库:
创建新的 RangeCalculators
Range
可能是您最常用的动画片段,但此库仅支持 int
、double
、Color
和 Point
。如果您需要其他类型的范围,则需要创建自己的范围计算器。创建范围计算器最简单的方法是继承自 RangeCalculator<T>
,其中 T
是您想要计算范围的数据类型。通过继承此类,您只需实现 Calculate
方法,该方法接收 initialValue
、finalValue
、actualTime
和 duration(或 totalTime
)。对于大多数类型,该方法将如下所示:
TimeSpan remaining = duration-actualTime;
return (initialValue * remaining.TotalSeconds + finalValue * actualTime.TotalSeconds) / duration.TotalSeconds;
我之所以说它“看起来”像这样,是因为你可能需要强制转换,并非所有类型都可以简单地乘以 double(例如,在颜色上,每个元素都必须独立处理),而且你可能很想创建一个不同的、不是“线性”的计算器。
嗯,在创建你的 RangeCalculator
之后,你可以将它作为参数传递给 Range,或者你可以将你的范围计算器注册为它的类型的默认计算器。就像这样:
RangeCalculator<MyType>.Default = new MyTypeRangeCalculator();
例如,这是计算两个点之间范围的代码:
public sealed class PointRangeCalculator:
RangeCalculator<Point>
{
public override Point Calculate(Point initialValue, Point finalValue, TimeSpan actualTime, TimeSpan duration)
{
double initialMultiplier = (duration - actualTime).TotalSeconds;
double finalMultiplier = actualTime.TotalSeconds;
double totalDivider = duration.TotalSeconds;
double x = (((initialValue.X * initialMultiplier) + (finalValue.X * finalMultiplier)) / totalDivider);
double y = (((initialValue.Y * initialMultiplier) + (finalValue.Y * finalMultiplier)) / totalDivider);
return new Point(x, y);
}
}
注意:这是生成此类计算器所需的最小代码。如果您查看源代码,您会注意到我创建了一个私有构造函数,并且我还声明了一个 Instance 属性。我这样做是因为不需要创建多个此类范围计算器实例,但如果您不这样做,您的代码将继续工作,并且如果您只创建一个实例并将其注册为默认实例,您将不会使用超过必要的内存。
创建新的 SpeedToTimeCalculators
RangeBySpeed
并没有使用完全不同的逻辑来计算范围。它只是通过速度计算给定范围的持续时间,然后调用普通的 Range
。
但是要根据速度信息计算持续时间,也需要找到该计算器,而且,同样,只有某些类型(int
、double
、Color
和 Point
)具有计算器。如果您想支持其他类型,您应该创建 SpeedToDurationCalculator
。
要做到这一点,请继承 SpeedToDurationCalculator<T>
并实现 CalculateDuration
方法。它接收 initialValue
、finalValue
和 speed
。重要的是要知道,即使 finalValue
小于 initialValue
,结果也不应为负(收到的 speed
也不应小于或等于零)。
speed
表示该值在一秒钟内会改变多少。因此,对于 Color
,它是差异最大的元素在一秒钟内会改变多少(其他元素不会干扰速度,因为它们是最大改变的比例)。
因此,以颜色为例,这是创建此类 SpeedToDurationCalculator
的代码:
public sealed class ColorSpeedToDurationCalculator:
SpeedToDurationCalculator<Color>
{
public override TimeSpan CalculateDuration(Color initialValue, Color finalValue, double speed)
{
double a = Math.Abs(finalValue.A - initialValue.A);
double r = Math.Abs(finalValue.R - initialValue.R);
double g = Math.Abs(finalValue.G - initialValue.G);
double b = Math.Abs(finalValue.B - initialValue.B);
double maxValue = Math.Max(a, Math.Max(r, Math.Max(g, b)));
return TimeSpan.FromSeconds(maxValue / speed);
}
}
实现 IAnimation 接口
IAnimation
接口具有以下成员:
IsUseless
, Reset
, Update
,并且它也是可处置的,因此它具有 Dispose
方法。因此,要实现它,您应该理解这些成员中的每一个。
如果动画已被处置或不再起作用,IsUseless
属性应返回 true。例如,没有内部动画的装饰器或组是无用的。此值有助于外部容器,然后可以删除此类无用的动画。
Reset
方法应该清理与动画实际位置相关的任何已用资源,并将其重新定位到开头。在正常情况下,调用 Reset
不应使对象变得无用,并且由 Loop 使用,以便动画可以一遍又一遍地播放。
Dispose
,嗯,它应该释放所有使用的资源并使动画无用。请注意,与通常预期的不同,Dispose
通常不会在动画上调用。如果您想在动画中间停止它,它很有用。如果动画正常结束,它应该能够被重置。
最后,是 Update
方法。它预计只会在正向经过时间的情况下被调用。在 Update
中,您应该根据经过的时间执行动画应该做的所有事情,并返回 true
如果动画应该再次播放,或者 false
如果动画结束。在正常情况下,动画结束不应该将其销毁,因为该动画可能会被 Reset
并再次播放。
IAfterEndAwareAnimation
假设你的动画是一个序列的一部分。它将持续 100 毫秒,但有一个减速,下一次更新只在 500 毫秒后到来。当然你的动画会结束,但如果没有这种减速,下一个动画将在 400 毫秒前开始。你应该忽略它,让下一个动画从零开始,还是让下一个动画推进那 400 毫秒?
如果您能精确计算动画何时结束,那么请实现 IAfterEndAwareAnimation
。它的唯一目的是告诉动画结束之后经过了多长时间,并且在序列中,它将帮助纠正减速(或者只是在中间结束的动画),这将避免注意到从一个动画片段到另一个动画片段的“小暂停”。
创建新装饰器
如果您想要一种不能通过使用 Range 或命令式动画实现的动画类型,创建新的 IAnimation
实现可能会很有用。但是也许您只想修改现有动画。
嗯,您能做的修改现有动画的事情不多(您只能拦截对 Reset
、Update
、IsUseless
和 Dispose
的调用),而且许多现有的行为更改已经实现。但是,如果您有一个框架中尚未实现的关于行为更改的想法,您可以继承 AnimationDecorator
类型。
在其中,您需要实现 Update
方法,因为这是您通常会拦截的方法,但您可以自由地重写其他方法。作为装饰器,它具有 InnerAnimation
属性。例如,TimeMultiplier
只是在将其传递给内部动画之前将经过的时间相乘,而 PrematureEndCondition
将验证条件,如果为 true
,则返回动画已结束,而不调用其内部动画。
“添加”方法到 Fluent API
在不更改 AnimationBuilder
单元的情况下,无法直接向其添加新方法,而且至少目前还没有方法添加静态扩展方法,因此如果您想添加一个可以直接通过 AnimationBuilder
静态类型使用的新方法,那是不可能的。
但是,如果您想要能够“添加”新方法到容器(也就是说,您已经做了类似 AnimationBuilder.BeginSequence().
的操作),您可以使用扩展方法,但这里又有一个问题,Fluent API 方法的签名非常麻烦。
解决方案是创建一个泛型扩展方法,其中 T
是 IAnimationBuilder
。所有的 AnimationBuilder
都实现了这个接口,所以这个方法将被添加到所有它们中,并且通过泛型,您可以返回正确的类型。因此,要添加一个普通方法(即,不是 Begin 方法),您可以使用类似这样的方法:
public static T MethodName<T>(this T animationBuilder, ... any parameters your method needs ...)
where
T: IAnimationBuilder
{
animationBuilder.Add(someAnimationThatShouldBeCreated);
return animationBuilder;
}
但如果您想创建 Begin/End
扩展方法对,您将需要额外做一些工作。您应该创建一个继承自 AnimationBuilder<TParent, TThis>
但将 TParent
保留为泛型参数的构建器。然后您应该创建一个接收父级和真实动画片段的构造函数,并且您还应该添加一个 EndName,它将返回 Parent
。您还需要实现 Add
方法(它通常会填充 InnerAnimation
或将给定动画添加到您自己的“动画”容器中)。
那么,扩展方法应该像这样:
public static NameBuilder<T> Name<T>(this T parent, ... parameters ...)
where
T: IAnimationBuilder
{
var animation = new NameAnimation(... parameters ...);
parent.Add(animation);
return new NameBuilder<T>(parent, animation);
}
示例
下载中的示例包含三个不同的应用程序:
- Windows Forms 应用程序仅用于演示该库可以在 Windows Forms 中使用,但它远非一个有用的示例。
- WpfSample 是一个更好的教程示例。它包含不同类型和复杂度的动画。
- 最后是 Silverlight 应用程序,它是该库更完整的应用,也是我们个人演示的开始。
外部文件
实际 Cyberminds57 动画中使用的背景图片来自网站 http://besthdwallpapersdesktop.com/designer-room/,不受本文许可限制。
Cyberminds57 中使用的声音来自 http://www.freesfx.co.uk/[^]。
我不记得士兵角色是从哪里得到的,但我真的相信它是来自 RPG Maker。
播放和暂停图标我从 http://www.iconarchive.com/show/play-stop-pause-icons-by-icons-land.html[^] 获得。
最后,我在标题和 WPF 示例中使用的那个机器人,我只知道它来自一个游戏,但我甚至不知道是哪个游戏,因为这是一个我拥有的非常老的动画。
版本历史
- 2016年6月18日:添加了 Windows 应用商店版库的下载;
- 2013年10月16日:添加了射击游戏示例,更改了许可证并添加了 codeplex 链接;
- 2013年8月25日:为 Cyberminds57 动画添加了声音,并添加了 SegmentedTime 类/动画片段;
- 2013年8月18日:添加了 PauseCondition,使 Cyberminds57 演示可暂停,并完成了一些代码中的 TODO,其中包括 Dispose 在此框架中应如何使用;
- 2013年8月8日:第一个版本。