📜 ⬆️ ⬇️

Use lambdas to animate WPF

Creating a graphic primitive and animating it, for example, moving it from point A to point B at a constant speed is a simple matter. But what if you need to arrange several objects in a certain sequence and then animate them nonlinearly? For this, neither WPF nor Silverlight has any built-in functions. In this essay, I will show how you can create objects and animations dynamically using lambda delegates and higher-order functions.


Generation


Suppose you need to create (and animate) something like this:


')
In principle, it is possible to manage with cycles, but if there is an opportunity to make everything tidier and clearer, “in one line”, then why not use it? Let's start with a simple one - a set of circles is obviously a collection, so we will create a class that will store links to all objects:

 public class LambdaCollection <T>: Collection <T> where T: DependencyObject, new ()
 {
   public LambdaCollection (int count) {while (count -> 0) Add (new T ());  }
   â‹®
 }

While everything is quiet - we simply defined a collection that is limited by the type of content (must inherit from DependencyObject and have a default constructor), and added a constructor that creates a certain number of objects we need. And now the most interesting thing is that we add a method that can initialize the properties of T objects using lambda delegates:

 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;
   }
 }

Then you should stop and see what happens. First of all, this is a fluent interface, because it says return this at the end. He himself takes two parameters. The first parameter is the property that we want to change in all elements of the collection . This seriously simplifies life as it is not necessary to write cycles everywhere. The second parameter is a reference to a value generator — that is, a function that takes an index of an item in the collection and returns a value of type U And the tim itself can be anything, the main thing is that it fits the property.

Attention: there is no automatic type conversion here, so if the property type is double , you cannot generate int type values, you will get an exception.

How to use it? Yes, very simple. For example, to create ten circles of increasing size, we write the following:

 var circles = new LambdaCollection <Ellipse> (10)
   .WithProperty (WidthProperty, i => 1.5 * (i + 1))
   .WithProperty (HeightProperty, i => 1.5 * (i + 1));

This expression allows us to make the diameter dependent on the position of the element. In our case, it will be 1.5 pixels for the smallest element, and 15 for the largest. Moreover, as can be seen from the code, you can vary the width and height independently.

Since changing the X and Y coordinates is often necessary, you can write a useful method that will simplify the task:

 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;
   }
 }

And now let's take all this together and create the picture that we showed at the beginning:

 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);

That's all - using a couple of methods you can very easily create different "constellations" of elements. Now look at the animation.

Animation


Linear animation like DoubleAnimation is boring. It is much more interesting when we ourselves control the value of an element. If we take this type for an example, then it is very easy to redefine it so that the animated value is controlled by our generator:

 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));
   }
 }

Now we have a class that does linear interpolation for us, and we in turn can get the transformed value and do something with it.

Since we are working with collections, again, we can use the ability to create a set of such animated objects. Just such a class:

 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]);
   }
 }

There are several constructors, only one of them is shown above. Parameters are generators of values, that is, all animation parameters can also be derived from the position of an element in the collection. The valueGenerator parameter expects a 2nd order function, or a “function generator function,” that is, a generator that depends on the index in the collection and whose value depends on the interpolated double value during the animation. In C #, this means that a “double lambda” must be passed here, for example, i => j => f(j) .

Here is a simple example: unwrap our spiral in a sine wave:

 var c = new LambdaDoubleAnimationCollection (
   circles.Count, 
   i => 10.0 * i, 
   i => new Duration (TimeSpan.FromSeconds (2)),
   i => j => 100.0 / j);
 c.BeginApplyAnimation (circles.Cast <UIElement> (). ToArray (), Canvas.LeftProperty);

The only trouble here is the lack of covariance. It is because of this that we cannot pass circles as a parameter - we have to convert to UIElement[] . And the array is chosen precisely because you want to know the length right away - although you could use IEnumerable .

The animation itself does not show, here is its final phase - a kind of spiral "from the side."



Extensions


Expanding our small framework is easy. Our elements are animated in parallel, but do we need everything to be consistent? No problem - just slightly change the 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]);
     }
   }
 }

The same with other elements. Good luck!

St. Petersburg ALT.NET Group

Source: https://habr.com/ru/post/57236/


All Articles