Плавная анимация в WinRT

97

Предположим, у объекта DoubleAnimation свойство From равно 100, свойство To - 500, а продолжительность Duration составляет 5 секунд. По умолчанию анимация DoubleAnimation осуществляется линейно, то есть целевое свойство принимает значения из диапазона от 100 до 500 в линейной зависимости от прошедшего времени:

Время Значение
0 секунд 100
1 180
2 260
3 340
4 420
5 500

Или более наглядно:

Значение = From + Время/Duration x (To - From)

Функции плавной анимации (easing functions) делают анимацию более интересной. Сначала я планировал начать эту тему с демонстрации наследования от EasingFunctionBase для создания пользовательской функции плавной анимации, но по некоторым причинам (я уверен, очень веским!) наследование от EasingFunctionBase запрещено. Если бы это было возможно, то вы могли бы создать собственную функцию плавной анимации, просто переопределяя метод Ease() и реализуя функцию перехода.

Метод Ease() получает аргумент типа double в диапазоне от 0 до 1. Метод возвращает значение типа double. Когда аргумент равен 0, метод возвращает 0; когда аргумент равен 1, метод возвращает 1. Промежуточные значения могут быть любыми. По сути, функция плавной анимации изменяет однородность течения времени, а зависимость между прошедшим временем и значением свойства, к которому применяется анимация, становится нелинейной.

Когда действует функция плавной анимации, прошедшее время нормализуется до значения в диапазоне от 0 до 1 посредством деления на Duration (как в приведенной выше формуле). Для полученного результата вызывается функция Ease, а возвращаемое значение используется в вычислениях:

Значение = From + Ease(Время/Duration) x (To - From)

Например, функция ExponentialEase с используемым по умолчанию значением свойства EasingMode, равным EaseOut, использует следующую передаточную функцию:

t' = (1 - e-Nt) / (1 - e-N)

где t - аргумент функции Ease, t'- результат, а N - значение свойства Exponent. Если N равно 2 (значение по умолчанию), то анимация из приведенной выше таблицы будет выглядеть так:

Время t t' Значение
0 секунд 0,0 0,000 100
1 0,2 0,381 252
2 0,4 0,637 355
3 0,6 0,808 423
4 0,8 0,923 469
5 1,0 1,000 500

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

Программа для тестирования функций плавной анимации

На графике изображена передаточная функция. По горизонтальной оси отсчитываются значения t от 0 до 1, а по вертикальной - значения t' (0 наверху, 1 внизу). Пунктирная линия от левого верхнего угла к правому нижнему изображает линейную, а синяя линия - выбранную передаточную функцию. Точки Polyline назначаются из файла фонового кода многократным вызовом метода Ease выбранного класса плавной анимации. При нажатии кнопки "Показать" маленький красный шарик в левом верхнем углу анимируется по горизонтали с обычной линейной анимацией, а по вертикали - с выбранной функцией плавной анимации. И - кто бы мог подумать? - шарик перемещается точно по графику.

Ниже приведен файл XAML программы. Он начинается с определения анимации красного шарика. Функция плавной анимации назначается из файла фонового кода. Значения To и From настраиваются с учетом радиуса шарика в 6 пикселов (ближе к концу файла):

<Page ...>

    <Page.Resources>
        <Storyboard x:Key="storyboard"
                    FillBehavior="Stop">
            <DoubleAnimation From="-6" To="994"
                             Storyboard.TargetProperty="(Canvas.Left)"
                             Storyboard.TargetName="ball"
                             Duration="0:0:3" />

            <DoubleAnimation x:Name="animation2" From="-6" To="494"
                             Storyboard.TargetProperty="(Canvas.Top)"
                             Storyboard.TargetName="ball"
                             Duration="0:0:3" />
        </Storyboard>
    </Page.Resources>

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <!-- Панель управления -->
        <Grid Grid.Column="0" VerticalAlignment="Center">

            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>

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

            <!-- Функция плавной анимации (заполняется в коде) -->
            <StackPanel Name="easeStp"
                        Grid.Column="0"
                        Grid.Row="0"
                        Grid.RowSpan="3"
                        VerticalAlignment="Center">
                <RadioButton Margin="6"
                             Content="Нет"
                             Checked="RadioButton_Checked_Easing" />
            </StackPanel>

            <!-- Режим плавной анимации -->
            <StackPanel Name="easeMsp"
                        VerticalAlignment="Center"
                        Grid.Column="1"
                        Grid.Row="0"
                        HorizontalAlignment="Center">
                <RadioButton Content="Плавность в"
                             Margin="6"
                             Checked="RadioButton_Checked_ModeChanged">
                    <RadioButton.Tag>
                        <EasingMode>EaseIn</EasingMode>
                    </RadioButton.Tag>
                </RadioButton>

                <RadioButton Margin="6"
                            Content="Плавность от"
                            Checked="RadioButton_Checked_ModeChanged">
                    <RadioButton.Tag>
                        <EasingMode>EaseOut</EasingMode>
                    </RadioButton.Tag>
                </RadioButton>

                <RadioButton Margin="6"
                             Content="Плавность в/от"
                             Checked="RadioButton_Checked_ModeChanged">
                    <RadioButton.Tag>
                        <EasingMode>EaseInOut</EasingMode>
                    </RadioButton.Tag>
                </RadioButton>
            </StackPanel>

            <!-- Свойства плавной анимации (заполняются в коде) -->
            <StackPanel Name="propStp"
                        VerticalAlignment="Center"
                        HorizontalAlignment="Center"
                        Grid.Column="1"
                        Grid.Row="1" />

            <!-- Кнопка "Показать" -->
            <Button Grid.Column="1" Grid.Row="2"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Content="Показать!"
                    Click="Button_Show" />
        </Grid>

        <!-- График использует произвольные координаты и 
             масштабируется по размерам окна -->
        <Viewbox Grid.Column="1">
            <Grid Height="500" Width="1000" Margin="0 230 0 230">

                <!-- Прямоугольный контур -->
                <Polygon Stroke="{StaticResource ApplicationForegroundThemeBrush}"
                         Points="1000 500, 0 500, 0 0, 1000 0"
                         StrokeThickness="3" />

                <Canvas>
                    <!-- Линейная передаточная функция -->
                    <Polyline Points="1000 500, 0 0"
                              Stroke="{StaticResource ApplicationForegroundThemeBrush}"
                              StrokeDashArray="2 2" 
                              StrokeThickness="1"
                              />

                    <!-- Точки, задаваемые в коде на основании
                         функции плавной анимации -->
                    <Polyline Name="poly" Stroke="Blue" StrokeThickness="3" />

                    <!-- Анимированный шарик -->
                    <Ellipse Name="ball" Width="12" Height="12" Fill="LimeGreen" />
                </Canvas>
            </Grid>
        </Viewbox>
    </Grid>

</Page>

Файл фонового кода использует рефлексию для получения всех классов, производных от EasingFunctionBase, и создает элемент RadioButton для каждого класса. При выборе RadioButton снова используется рефлексия - на этот раз для получения конструктора без параметров. Это позволяет создать экземпляр класса. Дополнительное использование рефлексии позволяет программе получить все открытые свойства, определенные конкретным классом, производным от EasingFunctionBase. К счастью, все эти открытые свойства ограничиваются типами int или double, поэтому для каждого из них создается элемент управления Slider.

using System;
using System.Collections.Generic;
using System.Reflection;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Animation;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        EasingFunctionBase ease;

        public MainPage()
        {
            this.InitializeComponent();
            Loaded += Page_Loading;
        }

        private void Page_Loading(object sender, RoutedEventArgs args)
        {
            Type b = typeof(EasingFunctionBase);
            TypeInfo bInfo = b.GetTypeInfo();
            Assembly assembly = bInfo.Assembly;

            // Перебор всех типов Windows Runtime
            foreach (Type type in assembly.ExportedTypes)
            {
                TypeInfo t = type.GetTypeInfo();

                // Создание элемента RadioButton для каждой функции плавной анимации
                if (t.IsPublic &&
                    bInfo.IsAssignableFrom(t) &&
                    type != b)
                {
                    RadioButton radioButton = new RadioButton
                    {
                        Content = type.Name,
                        Tag = type,
                        Margin = new Thickness(6)
                    };

                    radioButton.Checked += RadioButton_Checked_Easing;
                    easeStp.Children.Add(radioButton);
                }
            }

            // Проверка первого элемента управления RadioButton 
            // в панели StackPanel (с меткой "None")
            (easeStp.Children[0] as RadioButton).IsChecked = true;
        }

        private void RadioButton_Checked_Easing(object sender, RoutedEventArgs args)
        {
            RadioButton r = sender as RadioButton;
            Type type = r.Tag as Type;
            ease = null;
            propStp.Children.Clear();

            // Тип равен null только для кнопки "None"
            if (type != null)
            {
                TypeInfo t = type.GetTypeInfo();

                // Поиск конструктора без параметров и создание
                // экземпляра функции плавной анимации
                foreach (ConstructorInfo constructorInfo in t.DeclaredConstructors)
                {
                    if (constructorInfo.IsPublic && constructorInfo.GetParameters().Length == 0)
                    {
                        ease = constructorInfo.Invoke(null) as EasingFunctionBase;
                        break;
                    }
                }

                // Перебор свойств функции плавной анимации
                foreach (PropertyInfo p in t.DeclaredProperties)
                {
                    // Работаем только со свойствами типов int и double
                    if (p.PropertyType != typeof(int) &&
                        p.PropertyType != typeof(double))
                    {
                        continue;
                    }

                    // Создание TextBlock для имени свойства
                    TextBlock txtblk = new TextBlock
                    {
                        Text = p.Name + ":"
                    };
                    propStp.Children.Add(txtblk);

                    // Создание Slider для значения свойства
                    Slider slider = new Slider
                    {
                        Minimum = 0,
                        Maximum = 10,
                        Width = 138,
                        Tag = p
                    };

                    if (p.PropertyType == typeof(int))
                    {
                        slider.StepFrequency = 1;
                        slider.Value = (int)p.GetValue(ease);
                    }
                    else
                    {
                        slider.StepFrequency = 0.1;
                        slider.Value = (double)p.GetValue(ease);
                    }

                    // Определение обработчика события изменения Slider
                    slider.ValueChanged += (sliderSender, sliderArgs) =>
                    {
                        Slider sliderChanging = sliderSender as Slider;
                        PropertyInfo propertyInfo = sliderChanging.Tag as PropertyInfo;

                        if (p.PropertyType == typeof(int))
                            p.SetValue(ease, (int)sliderArgs.NewValue);
                        else
                            p.SetValue(ease, (double)sliderArgs.NewValue);

                        DrawGraph();
                    };
                    propStp.Children.Add(slider);
                }
            }

            // Инициализация переключателей EasingMode
            foreach (UIElement c in easeMsp.Children)
            {
                RadioButton erm = c as RadioButton;
                erm.IsEnabled = ease != null;

                erm.IsChecked =
                    ease != null &&
                    ease.EasingMode == (EasingMode)erm.Tag;
            }

            DrawGraph();
        }

        private void RadioButton_Checked_ModeChanged(object sender, RoutedEventArgs args)
        {
            RadioButton radioButton = sender as RadioButton;
            ease.EasingMode = (EasingMode)radioButton.Tag;
            DrawGraph();
        }

        private void Button_Show(object sender, RoutedEventArgs args)
        {
            // Задание выбранной функции плавной анимации и запуск анимации
            Storyboard storyboard = this.Resources["storyboard"] as Storyboard;
            (storyboard.Children[1] as DoubleAnimation).EasingFunction = ease;
            storyboard.Begin();
        }

        private void DrawGraph()
        {
            poly.Points.Clear();

            if (ease == null)
            {
                poly.Points.Add(new Point(0, 0));
                poly.Points.Add(new Point(1000, 500));
                return;
            }

            for (decimal t = 0; t <= 1; t += 0.01m)
            {
                double x = (double)(1000 * t);
                double y = 500 * ease.Ease((double)t);
                poly.Points.Add(new Point(x, y));
            }
        }
    }
}

В наборе функций плавной анимации есть некоторая избыточность: QuadraticEase, CubicEase, QuarticEase и QuinticEase являются частными случаями PowerEase и их можно продублировать, используя PowerEase со свойством Power, равным 2, 3, 4 и 5 соответственно.

Функция ElasticEase возвращает значения за пределами диапазона от 0 до 1. То же относится и к BackEase. Так как передаточная функция может возвращать значения меньше 0 и больше 1, анимация может принимать значения за пределами диапазона от From до To. Для многих свойств это не создает проблем, но в отдельных случаях может произойти исключение. Например, свойство Opacity не может принимать значения меньше 0 или больше 1. Свойствам Width и Height не могут присваиваться отрицательные значения, а свойство FontSize должно быть строго больше 0. Применение к этим свойствам анимации, приводящей к присваиванию недействительного значения, вызывает исключение времени выполнения.

И хотя функции плавной анимации обычно используются для замедления и ускорения анимаций, у них также имеются нетривиальные применения. Например, SineEase использует следующую передаточную функцию при задании EasingMode значения по умолчанию EaseOut:

t' = sin(π/2 * t)

В первой четверти синус быстро растет, затем постепенно замедляется. Для EaseIn используется первая четверть косинуса, нормализованная для изменения в диапазоне от 0 до 1:

t' = 1 - cos(π/2 * t)

Возрастание начинается медленно, потом постепенно ускоряется.

SineEase со свойством EasingMode, равным EaseInOut, изменяется в первой половине косинусоиды, нормализованная для изменения в диапазоне от 0 до 1:

t' = (1 - cos(π/2 * t))/2

Функция начинает расти медленно, ускоряется, затем снова замедляется. Если использовать вариант SineEase в режиме EaseInOut с применением DoubleAnimation к свойству Canvas.Left объекта Ellipse, задав при этом свойству AutoReverse значение True, а свойству RepeatBehavior - значение Forever, получается эффект, напоминающий Маятник: изменение происходит медленно непосредственно перед обратным ходом и в начале его, но ускоряется к середине.

Если применить к Canvas.Top аналогичную анимацию, сдвинутую на половину цикла, объект можно перемещать по кругу, как продемонстрировано в следующей программе:

<Page ...>

    <Page.Resources>
        <Storyboard x:Key="storyboard" SpeedRatio="3">
            <DoubleAnimation Storyboard.TargetName="greenBall"
                             
                             From="-400" To="400" Duration="0:0:2.0"
                             RepeatBehavior="Forever" AutoReverse="True" 
                             Storyboard.TargetProperty="(Canvas.Left)">
                <DoubleAnimation.EasingFunction>
                    <SineEase EasingMode="EaseInOut" />
                </DoubleAnimation.EasingFunction>
            </DoubleAnimation>

            <DoubleAnimation Storyboard.TargetName="greenBall"
                             AutoReverse="True"
                             Storyboard.TargetProperty="(Canvas.Top)"
                             From="-400" To="400" Duration="0:0:2.0"
                             BeginTime="0:0:1"
                             RepeatBehavior="Forever">
                <DoubleAnimation.EasingFunction>
                    <SineEase EasingMode="EaseInOut" />
                </DoubleAnimation.EasingFunction>
            </DoubleAnimation>
        </Storyboard>
    </Page.Resources>

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Canvas HorizontalAlignment="Center"
                Margin="0 0 34 34"
                VerticalAlignment="Center">
            <Ellipse Name="greenBall"
                     Height="34" Width="34"
                     Fill="LimeGreen" />
        </Canvas>
    </Grid>

</Page>

Объект Canvas выравнивается по центру, но смещается на размер эллипса; это означает, что точка (0, 0) относительно Canvas расположена на 24 пиксела влево и на 24 пиксела выше центра окна. Объект Ellipse использует значения Canvas.Left и Canvas.Top по умолчанию и располагается в центре. Анимации перемещают Ellipse на 350 пикселов влево и вправо, вверх и вниз.

Обратите внимание: у второй анимации задана 1-секундная задержка BeginTime, так что в первую секунду после загрузки программы первая анимация перемещает эллипс горизонтально от -350 пикселов до 0, после чего начинает работать вторая анимация, которая перемещает шарик вертикально от -350 до 0 одновременно с горизонтальным перемещением от 0 до 350. И хотя функции плавной анимации предназначены для ускорения и замедления анимаций, Ellipse перемещается по кругу с постоянной угловой скоростью.

Разработка современных мобильных приложений осложнена наличием большого количества мобильных платформ: Android, iOS, Windows Runtime и т.д. Мобильная разработка Promwad предлагает создание качественных программных продуктов на всех популярных мобильных осях. Сервис Promwad Mobile отлично подойдет, если вам нужно приложение для быстрого запуска на App Store или Google Play.

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