Главная » WPF » Анимация WPF

0

Можно сказать, что анимация – это модификация некоторого значения на про тяжении времени. Любой объект, в котором периодически вычисляется некоторая функция, можно считать частным случаем анимации. Начнем с простого примера.

Анимация как new Timer

Чтобы лучше понять, как работает анимация, попробуем реализовать  ее трудным путем. Предположим,  что мы хотим увеличить размер шрифта кнопки от 9.0 до 18.0 постоянными  приращениями за 5 секунд. Запустим  таймер и при каждом срабатыва нии будем вычислять новое значение FontSize. Время запуска сохраним в переменной

_start; тогда разность между _start и текущим временем можно будет использовать для вычисления приращения на очередной секунде (нам надо увеличить размер шрифта на

9.0 за 5 секунд). Когда будет достигнута верхняя граница, мы отключим таймер:

<Window x:Class=’EssentialWPF.Animations’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’EssentialWPF’

<Button

Name=’_button1’

HorizontalAlignment=’Center’ VerticalAlignment=’Center’ FontSize=’9pt’>

Hello World

</Button>

</Window>

public partial class Animations : Window {

public Animations() { InitializeComponent();

DispatcherTimer timer = new DispatcherTimer();

timer.Interval = TimeSpan.FromMilliseconds(50);

_start = Environment.TickCount; timer.Tick += timer_Tick; timer.IsEnabled = true;

}

long _start;

void timer_Tick(object sender, EventArgs e) {

long elapsed = Environment.TickCount   _start;

if (elapsed >= 5000) {

_button1.FontSize = 18.0; ((DispatcherTimer)sender).IsEnabled = false; return;

}

_button1.FontSize = 9.0 + (9.0 / (5000.0 / elapsed));

}

}

Некоторые параметры в этом примере «зашиты» в код: продолжительность анима ции, начальное и конечное значения. Зашит и еще один, более тонкий аспект: частота кадров. Но прежде чем переходить к частоте кадров, явно выделим все параметры:

long _start;

double _duration = 5000; double _from = 9.0; double _to = 18.0;

void timer_Tick(object sender, EventArgs e) {

long elapsed = Environment.TickCount   _start;

if (elapsed >= _duration) {

_button1.FontSize = _to; ((DispatcherTimer)sender).IsEnabled = false; return;

}

double increase = _to   _from;

_button1.FontSize = _from + (increase / (_duration / elapsed));

}

Теперь у нас есть универсальная функция для вычисления размера шрифта в любой момент между начальной и конечной точкой. (Поддержку уменьшения размера шрифта я оставляю читателю в качестве упражнения.) Очевидно, у этого подхода есть целый ряд недостатков (даже если исключить проблему частоты кадров). Прежде всего, всякий раз, как мы захотим создать анимируемое свойство, этот код придется писать заново. Кроме того, все кончится тем, что для каждой анимации будет создаваться свой таймер, или придется организовывать центральное хранилище информации обо всех анимациях.

А теперь добро пожаловать  в мир настоящей  анимации.

Анимация   – это  общий  способ  инкапсулировать  зависимость  значений свойств от времени. Отслеживание частоты кадров, обратные вызовы для вычис ления нового значения и объектная модель для создания повторно используемых анимаций  – все это сосредоточено  в одном месте. Мы можем переписать  преды дущий пример, определив  новый тип MyFontAnimation:

public class MyFontAnimation : AnimationTimeline {

double _from = 9.0;

double _to = 18.0;

public double From { get { return _from; } set { _from = value; }

}

public double To { get { return _to; } set { _to = value; } }

public MyFontAnimation() {}

public override Type TargetPropertyType {

get { return typeof(double); }

}

public override object GetCurrentValue(object defaultOriginValue, object defaultDestinationValue,

AnimationClock animationClock) {

TimeSpan? current = animationClock.CurrentTime;

double increase = _to   _from;

return _from + (increase /

((double)Duration.TimeSpan.Ticks /

(double)current.Value.Ticks));

}

protected override Freezable CreateInstanceCore() {

return new MyFontAnimation();

}

}

Как видно из этого примера, для представления анимации  нужно следующее: начальное значение (From), конечное значение (To), фиксированное время пере хода от начального значения  к конечному (Duration) и таймер, позволяющий из менять текущее значение с течением времени (он представлен типом AnimationClock и запускается самой системой).

Далее  мы  можем  модифицировать код  и  воспользоваться этой  анимацией вместо таймера:

public partial class Animations : Window {

public Animations() { InitializeComponent();

MyFontAnimation animation = new MyFontAnimation();

animation.Duration = TimeSpan.FromMilliseconds(5000);

_button1.BeginAnimation(Button.FontSizeProperty, animation);

}

}

Это лишь первое знакомство  с системой анимации,  точнее, с классом AnimationClock. В классе MyFontAnimation определен  алгоритм  анимации  зна чения типа double, а объект этого класса модифирует значение анимируемого свойства.  Класс же AnimationClock отслеживает текущее состояние  каждой  вы полняющейся анимации.

Методу  BeginAnimation в качестве  параметра  передается  свойство,  которое мы хотим анимировать. Сделав класс MyFontAnimation производным от AnimationTimeline и наделив некоторыми параметрами,  мы превратили его в универсальное средство анимации  любого значения  типа double. Но в WPF уже есть предопределенные анимации  для всех стандартных  типов данных. Поэтому последний  шаг в реализации нашей собственной  анимации  – отказаться от нее вовсе и заменить встроенным  типом DoubleAnimation:

public partial class Animations : Window {

public Animations() { InitializeComponent();

DoubleAnimation animation = new DoubleAnimation();

animation.From = 9.0;

animation.To = 18.0;

animation.Duration = TimeSpan.FromMilliseconds(5000);

_button1.BeginAnimation(Button.FontSizeProperty, animation);

}

}

Написанному нами простому коду класса MyFontAnimation недостает многих функций, имеющихся в классе DoubleAnimation. У всех встроенных в WPF анимаций имеется общий набор средств24, и одна из них – возможность начать анимацию с теку щего значения (мы не задавали свойства From). Воспользовавшись ей, мы можем из менить код, опустив значение From; при этом он сохранит полную работоспобность.

24 Возможно, изучая пространство имен System.Windows.Media, вы обратили внимание, что неко торые типы следуют общему паттерну, но не имеют общего базового типа. Для создания строго типизированных наборов, анимаций и прочего команда WPF пользовалась шаблоном генерации кода. Этот общий шаблон (в конечном продукте от него не осталось никаких следов) и гаранти рует, что у всех анимаций одни и те же свойства и методы, хотя определены они независимо.

public partial class Animations : Window {

public Animations() { InitializeComponent();

DoubleAnimation animation = new DoubleAnimation();

animation.To = 18.0;

animation.Duration = TimeSpan.FromMilliseconds(5000);

_button1.BeginAnimation(Button.FontSizeProperty, animation);

}

}

Познакомившись с основами работы анимации,  мы можем перейти к вопросу об управлении. Вместо того чтобы начинать  анимацию  автоматически в момент отображения окна, сделаем так, чтобы она начиналась  при наведении  мыши  на элемент управления. Для этого запустим  анимацию  не в конструкторе, а в обра ботчике события MouseEnter:

public partial class Animations : Window {

public Animations() { InitializeComponent();

_button1.MouseEnter += Button1_MouseEnter;

}

void Button1_MouseEnter(object sender, MouseEventArgs e) { DoubleAnimation animation = new DoubleAnimation(); animation.To = 18.0;

animation.Duration = TimeSpan.FromMilliseconds(5000);

_button1.BeginAnimation(Button.FontSizeProperty, animation);

}

}

Общим  для всех эффектов наката является возврат  к исходному  состоянию, когда мышь покидает  элемент  управления. Можно  было бы написать  обратную анимацию, которая возвращает  шрифт к размеру 9.0, но более элегантно было бы просто иметь анимацию «при выходе». Поскольку мы не задавали значение From для анимации  «при входе», то даже при изменении начального размера шрифта в разметке все будет работать правильно.  Это несомненный  плюс. В рассматривае мом случае анимация  поддерживает режим, когда значения  From или To не зада ются. При этом предполагается, что анимация  продолжается от текущего значе ния свойства до не анимированного значения:

public partial class Animations : Window {

public Animations() { InitializeComponent();

_button1.MouseEnter += Button1_MouseEnter;

_button1.MouseLeave += Button1_MouseLeave;

}

void Button1_MouseEnter(object sender, MouseEventArgs e) { DoubleAnimation animation = new DoubleAnimation(); animation.To = 18.0;

animation.Duration = TimeSpan.FromMilliseconds(5000);

_button1.BeginAnimation(Button.FontSizeProperty, animation);

}

void Button1_MouseLeave(object sender, MouseEventArgs e) { DoubleAnimation animation = new DoubleAnimation(); animation.Duration = TimeSpan.FromMilliseconds(5000);

_button1.BeginAnimation(Button.FontSizeProperty, animation);

}

}

Выглядит  неплохо, но есть проблема. Один из принципов WPF требует отде лять определение  пользовательского интерфейса от поведения. Приведенный императивный код подключения анимации  гораздо  изящнее,  чем первоначаль ный подход, основанный на таймере, но было бы еще лучше, если бы мы могли перенести часть определения интерфейса в разметку. Это становится возможным при использовании более высокого уровня анимации,  который называется «рас кадровкой»  (storyboard).

Термин «раскадровка» пришел из киноиндустрии. Мы заранее описываем пос ледовательность эпизодов, затем кричим «Мотор», и все снимается в заданном по рядке. В нашем примере эффект при входе описывается одной раскадровкой, а эф фект при выходе – другой. Чтобы определить  экземпляр класса Storyboard, необ ходимо спланировать одну или несколько анимаций  и для каждой задать объект и его анимируемое  свойство:

public partial class Animations : Window {

Storyboard _enter; Storyboard _leave;

public Animations() { InitializeComponent();

DoubleAnimation animation1 = new DoubleAnimation();

animation1.To = 18.0;

animation1.Duration = TimeSpan.FromMilliseconds(5000); Storyboard.SetTargetName(animation1, _button1.Name); Storyboard.SetTargetProperty(animation1,

new PropertyPath(Button.FontSizeProperty));

_enter = new Storyboard();

_enter.Children.Add(animation1);

DoubleAnimation animation2 = new DoubleAnimation(); animation2.Duration = TimeSpan.FromMilliseconds(5000); Storyboard.SetTargetName(animation2, _button1.Name); Storyboard.SetTargetProperty(animation2,

new PropertyPath(Button.FontSizeProperty));

_leave = new Storyboard();

_leave.Children.Add(animation2);

_button1.MouseEnter += Button1_MouseEnter;

_button1.MouseLeave += Button1_MouseLeave;

}

void Button1_MouseEnter(object sender, MouseEventArgs e) {

_enter.Begin(this);

}

void Button1_MouseLeave(object sender, MouseEventArgs e) {

_leave.Begin(this);

}

}

Вот теперь мы очень близки  к цели. Выделив  переменную, в которой хранят ся описания  свойств, мы можем перенести ее в разметку. Поскольку объекты Storyboard – не элементы управления, необходимо поместить их в секцию ресур сов окна. Подробно ресурсы мы будем рассматривать в главе 6, а пока достаточно знать, что с ресурсом можно ассоциировать ключ и обращаться  к нему по этому ключу с помощью метода FindResource:

<Window x:Class=’EssentialWPF.Animations’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’EssentialWPF’

<Window.Resources>

<Storyboard x:Key=’_enter’>

<DoubleAnimation To=’18.0’ Duration=’0:0:5’

Storyboard.TargetName=’_button1’ Storyboard.TargetProperty=’FontSize’ />

</Storyboard>

<Storyboard x:Key=’_leave’>

<DoubleAnimation Duration=’0:0:5’ Storyboard.TargetName=’_button1’ Storyboard.TargetProperty=’FontSize’ />

</Storyboard>

</Window.Resources>

<Button

Name=’_button1’

HorizontalAlignment=’Center’ VerticalAlignment=’Center’ FontSize=’9pt’>

Hello World

</Button>

</Window>

Определив анимации  в разметке, мы можем изменить  код так, чтобы он искал нужный ресурс и активировал подходящий экземпляр Storyboard:

public partial class Animations : Window {

public Animations() { InitializeComponent();

_button1.MouseEnter += Button1_MouseEnter;

_button1.MouseLeave += Button1_MouseLeave;

}

void Button1_MouseEnter(object sender, MouseEventArgs e) {

((Storyboard)FindResource(«_enter»)).Begin(this);

}

void Button1_MouseLeave(object sender, MouseEventArgs e) {

((Storyboard)FindResource(«_leave»)).Begin(this);

}

}

Напомним,  что мы сделали. Начали  мы с использования таймера и написали анимацию  вручную.  Затем  обнаружилось, что вместо  этого  можно  воспользо ваться готовым классом DoubleAnimation. С помощью имеющихся в нем свойств мы реализовали простой  эффект  наката, создав анимацию,  которая  работает без явного  задания  значений  From  и To. И, наконец,  мы объявили эти анимации  в разметке, а для управления ими применили раскадровку.

Остался последний  шаг. Ассоциация между событием (MouseEnter и MouseLeave) и действием  (запуск  Storyboard) по прежнему  «зашита»  в код. Ес ли мы хотим, чтобы вся последовательность отображения объявлялась в размет ке, то нужно перенести туда и этот кусочек.

Для этого завершающего  этапа понадобится новая концепция:  триггеры. Мы еще будем говорить о них в главе 7, но сама идея настолько  важна для анимации, что стоит упомянуть  о ней прямо здесь. Триггеры позволяют  декларативно ассо циировать  действие с событием или значением  свойства.

В данном случае мы ассоциируем  событие MouseEnter с действием BeginStoryboard. Вместо того чтобы определять  раскадровку в секции  ресурсов окна, мы можем описать ее прямо в теге BeginStoryboard, и она будет вызывать ся напрямую. То же самое можно сделать и для события MouseLeave:

<Window x:Class=’EssentialWPF.Animations’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’EssentialWPF’

<Button

Name=’_button1’

HorizontalAlignment=’Center’ VerticalAlignment=’Center’ FontSize=’9pt’>

<Button.Triggers>

<EventTrigger RoutedEvent=’Button.MouseEnter’>

<EventTrigger.Actions>

<BeginStoryboard>

<Storyboard>

<DoubleAnimation To=’18.0’ Duration=’0:0:5’

Storyboard.TargetName=’_button1’ Storyboard.TargetProperty=’FontSize’ />

</Storyboard>

</BeginStoryboard>

</EventTrigger.Actions>

</EventTrigger>

<EventTrigger RoutedEvent=’Button.MouseLeave’>

<EventTrigger.Actions>

<BeginStoryboard>

<Storyboard>

<DoubleAnimation Duration=’0:0:5’ Storyboard.TargetName=’_button1’ Storyboard.TargetProperty=’FontSize’ />

</Storyboard>

</BeginStoryboard>

</EventTrigger.Actions>

</EventTrigger>

</Button.Triggers> Hello World

</Button>

</Window>

Вот теперь действительно все. Мы проделали путь от чисто императивного ре шения,  основанного  на таймерах,  до полностью  декларативного описания  с по мощью встроенной  системы  анимирования. Мы видели,  как можно  расширить эту систему, не затрагивая при этом большую часть типичных операций. А теперь внимательно рассмотрим  те идеи, которые лежат за всем увиденным.

Источник: К. Андерсон  Основы  Windows Presentation Foundation. Пер. с англ. А. Слинкина — М.: ДМК Пресс, 2008 — 432 с.: ил.

По теме:

  • Комментарии