Анимации определенные в XAML в WinRT

62

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

На самом деле запуск анимации по событию Loaded можно выполнить исключительно средствами XAML - для этого используется старое свойство с именем Triggers, унаследованное из WPF. В длинном пути от WPF до Windows Runtime свойство Triggers утратило почти всю свою прежнюю функциональность, но оно все еще может использоваться для запуска Storyboard:

<Page.Triggers>
    <EventTrigger>
        <BeginStoryboard>
           <Storyboard ...>
               ...
           </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
</Page.Triggers>

Элемент свойства Triggers обычно размещается в корневом элементе файла XAML - традиционно ближе к концу файла, но формально элемент свойства Triggers может определяться в любом элементе-предке целевого объекта анимации.

Обратите внимание на теги EventTrigger и BeginStoryboard - это единственный контекст, в котором вы их увидите. У EventTrigger имеется свойство RoutedEvent, но при попытке задать ему какое-либо значение (включая вполне разумные «Loaded» или «Page.Loaded») происходит ошибка времени выполнения. BeginStoryboard может иметь несколько дочерних элементов Storyboard.

Следующая программа похожа на программу по анимации цвета из статьи «Таймеры и анимации в WinRT». Фон Grid и основной цвет (Foreground) поля TextBlock анимируются от черного цвета к белому в разных направлениях. Два объекта ColorAnimation применяются к свойствам Color двух объектов SolidColorBrush:

<Page ...>

    <Grid>
        <Grid.Background>
            <SolidColorBrush x:Name="gridBrush" />
        </Grid.Background>

        <TextBlock Text="Анимация цвета"
                   FontFamily="Calibri"
                   FontSize="102"
                   FontWeight="Bold"
                   VerticalAlignment="Center"
                   HorizontalAlignment="Center">
            <TextBlock.Foreground>
                <SolidColorBrush x:Name="txtblkBrush" />
            </TextBlock.Foreground>
        </TextBlock>
    </Grid>

    <Page.Triggers>
        <EventTrigger>
            <BeginStoryboard>
                <Storyboard RepeatBehavior="Forever"
                            AutoReverse="True">
                    <ColorAnimation Storyboard.TargetName="gridBrush"
                                    Storyboard.TargetProperty="Color"
                                    From="Black" To="White" Duration="0:0:3" />

                    <ColorAnimation Storyboard.TargetName="txtblkBrush"
                                    Storyboard.TargetProperty="Color"
                                    From="White" To="Black" Duration="0:0:3" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Page.Triggers>

</Page>

Вероятно, класс ColorAnimation занимает второе место по популярности среди классов анимации после DoubleAnimation. Возможности его применения ограничиваются свойством Color кистей SolidColorBrush и GradientStop, но эти кисти используются очень часто, поэтому он более универсален, чем может показаться на первый взгляд. Обратите внимание на настройки RepeatBehavior и AutoReverse в Storyboard.

Файл фонового кода не содержит ничего, кроме вызова InitializeComponent в конструкторе страницы. Это означает, что вы можете скопировать файл XAML в область редактора программы XamlCruncher, созданную ранее, удалить атрибут x:Class и запустить анимацию без какой-либо поддержки из программного кода. XamlCruncher (или другая программа редактирования XAML) - удобный инструмент для экспериментов с анимацией.

Также возможна анимация свойств типа Point. Эти свойства встречаются не так часто, но у класса EllipseGeometry имеется свойство Center типа Point. Если вы создаете круг или эллипс, используя Path и EllipseGeometry вместо класса Ellipse, то фигуру можно перемещать по экрану, применяя анимацию к свойству Center. В отличие от анимации Canvas.Left и Canvas.Top, объект Path не обязан находиться в Canvas, а позиция фигуры задается относительно центра (вместо левого верхнего угла).

Однако анимация не может применяться к свойствам X и Y значения Point по отдельности. Point - структура, а не класс; это означает, что Point не может наследовать от DependencyObject, а следовательно, свойства X и Y не поддерживаются свойствами зависимости.

Свойства типа Point также встречаются в некоторых классах, производных от PathSegment: ArcSegment, BezierSegment, LineSegment и QuadraticBezierSegment содержат свойства типа Point. Анимация свойств Point позволяет динамически изменять графические фигуры. Следующая программа применяет аппроксимацию Безье к кругу, а затем анимирует все 13 точек, чтобы круг деформировался в квадрат. Просто чтобы продемонстрировать, что свойство Triggers не обязательно определять в корневом элементе файла XAML, я определил его прямо в Path:

<Page ...>

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Canvas HorizontalAlignment="Center"
                VerticalAlignment="Center">
            <Path Fill="{StaticResource ApplicationPressedForegroundThemeBrush}"
                  Stroke="{StaticResource ApplicationForegroundThemeBrush}" 
                  StrokeThickness="3" >
                <Path.Data>
                    <PathGeometry>
                        <PathFigure x:Name="bezier1" IsClosed="True">
                            <BezierSegment x:Name="bezier2" />
                            <BezierSegment x:Name="bezier3" />
                            <BezierSegment x:Name="bezier4" />
                            <BezierSegment x:Name="bezier5" />
                        </PathFigure>
                    </PathGeometry>
                </Path.Data>

                <Path.Triggers>
                    <EventTrigger>
                        <BeginStoryboard>
                            <Storyboard RepeatBehavior="Forever">
                                <PointAnimation Storyboard.TargetName="bezier1"
                                                Storyboard.TargetProperty="StartPoint"
                                                EnableDependentAnimation="True"
                                                From="0 200" To="0 250"
                                                AutoReverse="True" />

                                <PointAnimation Storyboard.TargetName="bezier2"
                                                Storyboard.TargetProperty="Point1"
                                                EnableDependentAnimation="True"
                                                From="110 200" To="125 125"
                                                AutoReverse="True" />

                                <PointAnimation Storyboard.TargetName="bezier2"
                                                Storyboard.TargetProperty="Point2"
                                                EnableDependentAnimation="True"
                                                From="200 110" To="125 125"
                                                AutoReverse="True" />

                                <PointAnimation Storyboard.TargetName="bezier2"
                                                Storyboard.TargetProperty="Point3"
                                                EnableDependentAnimation="True"
                                                From="200 0" To="250 0"
                                                AutoReverse="True" />

                                <PointAnimation Storyboard.TargetName="bezier3"
                                                Storyboard.TargetProperty="Point1"
                                                EnableDependentAnimation="True"
                                                From="200 -110" To="125 -125"
                                                AutoReverse="True" />

                                <PointAnimation Storyboard.TargetName="bezier3"
                                                Storyboard.TargetProperty="Point2"
                                                EnableDependentAnimation="True"
                                                From="110 -200" To="125 -125"
                                                AutoReverse="True" />

                                <PointAnimation Storyboard.TargetName="bezier3"
                                                Storyboard.TargetProperty="Point3"
                                                EnableDependentAnimation="True"
                                                From="0 -200" To="0 -250"
                                                AutoReverse="True" />

                                <PointAnimation Storyboard.TargetName="bezier4"
                                                Storyboard.TargetProperty="Point1"
                                                EnableDependentAnimation="True"
                                                From="-110 -200" To="-125 -125"
                                                AutoReverse="True" />

                                <PointAnimation Storyboard.TargetName="bezier4"
                                                Storyboard.TargetProperty="Point2"
                                                EnableDependentAnimation="True"
                                                From="-200 -110" To="-125 -125"
                                                AutoReverse="True" />

                                <PointAnimation Storyboard.TargetName="bezier4"
                                                Storyboard.TargetProperty="Point3"
                                                EnableDependentAnimation="True"
                                                From="-200 0" To="-250 0"
                                                AutoReverse="True" />

                                <PointAnimation Storyboard.TargetName="bezier5"
                                                Storyboard.TargetProperty="Point1"
                                                EnableDependentAnimation="True"
                                                From="-200 110" To="-125 125"
                                                AutoReverse="True" />

                                <PointAnimation Storyboard.TargetName="bezier5"
                                                Storyboard.TargetProperty="Point2"
                                                EnableDependentAnimation="True"
                                                From="-110 200" To="-125 125"
                                                AutoReverse="True" />

                                <PointAnimation Storyboard.TargetName="bezier5"
                                                Storyboard.TargetProperty="Point3"
                                                EnableDependentAnimation="True"
                                                From="0 200" To="0 250"
                                                AutoReverse="True" />
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger>
                </Path.Triggers>
            </Path>
        </Canvas>
    </Grid>

</Page>
Анимация полигонов и путей

Анимация пользовательских классов

Да, анимация может применяться к свойствам пользовательских классов. Однако для этого анимируемые свойства должны поддерживаться свойствами зависимости.

Класс PieSlice, производный от Path, рисует сектор (наподобие тех, которые используются в круговых диаграммах). В нем дополнительно определяются свойства Center, Radius, StartAngle (в градусах по часовой стрелке от 12:00) и SweepAngle (в градусах по часовой стрелке от StartAngle):

using System;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;

namespace WinRTTestApp
{
    public class PieSlice : Path
    {
        PathFigure pathFigure;
        LineSegment lineSegment;
        ArcSegment arcSegment;

        static PieSlice()
        {
            CenterProperty = DependencyProperty.Register("Center",
                typeof(Point), typeof(PieSlice),
                new PropertyMetadata(new Point(100, 100), OnPropertyChanged));

            RadiusProperty = DependencyProperty.Register("Radius",
                typeof(double), typeof(PieSlice),
                new PropertyMetadata(100.0, OnPropertyChanged));

            StartAngleProperty = DependencyProperty.Register("StartAngle",
                typeof(double), typeof(PieSlice),
                new PropertyMetadata(0.0, OnPropertyChanged));

            SweepAngleProperty = DependencyProperty.Register("SweepAngle",
                typeof(double), typeof(PieSlice),
                new PropertyMetadata(90.0, OnPropertyChanged));
        }

        public PieSlice()
        {
            pathFigure = new PathFigure { IsClosed = true };
            lineSegment = new LineSegment();
            arcSegment = new ArcSegment { SweepDirection = SweepDirection.Clockwise };
            pathFigure.Segments.Add(lineSegment);
            pathFigure.Segments.Add(arcSegment);

            PathGeometry pathGeometry = new PathGeometry();
            pathGeometry.Figures.Add(pathFigure);

            this.Data = pathGeometry;
            UpdateValues();
        }

        public static DependencyProperty CenterProperty { private set; get; }
        public static DependencyProperty RadiusProperty { private set; get; }
        public static DependencyProperty StartAngleProperty { private set; get; }
        public static DependencyProperty SweepAngleProperty { private set; get; }

        public Point Center
        {
            set { SetValue(CenterProperty, value); }
            get { return (Point)GetValue(CenterProperty); }
        }

        public double Radius
        {
            set { SetValue(RadiusProperty, value); }
            get { return (double)GetValue(RadiusProperty); }
        }

        public double StartAngle
        {
            set { SetValue(StartAngleProperty, value); }
            get { return (double)GetValue(StartAngleProperty); }
        }

        public double SweepAngle
        {
            set { SetValue(SweepAngleProperty, value); }
            get { return (double)GetValue(SweepAngleProperty); }
        }

        static void OnPropertyChanged(DependencyObject obj,
                                      DependencyPropertyChangedEventArgs args)
        {
            (obj as PieSlice).UpdateValues();
        }

        private void UpdateValues()
        {
            pathFigure.StartPoint = this.Center;

            double x = this.Center.X + this.Radius * Math.Sin(Math.PI * this.StartAngle / 180);
            double y = this.Center.Y - this.Radius * Math.Cos(Math.PI * this.StartAngle / 180);
            lineSegment.Point = new Point(x, y);

            x = this.Center.X + this.Radius * Math.Sin(Math.PI * (this.StartAngle +
                                                                  this.SweepAngle) / 180);

            y = this.Center.Y - this.Radius * Math.Cos(Math.PI * (this.StartAngle +
                                                                  this.SweepAngle) / 180);
            arcSegment.Point = new Point(x, y);
            arcSegment.IsLargeArc = this.SweepAngle >= 180;

            arcSegment.Size = new Size(this.Radius, this.Radius);
        }
    }
}

Практически все в этом классе - дополнительная нагрузка, связанная со свойствами зависимости. Исключение составляет только метод UpdateValues(), который играет очень важную роль. Метод UpdateValues() вызывается при изменении любого из четырех свойств. Каждое из четырех свойств может быть целевым для анимации, а это означает, что UpdateValues() может вызываться 60 раз в секунду в течение неопределенного промежутка времени.

В методах, вызываемых с такой частотой, следует внимательно относиться к созданию объектов, требующих выделения памяти из управляемой кучи (GC). С созданием новых значений double и Point проблем нет, потому что они создаются в стеке. Однако реализуется этот метод созданием новых объектов PathFigure, LineSegment и ArcSegment при каждом вызове, потому что при этом выполняется значительная работа по выделению памяти, которую позднее приходится освобождать. Попробуйте организовать повторное использование или кэширование объектов вместо их повторного создания.

Класс PieSlice является частью проекта AnimatedPieSlice. В этот проект входит страница MainPage.xaml, которая создает экземпляр этого класса, инициализирует и применяет к нему анимацию:

<Page ...
    xmlns:local="using:WinRTTestApp">

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <local:PieSlice x:Name="pieSlice"
                        Radius="200"
                        Center="400 400"
                        Stroke="#999"
                        StrokeThickness="3"
                        Fill="LimeGreen" />
    </Grid>

    <Page.Triggers>
        <EventTrigger>
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetName="pieSlice"
                                     Storyboard.TargetProperty="SweepAngle"
                                     EnableDependentAnimation="True"
                                     AutoReverse="True"
                                     RepeatBehavior="Forever"
                                     From="1" To="359" Duration="0:0:3" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Page.Triggers>

</Page>

Результат представляет собой сектор, угловая величина которого изменяется от 1° до 359° и обратно (до бесконечности):

Пример использования пользовательской анимации
Лучший чат для C# программистов