65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (83投票s)

2010年6月4日

CPOL

4分钟阅读

viewsIcon

88493

downloadIcon

929

演示了如何使用 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]);
    }
  }
}

任何其他你可能需要的操作也是如此。祝你好运!

© . All rights reserved.