Основные принципы анимации в WinRT

189

На первый взгляд кажется, что анимации больше нужны программистам, работающим над играми иди моделированием физических процессов. Разве они нужна в спокойных, почтенных бизнес-приложениях?

Однако анимация играет в приложениях Windows 8 более важную роль, чем может показаться. Позже эта тема будет раскрыта, когда мы рассмотрим использование XAML для создания объектов ControlTemplate (шаблонов), полностью переопределяющих внешний вид элементов управления. И хотя самым важным аспектом ControlTemplate является визуальное дерево, шаблон также должен описывать изменение внешнего вида элемента управления в некоторых условиях. Например, элемент управления Button может выделяться при нажатии или окрашиваться в серый цвет при блокировке. Все эти изменения в оформлении ControlTemplate определяются как анимации — даже если изменение происходит мгновенно и мало похоже на анимацию.

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

Пространство имен Windows.UI.Xaml.Media.Animation

В статье «Таймеры и анимации в WinRT» было показано, как организовать анимацию объектов с использованием события CompositionTarget.Rendering - прием, который я назвал «ручной» анимацией. Хотя ручная анимация может быть достаточно мощной, у нее есть свои недостатки. Метод обратного вызова всегда выполняется в потоке пользовательского интерфейса, а это означает, что анимация может замедлить реакцию программы на ввод пользователя.

Кроме того, анимации, продемонстрированные с CompositionTarget.Rendering, были полностью линейными, то есть линейно увеличивали или уменьшали некоторое значение с течением времени. Часто небольшое изменение темпа анимации делает ее более приятной для глаза; обычно анимация ускоряется в начале и замедляется в конце, иногда с небольшим «обратным ходом» для пущего реализма. Разумеется, такие анимации тоже можно выполнить с использованием CompositionTarget.Rendering, но организовать необходимые вычисления может быть непросто.

В последующих примерах будут использоваться встроенные средства анимации Windows Runtime, состоящие из 71 класса, 4 перечислений и 2 структур, объединенных в пространство имен Windows.UI.Xaml.Media.Animation. Эти анимации часто выполняются в фоновых потоках и поддерживают ряд возможностей для реализации нетривиальных эффектов. Очень часто анимации полностью определяются в XAML, а затем инициируются из кода или (в одном особом, но распространенном случае) из XAML.

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

Анимация основана на изменениях некоторого свойства объекта. Это свойство часто называется «целевым свойством» анимации. Анимации Windows Runtime требуют, чтобы целевое свойство поддерживалось свойством зависимости, а следовательно, определялось в классе, производном от DependencyObject.

В некоторых графических средах поддерживаются кадровые анимации, у которых темп анимации определяется частотой смены кадров экрана. Различия в частоте смены кадров на разных аппаратных платформах могут привести к искажению темпа анимации. Анимации Windows Runtime относятся к категории синхронных, то есть выполняются по истечении фактических интервалов времени: секунд и миллисекунд.

Что произойдет, если поток, в котором выполняется анимация, должен выполнить некоторую работу и анимация пропустит несколько тактов? Кадровая анимация обычно продолжается с того места, в котором она была приостановлена. Синхронная анимация Windows Runtime подстраивается под текущее время и продолжается с того состояния, в котором она должна находиться.

Простой пример использования анимаций

Начнем с анимации свойства FontSize элемента TextBlock. Следующий проект содержит панель Grid из двух строк с полем TextBlock и кнопкой Button для запуска анимации. Очень часто анимации определяются в секции Resources корневого элемента файла XAML. Простая анимация состоит из объектов Storyboard и DoubleAnimation:

<Page ...>

    <Page.Resources>
        <Storyboard x:Key="storyboard">
            <DoubleAnimation Storyboard.TargetProperty="FontSize"
                             Storyboard.TargetName="txb"
                             EnableDependentAnimation="True"
                             From="1" To="144" Duration="0:0:3" />
        </Storyboard>
    </Page.Resources>

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <TextBlock Name="txb"
                   Text="Анимированный текст"
                   Grid.Row="0"
                   FontSize="48"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center" />

        <Button Content="Запустить!"
                VerticalAlignment="Center"
                HorizontalAlignment="Center"
                Grid.Row="1"
                Click="Button_Click" />
    </Grid>

</Page>

Имя класса DoubleAnimation вовсе не означает, что он выполняет две анимации! Это анимация, предназначенная для целевых свойств типа Double. Как вы вскоре увидите, Windows Runtime также поддерживает анимации для целевых свойств типа Point, Color и Object. (Казалось бы, анимации типа Object должно быть достаточно для любых целей, но в действительности такая анимация ограничивается заданием дискретных свойств значений вместо их плавного изменения.)

Windows Runtime требует, чтобы объект анимации (такой, как DoubleAnimation) был потомком Storyboard. Объект Storyboard может иметь несколько потомков для выполнения параллельных анимаций, а задача Storyboard - организационная структура для синхронизации потомков.

Storyboard также определяет два вложенных свойства с именами TargetName и TargetProperty. Значения этих свойств задаются в объекте анимации для обозначения целевого объекта и анимируемого свойства этого объекта:

<Storyboard x:Key="storyboard">
    <DoubleAnimation Storyboard.TargetProperty="FontSize"
                        Storyboard.TargetName="txb"
                        EnableDependentAnimation="True"
                        From="1" To="144" Duration="0:0:3"... />

По умолчанию анимации выполняются во вторичном потоке, чтобы поток пользовательского интерфейса оставался свободным для реакции на пользовательский ввод. Однако анимация свойства FontSize элемента TextBlock должна выполняться в потоке пользовательского интерфейса, потому что измерение размера шрифта инициирует изменение макета. Windows Runtime не любит выполнять анимации в потоке пользовательского интерфейса - вплоть до того, что по умолчанию они запрещены! Чтобы среда Windows Runtime знала о ваших намерениях (да, вы хотите, чтобы анимация была выполнена, даже если это происходит в потоке пользовательского интерфейса), необходимо задать свойству EnableDependentAnimation значение true.

В этом контексте под «зависимостью» (dependent) подразумевается зависимость от потока пользовательского интерфейса. В оставшейся части этой конкретной анимации указано, что она должна изменять значение свойства FontSize от 1 до 144 в течение трех секунд. Продолжительность анимации задается в часах, минутах и секундах. Все три значения и два двоеточия являются обязательными. Если задать только одно число, оно будет интерпретировано как целое количество часов, а два числа, разделенных двоеточием - как часы с минутами. Количество секунд может быть дробным. Если анимация должна выполняться больше суток, перед часами можно указать количество дней и точку.

При первом запуске этой программы элемент TextBlock отображается с высотой 48 пикселов, указанной в элементе TextBlock в файле XAML:

Текст до вызова анимации

Объект Storyboard не начинает работать сам по себе. Его выполнение должно быть инициировано - обычно каким-то условием в пользовательском интерфейсе. В этой программе обработчик Click элемента Button получает ссылку на Storyboard из коллекции Resources, после чего вызывает метод Begin():

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Animation;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            (this.Resources["storyboard"] as Storyboard).Begin();
        }
    }
}

Обратите внимание на директиву using для пространства Windows.UI.Xaml.Media.Animation. Она не генерируется автоматически шаблоном Visual Studio.

При запуске объекта Storyboard элемент TextBlock немедленно задает свойству FontSize значение 1 (свойство From в DoubleAnimation), после чего FontSize возрастает до 144 (свойство To в DoubleAnimation) в течение трех секунд. Возрастание происходит линейно: через секунду свойство FontSize составляет 48-2/3 пиксела, а через две секунды - 96-1/3. Через три секунды анимация останавливается, а размер шрифта TextBlock остается равным 144 пикселам.

Анимация размера текста

Если щелкнуть на кнопке еще раз, анимация начнется снова. Более того, это можно делать во время воспроизведения анимации, и она каждый раз будет начинаться заново с 1 пиксела.

Настройка параметров анимации

Когда анимация в программе завершается, свойство FontSize сохраняет значение, заданное свойством To объекта DoubleAnimation. Такое поведение определяется свойством FillBehavior объекта DoubleAnimation, которое по умолчанию содержит значение перечислимого типа HoldEnd. Также можно задать ему значение Stop:

<DoubleAnimation ... FillBehavior="Stop" />

В этом случае при завершении анимации свойство FontSize возвращается к своему исходному значению 48. Также в определении анимации можно не указывать значение From или To, например:

<DoubleAnimation Storyboard.TargetProperty="FontSize"
                 Storyboard.TargetName="txb"
                 EnableDependentAnimation="True"
                 From="1" Duration="0:0:3" />

Анимация начинается с 1, но продолжается только до исходного значения 48. Увеличение размера шрифта происходит медленнее, потому что продолжительность анимации составляет те же три секунды.

В следующей анимации свойство FontSize увеличивается от своего текущего значения до 144 за три секунды:

<DoubleAnimation ...
                 To="144" Duration="0:0:3" />

Я говорю о «текущем значении», потому что это значение не обязательно совпадает с предшествующим анимации значением 48. Щелкните на кнопке, и пока шрифт в TextBlock продолжает увеличиваться в размерах, щелкните еще раз. Каждый последующий щелчок завершает текущую анимацию и начинает ее заново от текущего значения FontSize. С каждым новым щелчком скорость увеличения замедляется, потому что продолжительность анимации составляет те же три секунды.

Можно предположить, что класс DoubleAnimation определяет свойства To и From с типом double. Это почти верно - на самом деле они относятся к типу double с поддержкой null (nullable), и null является значением по умолчанию. Так DoubleAnimation определяет, были ли заданы эти свойства.

Также можно использовать значение By:

<DoubleAnimation ...
                 By="100" Duration="0:0:3" />

Теперь каждое нажатие кнопки запускает анимацию, которая увеличивает FontSize на 100 пикселов за три секунды. Текст просто становится все больше и больше, не возвращаясь к исходной величине.

Попробуйте вернуться к исходной конфигурации и добавить атрибут, который задает свойству AutoReverse значение true. При запуске этой анимации свойство FontSize становится равным 1, увеличивается до 144 за три секунды, потом снова уменьшается до 1 за следующие три секунды, после чего анимация завершается. Все изменение занимает шесть секунд. Задайте FillBehavior значение Stop, и свойство FontSize по истечении этих шести секунд вернется к значению 48, действовавшему до начала анимации.

Также можно задать атрибут RepeatBehavior с AutoReverse или без. Следующая комбинация означает, что в анимации должны быть выполнены три цикла увеличения и уменьшения FontSize:

<DoubleAnimation ...
                 From="1" To="144" Duration="0:0:3"
                 AutoReverse="True"
                 RepeatBehavior="3x" />

Вся анимация продолжается 18 секунд.

Также можно задать RepeatBehavior конкретную продолжительность:

<DoubleAnimation ...
                 From="1" To="144" Duration="0:0:3"
                 AutoReverse="True"
                 RepeatBehavior="0:0:7.5" />

Вся анимация продолжается 7 секунд. Значение FontSize увеличивается от 1 до 144 за первые три секунды, уменьшается от 144 до 1 за следующие три секунды, а потом начинает расти снова, но останавливается. Итоговое значение FontSize равно 73,5. Также можно задать RepeatBehavior значение Forever, и тогда анимация будет повторяться вечно (или по крайней мере до тех пор, пока вам не надоест и вы не завершите программу).

Запуск анимации можно отложить при помощи свойства BeginTime:

<DoubleAnimation ...
                 From="1" To="144" Duration="0:0:3"
                 BeginTime="0:0:0.5" />

При щелчке на кнопке полторы секунды не происходит ничего, но затем TextBlock уменьшается до размера 1 пиксел и начинает расширяться. Анимация завершается через 4,5 секунды после щелчка на кнопке.

Даже при всех вариациях все анимации, рассмотренные до настоящего момента, были линейными. Свойство FontSize всегда увеличивается или уменьшается линейно на заданное количество пикселов в секунду. Простой способ создания нелинейной анимации основан на присваивании значения свойству EasingFunction, определенному классом DoubleAnimation. Оформите свойство в формате элемента свойства и задайте один из 11 классов, производных от EasingFunctionBase. Пример использования класса ElasticEase:

<DoubleAnimation Storyboard.TargetProperty="FontSize"
                 Storyboard.TargetName="txb"
                 EnableDependentAnimation="True"
                 From="1" To="144" Duration="0:0:3">
    <DoubleAnimation.EasingFunction>
        <ElasticEase />
    </DoubleAnimation.EasingFunction>
</DoubleAnimation>
            

Обязательно опробуйте этот вариант и понаблюдайте за эффектом. С увеличением шрифт TextBlock выходит за пределы 144-пиксельного размера, затем уменьшается до величины ниже 144 пикселов и пару раз проходит туда и обратно, пока не остановится на значении To.

EasingFunctionBase определяет свойство EasingMode, наследуемое всеми 11 производными классами. По умолчанию используется значение перечисляемого типа EasingMode.EaseOut, при котором анимация начинается линейно, а специальный эффект применяется в конце анимации. В режиме EaseIn эффект применяется в начале анимации, а в режиме EaseInOut - в начале и в конце.

Некоторые классы, производные от EasingFunctionBase, определяют собственные свойства для дополнительной настройки. ElasticEase определяет свойство Oscillations (целое число со значением по умолчанию 3, определяющее количество «колебаний») и свойство Springiness типа double, также имеющее значение по умолчанию 3. Чем меньше значение Springiness, тем более ярко выражен эффект. Опробуйте следующий фрагмент:

<DoubleAnimation Storyboard.TargetProperty="FontSize"
                 Storyboard.TargetName="txb"
                 EnableDependentAnimation="True"
                 From="1" To="144" Duration="0:0:3">
    <DoubleAnimation.EasingFunction>
        <ElasticEase Springiness="0" Oscillations="10" />
    </DoubleAnimation.EasingFunction>
</DoubleAnimation>

Вскоре мы рассмотрим программу для экспериментов с разными функциями реалистичной анимации.

Ранее я уже упоминал о том, что объект анимации (такой, как DoubleAnimation) должен быть потомком Storyboard. Интересно, что классы Storyboard и DoubleAnimation являются одноранговыми в иерархии классов:

Object
    DependencyObject
        Timeline
            Storyboard
            DoubleAnimation
            ...

Storyboard определяет свойство Children типа TimelineCollection, вложенные свойства TargetName и TargetProperty, а также методы для приостановки и продолжения анимации. DoubleAnimation определяет свойства From, To, By, EnableDependentAnimation и EasingFunction.

Все остальные свойства, встречавшиеся ранее - AutoReverse, BeginTime, Duration, FillBehavior и RepeatBehavior, - определяются классом Timeline; это означает, что их можно задать в Storyboard, чтобы определить поведение всех потомков Storyboard. Timeline также определяет свойство с именем SpeedRatio:

<DoubleAnimation Storyboard.TargetProperty="FontSize"
                 Storyboard.TargetName="txb"
                 EnableDependentAnimation="True"
                 From="1" To="144" Duration="0:0:3"
                 SpeedRatio="10" />

С заданным коэффициентом SpeedRatio анимация ускоряется в 10 раз! Задавать свойство SpeedRatio для объекта DoubleAnimation, конечно, можно, но гораздо чаще оно задается для объекта Storyboard, чтобы его значение распространялось ко всем дочерним анимациям в этом объекте. Свойство SpeedRatio можно использовать для точной настройки скорости анимации без изменения отдельных значений Duration или для отладки сложных наборов анимаций. Например, присваивание SpeedRatio значения 0,1 замедляет анимацию и позволяет лучше проследить за происходящим.

Класс Timeline также определяет событие Completed, которое можно задать либо для Storyboard, либо для DoubleAnimation для получения оповещений о завершении анимации. Анимацию также можно определить полностью в коде. Файл XAML для проекта SimpleAnimationCode содержит панель Grid с девятью элементами Button, совместно использующими один обработчик Click. В файле XAML не встречается ни Storyboard, ни DoubleAnimation:

<Page
    x:Class="WinRTTestApp.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinRTTestApp">

    <Page.Resources>
        <Style TargetType="Button">
            <Setter Property="Content" Value="Запустить!" />
            <Setter Property="Margin" Value="12" />
            <Setter Property="FontSize" Value="48" />
            <Setter Property="VerticalAlignment" Value="Center" />
            <Setter Property="HorizontalAlignment" Value="Center" />
        </Style>
    </Page.Resources>

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Grid HorizontalAlignment="Center" VerticalAlignment="Center">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <Button Grid.Row="0" Grid.Column="0" Click="Button_Click" />
            <Button Grid.Row="0" Grid.Column="1" Click="Button_Click" />
            <Button Grid.Row="0" Grid.Column="2" Click="Button_Click" />
            <Button Grid.Row="1" Grid.Column="0" Click="Button_Click" />
            <Button Grid.Row="1" Grid.Column="1" Click="Button_Click" />
            <Button Grid.Row="1" Grid.Column="2" Click="Button_Click" />
            <Button Grid.Row="2" Grid.Column="0" Click="Button_Click" />
            <Button Grid.Row="2" Grid.Column="1" Click="Button_Click" />
            <Button Grid.Row="2" Grid.Column="2" Click="Button_Click" />
        </Grid>
    </Grid>

</Page>

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

using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Animation;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            DoubleAnimation animation = new DoubleAnimation
            {
                EnableDependentAnimation = true,
                To = 96,
                Duration = new Duration(new TimeSpan(0, 0, 1)),
                AutoReverse = true,
                RepeatBehavior = new RepeatBehavior(3)
            };

            Storyboard.SetTarget(animation, sender as Button);
            Storyboard.SetTargetProperty(animation, "FontSize");

            Storyboard storyboard = new Storyboard();
            storyboard.Children.Add(animation);
            storyboard.Begin();
        }
    }
}

В предшествующем определении DoubleAnimation в XAML вложенные свойства Storyboard.TargetName и Storyboard.TargetProperty обозначают объект и свойство, к которым применяется анимация. В коде дело обстоит немного иначе: для задания имени свойства используется тот же статический метод Storyboard.SetTargetProperty, но для объекта используется метод Storyboard.SetTarget (а не Storyboard.SetTargetName), задающий целевой объект, а не имя XAML целевого объекта. Если целевым объектом является элемент TextBlock, определяемый в XAML с именем «txtblk», то вызов SetTarget() будет выглядеть так:

Storyboard.SetTarget(animation, txtblk);

Указывается имя переменной объекта, а не текстовое имя. В приведенном примере кода я задал целевым объектом кнопку, генерирующую событие Click.

Так же обратите внимание на способ задания свойства Duration. Вариант с использованием TimeSpan является самым распространенным, но Duration также содержит два статических свойства: Automatic (одна секунда в данном контексте) и Forever (не рекомендуется, потому что анимация становится бесконечно медленной). По умолчанию используется значение Automatic; это удобно в том случае, если вы забудете задать его.

Так как изменение каждого свойства FontSize влияет на размер каждого объекта Button, панель Grid должна пересчитывать ширину и высоту своих ячеек. Интересно запустить все анимации одновременно, чтобы понаблюдать за изменением размеров Grid.

Пройди тесты
Лучший чат для C# программистов