Создать графический примитив и анимировать его, например передвинув его с точки А в точку В с постоянной скоростью – дело нехитрое. Но что если нужно расположить несколько объектов в определенной последовательности и потом их нелинейно анимировать? Для этого ни в WPF ни в Silverlight нет встроенных функций. В этом очерке я покажу, как можно создавать объекты и анимацию динамически, используя лямбда-делегаты и функции высшего порядка.
Допустим вам потребовалось создать (и анимировать) нечто подобное:
В принципе, можно обойтись и циклами, но если есть возможность сделать все опрятнее и понятнее, «в одну строчку», то почему бы не воспользоваться? Начнем с простого – набор кружков это очевидно коллекция, поэтому создадим класс который будет хранить ссылки на все объекты:
Пока все тихо – мы просто определили коллекцию которая ограничена по типу содержимого (должно наследовать от
Тут следует остановиться и посмотреть, что же происходит. Во-первых это fluent interface, потому как у него в конце написано
Внимание: автоматического приведения типов тут нет, поэтому если тип свойства –
Как это использовать? Да очень просто. Например чтобы создать десять кругов увеличивающегося размера, мы пишем следующее:
Такое выражение позволяет нам сделать диаметр зависимым от позиции элемента. В нашем случае он будет 1.5 пикселя для самого маленького элемента, и 15 для самого большого. Причем как видно из кода, можно варьировать ширину и высоту независимо.
Поскольку изменение X и Y координат нужно часто, можно написать полезный метод который упростит задачу:
А теперь возьмем все это вместе и создадим ту картинку, что мы показали в начале:
Вот и все – с помощью пары методов можно очень легко создавать разные «созвездия» элементов. Теперь посмотрим на анимацию.
Линейная анимация типа
Теперь у нас есть класс, который за нас делает линейную интерполяцию, а мы в свою очередь можем получить преобразованное значение и что-то с ним сделать.
Поскольку мы работаем с коллекциями, нам опять же пригодится возможность создания набора таких анимационных объектов. Вот как раз такой класс:
Конструкторов несколько, выше показан только один из них. Параметры – это генераторы значений, то есть все параметры анимации тоже могут быть производными от позиции элемента в коллекции. Параметр
Вот простой пример: разворачиваем нашу спираль в синусоиду:
Единственная неприятность тут – это отсутствие ковариантности. Именно из-за этого мы не можем передать круги в качестве параметра – приходится конвертировать в
Саму анимацию не показать, вот ее конечная фаза – вид спирали «с боку».
Расширять наш небольшой фреймворк просто. Наши элементы анимируются параллельно, а нам нужно чтобы все было последовательно? Нет проблем – просто чуть-чуть меняем
Так же и с другими элементами. Удачи!
Генерация
Допустим вам потребовалось создать (и анимировать) нечто подобное:
В принципе, можно обойтись и циклами, но если есть возможность сделать все опрятнее и понятнее, «в одну строчку», то почему бы не воспользоваться? Начнем с простого – набор кружков это очевидно коллекция, поэтому создадим класс который будет хранить ссылки на все объекты:
public class LambdaCollection<T> : Collection<T> where T : DependencyObject, new() { public LambdaCollection(int count) { while (count --> 0) Add(new T()); } ⋮ }
Пока все тихо – мы просто определили коллекцию которая ограничена по типу содержимого (должно наследовать от
DependencyObject
и иметь дефолтный конструктор), и добавили конструктор который создает определенное количество нужных нам объектов. А вот теперь самое интересное – мы добавляем метод, который может инициализировать свойства объектов 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; } }
Тут следует остановиться и посмотреть, что же происходит. Во-первых это fluent interface, потому как у него в конце написано
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
– это скучно. Гораздо интересней когда мы сами контролируем значение элемента. Если взять для примера именно этот тип, то можно очень просто переопределить его чтобы анимированное значение контролировалось нашим генератором: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
ожидает функцию 2го порядка, или «функцию-генератор функций», то есть генератор, который зависит от индекса в коллекции и значение которого зависит от интерполированного double
значения во время анимации. На языке C# это означает что сюда нужно передавать «двойную лямбду», например i => j => f(j)
.Вот простой пример: разворачиваем нашу спираль в синусоиду:
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);
Единственная неприятность тут – это отсутствие ковариантности. Именно из-за этого мы не можем передать круги в качестве параметра – приходится конвертировать в
UIElement[]
. А массив выбран именно потому, что хочется сразу узнать длину – хотя можно было бы и IEnumerable
использовать.Саму анимацию не показать, вот ее конечная фаза – вид спирали «с боку».
Расширения
Расширять наш небольшой фреймворк просто. Наши элементы анимируются параллельно, а нам нужно чтобы все было последовательно? Нет проблем – просто чуть-чуть меняем
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]); } } }
Так же и с другими элементами. Удачи!