列表切换动画_MVVM 遇上 Async-Ctp
两个列表之间逻辑项切换的视觉表示

引言
在本文中,我将展示带有视觉表现的两个列表项之间的切换。我将在 MVVM 模式下进行,执行 View-ViewModel-View-ViewModel 传递,同时保持解耦。
我将使用 Async-Ctp 的 Awaitable Task 在 ViewModel 中进行异步等待。在此过程中,我将展示一种在 Silverlight 中实现 贝塞尔曲线动画 的技术。
背景
在过去的几年里,我遇到过一个可以理解的误解,那就是关于 WPF/Silverlight 中动画的使用/必要性/作用。
许多程序员,通常拥有深厚的 Winforms 背景,倾向于将动画视为旧 UI 集合的装饰性补充。从大量跳跃、旋转、变色的控件来看,这种误解是可以理解的。然而,事实上,动画实际上是允许应用程序与用户通信的 附加通道,从而传达有关应用程序状态和活动的重要信息(无论是否与用户操作相对应)。
有一次,我遇到了一家持有这种错误观点的公司,并决定承担说服他们动画在现代 UI 中扮演关键角色的任务。由于该公司从事篮球(是的,就是那个游戏……)管理相关的软件,我正在寻找该领域的一个场景来证明我的观点。
我想到的场景是“替换”管理场景。简而言之:一个包含两个列表的界面(首发阵容、替补席),允许应用程序操作员在它们之间执行项(Players
)的切换。尽管拖放解决方案很直观,但由于操作的易用性和快速响应原因而被排除。因此,我提出了一个由“在每个列表中选择球员”触发的切换解决方案。
这个场景的好处在于,它几乎不可能在 Winforms 中(以真正可理解的 UI 方式)实现,而使用 WPF/Silverlight 的动画则相当容易。
代码描述
ViewModel 中有两个逻辑上的球员列表(首发、替补),分别绑定到 View 中的两个 Listbox。当 ViewModel 检测到相互(首发和替补都已选中)选择时,它会引发一个事件,告知 View 开始“切换动画”:View 构建并显示相应的箭头,并执行动画。完成后,它会通知 Model,Model 会在其列表中“提交”逻辑切换,并存储切换数据以便撤销。
Using the Code
MVVM 遇见 Async-Ctp
流程可以这样说明

如上所述,“切换任务”实际上是在 ViewModel
识别出两个球员(每个列表一个)被选中后开始的。然后它执行 StoreAndPerformeSwitch
子例程。
private async void StoreAndPerformeSwitch(Player SwitchBP, Player SwitchFP)
这个子例程以“等待”视觉切换执行完成开始。
await TaskEx.Run(() =>PerformSwitch());
PerformSwitch
所做的就是引发 BeginSwitchAnimation
事件,并等待(其线程)ManualResetEvent
“门”被打开(设置)。
当 View 调用 ViewSwitchEnded
方法(当它完成动画时)时,它会被打开(被设置)。
然后它提交逻辑切换并将切换数据存储到撤销堆栈中。
SwitchData sd = new SwitchData() { BenchPlayer = SwitchBP, FivePlayer = SwitchFP };
...
sd.BenchPlayer.IsSelected = false;
sd.FivePlayer.IsSelected = false;
int InxInFive = Five.IndexOf(sd.FivePlayer);
int InxInBench = Bench.IndexOf(sd.BenchPlayer);
Bench[InxInBench] = Players[Players.IndexOf(sd.FivePlayer)];
Five[InxInFive] = Players[Players.IndexOf(sd.BenchPlayer)];
if (!OnUndo) stckSwitches.Push(new SwitchData()
{ BenchPlayer = Bench[InxInBench], FivePlayer = Five[InxInFive] });
最后,它“刷新” UndoCommand
- 这会导致它根据 Undo 堆栈的状态重新评估其 IsEnabled
状态。
UndoCommand = new DelegateCommand(Undo, CanUndo);
ArrowPath
最初,这是 WPF 版本项目中的一个自定义形状。这个自定义路径负责绘制箭头,并在此过程中构建一个匹配的贝塞尔曲线用于移动动画。
贝塞尔曲线动画
虽然 Silverlight 中的路径/曲线动画通常是通过将曲线分段并从一个片段动画到下一个片段来完成的,但在这里,我们可以使用一种更简单的方法:Bezier
实际上是一个数学公式,通过给出四个控制点来描述一条曲线。方便的是,对于任何零到一的输入值,它都可以返回其曲线上的 x,y 位置。因此,我们实际需要做的就是将值从零到一进行“DoubleAnimation
”,将其放入我们的贝塞尔曲线公式中,然后获得我们需要定位动画元素的 X,Y。正如前面的几行暗示的那样,应该有一个 Object
,一方面持有贝塞尔曲线公式,另一方面接收动画值。
在一个“完美的世界”里,我们将构建一个具有两个 AttachedProperties
的对象
Zero2One
(double
)Bezier
(AnimatedBezier
)
我们将它附加到目标元素上(我们想为其设置相应的 x,y)。类似这样(伪代码)
<TranslateTransform x:Name="ttSTF" Bezier={Binding ElementName="
ArrowWithMovementBezier", Path="MovementBezier" }/>
然后,我们只需要 DoubleAnimation
,并设置 Storyboard.TargetName="ttSTF"
和类似这样的内容 - Storyboard.TargetProperty="(local:OurObject.Zero2One)"
。
在 Zero2One
依赖属性更改的子例程中,我们将通过针对贝塞尔曲线公式进行计算来设置附加对象 x,y 的值。
不幸的是,似乎属性路径到自定义附加属性不支持!
也就是说,您可以拥有这个 propertyPath
- '...(Canvas.Left
)',但您不能这样做 - '...(some_namespace:MyCanvas.MyLeft
)'
* 如果任何读者知道如何设置 PropertyPath
到自定义 AttachedProperty
,请分享。
在“现实生活”中,有几种方法可以克服这个障碍,我选择创建一个惰性代理元素。
<local:Bezier2DoubleAnimationMediator x:Name="mediator2ttSTB"
Target="{Binding ElementName=ttSTB}" AnimBezier="{Binding ElementName=SwitchToBenchArrow,
Path=AnimBezier}"/>
该元素具有 Zero2OneValue
dp,AnimBezier
dp,如上一个解决方案,但具有额外的 Target
dp。
现在动画作用于该元素的 Zero2OneValue
DependencyProperty
,因此,它将改变其 Target 的 X,Y 值。
历史
- 2011年11月24日:初版