在 WPF 或 Silverlight 动画中使用 Lambda 表达式






4.91/5 (83投票s)
演示了如何使用 lambda 表达式和高阶函数进行 WPF/Silverlight 图形处理。
引言
创建和动画化一个简单的图形元素,例如以恒定速度从 A 点移动到 B 点,是相当容易的。但如果你需要将多个图形对象排列成特定的布局,然后以非线性方式对它们进行动画化呢?Silverlight 和 WPF 都没有内置函数来完成此操作。在本文中,我将演示如何使用 lambda 委托和高阶函数动态创建对象和动画。
顺便说一句,你真的应该看看示例项目 - 这个动画( IMHO)相当令人印象深刻,它说明了 2D 动画如何能呈现出 3D 的效果。当然,人们早已在做这样的事情了,但我自己却惊讶于它竟然如此容易实现。
生成
让我们假设你决定创建类似以下的内容:
理论上,你可以只使用一个 for
循环,但既然有更简洁的实现方式,为什么不加以利用呢?让我们从简单的开始 - 一组圆显然是一个集合,所以我们来创建一个类来保存这些对象的引用。
public class LambdaCollection<T> : Collection<T> where T : DependencyObject, new()
{
public LambdaCollection(int count) { while (count --> 0) Add(new T()); }
⋮
}
到目前为止,我们保持简单 - 我们所做的只是定义了一个集合,该集合在包含的对象类型上有约束(只包含继承自 DependencyObject
且具有默认构造函数的对象)。我们添加了一个构造函数,用于创建所需数量的对象。但有趣的部分来了:我们现在添加一个方法,该方法可以使用 lambda 表达式来初始化其中 T
对象的属性。
public class LambdaCollection<T> : Collection<T> where T : DependencyObject, new()
{
⋮
public LambdaCollection<T> WithProperty<U>
(DependencyProperty property, Func<int, U> generator)
{
for (int i = 0; i < Count; ++i)
this[i].SetValue(property, generator(i));
return this;
}
}
让我们暂停一下,看看发生了什么。首先,你会注意到这是一个流式接口,因为方法以 return this
结尾。该方法本身接受两个参数。第一个参数是我们想要更改的属性,它作用于集合中的所有元素。第二个参数是对值生成器的引用,即一个函数,该函数接收集合中的元素索引并返回一个 U
类型的值。这个类型可以是任何东西 - 唯一的要求是它必须与正在设置的属性匹配。
注意:这里没有自动类型转换,所以如果属性是 double
类型,你就不能生成一个 int
类型的值 - 否则会抛出异常。
那么我们如何使用这段代码呢?它非常简单。例如,要创建十个大小逐渐增加的圆,我们这样写:
var circles = new LambdaCollection<Ellipse>(10)
.WithProperty(WidthProperty, i => 1.5 * (i+1))
.WithProperty(HeightProperty, i => 1.5 * (i+1));
这样的表达式允许我们将直径与元素位置相关联。在我们的例子中,最小元素的直径是 1.5 像素,最大的是 15 像素。而且,正如你在代码中看到的,宽度和高度可以独立变化。
鉴于 X 和 Y 坐标的操作是一项非常常见的任务,我们可以编写一个有用的方法来进一步简化它。
public class LambdaCollection<T> : Collection<T> where T : DependencyObject, new()
{
⋮
public LambdaCollection<T> WithXY<U>(Func<int, U> xGenerator, Func<int, U> yGenerator)
{
for (int i = 0; i < Count; ++i)
{
this[i].SetValue(Canvas.LeftProperty, xGenerator(i));
this[i].SetValue(Canvas.TopProperty, yGenerator(i));
}
return this;
}
}
现在,让我们将它们整合在一起,创建我们在文章开头展示的那个图像。
int count = 20;
var circles = new LambdaCollection<Ellipse>(count)
.WithXY(i => 100.0 + (4.0 * i * Math.Sin(i / 4.0 * (Math.PI))),
i => 100.0 + (4.0 * i * Math.Cos(i / 4.0 * (Math.PI))))
.WithProperty(WidthProperty, i => 1.5 * i)
.WithProperty(HeightProperty, i => 1.5 * i)
.WithProperty(Shape.FillProperty, i => new SolidColorBrush(
Color.FromArgb(255, 0, 0, (byte)(255 - (byte)(12.5 * i)))));
foreach (var circle in circles)
MyCanvas.Children.Add(circle);
就是这样 - 使用一对方法,可以轻松创建各种“星座”。现在让我们来看看动画。
动画
使用 DoubleAnimation
进行线性动画很无聊。当我们自己控制元素值时,会更有趣。这其实很简单 - 通过采用现有的动画类,我们可以重新定义它的动画“tick”值,使其由我们自己的生成器控制。
public class LambdaDoubleAnimation : DoubleAnimation
{
public Func<double, double> ValueGenerator { get; set; }
protected override double GetCurrentValueCore
(double origin, double dst, AnimationClock clock)
{
return ValueGenerator(base.GetCurrentValueCore(origin, dst, clock));
}
}
现在我们有了一个为我们进行线性插值的类,而我们可以获得一个转换后的值并对其进行处理。
考虑到我们正在处理集合,再次定义一个用于我们目的的集合类会很有用。这是一个这样的类:
public class LambdaDoubleAnimationCollection : Collection<LambdaDoubleAnimation>
{
⋮
public LambdaDoubleAnimationCollection
(int count, Func<int, double> from, Func<int, double> to,
Func<int, Duration> duration, Func<int, Func<double, double>> valueGenerator)
{
for (int i = 0; i < count; ++i)
{
var lda = new LambdaDoubleAnimation
{
From = from(i),
To = to(i),
Duration = duration(i),
ValueGenerator = valueGenerator(i)
};
Add(lda);
}
}
public void BeginApplyAnimation(UIElement [] targets, DependencyProperty property)
{
for (int i = 0; i < Count; ++i)
targets[i].BeginAnimation(property, Items[i]);
}
}
实际上,这里拥有多个构造函数(或带有许多可选参数的构造函数)是有益的。这里的参数是值生成器,也就是说,这些参数可以从集合中元素的位置派生。valueGenerator
参数期望一个二阶函数或一个“函数生成器”,即一个依赖于集合索引的生成器,其值依赖于动画过程中插值的 double
值。在 C# 编程语言中,这意味着使用“双 lambda”,例如 i => j => f(j)
。
这是一个将我们的螺旋展开成正弦波的小动画示例:
var c = new LambdaDoubleAnimationCollection(
circles.Count,
i => Canvas.GetLeft(circles[i]),
i => 10.0 * i,
i => new Duration(TimeSpan.FromSeconds(2)),
i => j => 100.0 / j);
c.BeginApplyAnimation(circles.Cast<UIElement>().ToArray(), Canvas.LeftProperty);
我无法展示动画本身,但这是最终结果的视图:
扩展
扩展这个迷你框架很容易。例如,如果你想让元素按顺序而不是并行地进行动画,你可以将 LambdaDoubleAnimationCollection
更改为以下内容:
public class LambdaDoubleAnimationCollection : Collection<LambdaDoubleAnimation>
{
⋮
public void BeginApplyAnimation(UIElement [] targets, DependencyProperty property)
{
for (int i = 0; i < Count; ++i)
{
Items[i].BeginTime = new TimeSpan(0);
targets[i].BeginAnimation(property, Items[i]);
}
}
public void BeginSequentialAnimation(UIElement[] targets, DependencyProperty property)
{
TimeSpan acc = new TimeSpan(0);
for (int i = 0; i < Items.Count; ++i)
{
Items[i].BeginTime = acc;
acc += Items[i].Duration.TimeSpan;
}
for (int i = 0; i < Count; ++i)
{
targets[i].BeginAnimation(property, Items[i]);
}
}
}
任何其他你可能需要的操作也是如此。祝你好运!