WPF анимация движения

Все исходники /  Язык программирования C# /  OS Windows /  Desktop /  WPF программирование / WPF анимация движения

Анимация движения кнопок

Задача

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

Выбор контейнера

Для перемещения кнопки (или любого другого элемента) по плоской форме достаточно двух координат Left по горизонтали и Top по вертикали. В силу философии компоновки WPF позиционированием своих детей ведают контейнеры и поэтому элементы не имеют свойств Left и Top. Класс Button, соответственно, также не имеет свойств абсолютного позиционирования.

Чтобы кнопки перемещались точно в указанные координаты, в качестве контейнера, выберем панель Canvas. Canvas автоматически не позиционирует свои дочерние элементы, но позволяет указывать для них абсолютные места размещения. Статический методы Canvas.SetLeft(элемент, координата-X), Canvas.SetTop(элемент, координата-Y) перемещают элементы в любую позицию в пределах канвы.

Построение каркаса класса

Метод анимационного перемещения кнопки создадим в отдельном классе. Таким образом, используя принцип инкапсуляции, значительно уменьшим количество кода в файле главного окна приложения.

Построим скелет класса, назовём его Moving: Листинг скелета класса:
static class Moving
{
    public static void MoveTo(FrameworkElement fe, double x, double y)
    {
	    ...
    }
}

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

Разработка кода

По условию нет необходимости создавать длительную анимацию, поэтому код упростим применением метода UIElement.BeginAnimation(…), который наследует класс Button. Один метод с параметрами для Left координаты, аналогичный метод для Top координаты. BeginAnimation(...) работает в отдельном потоке, не прерывая основной. Два метода запустятся почти одновременно и управляемый элемент поедет по прямой до точки с координатами x и y.

Чтобы скорость была постоянной, длительность анимации должна быть пропорциональна расстоянию перемещения. Перед началом движения необходимо вычислять длину гипотенузы (катеты – Left->Left1 и Top->Top1) и делить её на значение скорости. Так мы получаем время движения в пути, обеспечивая константную скорость на любые дистанции.

Вот что у нас получилось, полный листинг класса Moving:
static class Moving
{
    public static void MoveTo(FrameworkElement fe, double x, double y)
    {
        // Значение скорость условные единицы в секунду.
        double speed = 300;
  
        // Получение начальных координат
        double leftInit = Canvas.GetLeft(fe);
        double topInit = Canvas.GetTop(fe);
 
        // Вычисление катетов. Нас интересует только расстояние,
        // поэтому используем модули значений катетов.
        double X = Math.Abs(x - leftInit);
        double Y = Math.Abs(y - topInit);
        double quart = Math.Sqrt(X * X + Y * Y);

        // Время для данного расстояния с указанной скоростью.
        double time = quart / speed;

        var left = new DoubleAnimation
        {
            From = leftInit,
            To = x,
            Duration = new Duration(TimeSpan.FromSeconds(time))
        };

        var top = new DoubleAnimation
        {
            From = topInit,
            To = y,
            Duration = new Duration(TimeSpan.FromSeconds(time))
        };

        fe.BeginAnimation(Canvas.LeftProperty, left);
        fe.BeginAnimation(Canvas.TopProperty, top);
    }
}

Анимация движения червяка

Задача

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

Разработка алгоритма движения элемента

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

Напишем скелет-алгоритм класса движения червяка в направлении вращения часовой стрелки:
class WormAnimation
{
    public WormAnimation(FrameworkElement parent, FrameworkElement fe, int framerate){ }

    // Ширина увеличивается до правого края.
    // Высота равна первоначальному размеру.
    private void Step1(){ }
    private void Step1_Completed(object sender, EventArgs e)
    {
        Step2();
    }

    // Занимает крайнее правое положение.
    // Уменьшается по ширине до размера первоначальной высоты. 
    private void Step2(){ }
    private void Step2_Completed(object sender, EventArgs e)
    {
        Step3();
    }

    // Закрепляет ширину равную первоначальной высоте.
    // Растягивается по высоте вниз.
    private void Step3(){ }
    private void Step3_Completed(object sender, EventArgs e)
    {
        Step4();
    }

    // Занимает крайнее нижнее положение.
    // Уменьшается до первоначальной высоты.
    private void Step4(){ }
    private void Step4_Completed(object sender, EventArgs e)
    {
        Step5();
    }

    // Высота закрепляется равной первоначальной.
    // Ширина увеличивается до крайнего левого положения.
    private void Step5(){ }
    private void Step5_Completed(object sender, EventArgs e)
    {
        Step6();
    }

    // Закрепляется в крайнем левом положении.
    // Ширина уменьшается до размера первоначальной высоты.
    private void Step6(){}
    private void Step6_Completed(object sender, EventArgs e)
    {
        Step7();
    }

    // Ширина закрепляется равной высоте.
    // Растягивается до верхнего положения.
    private void Step7(){ }
    private void Step7_Completed(object sender, EventArgs e)
    {
        Step8();
    }

    // Занимает крайнее верхнее положение.
    // Высота стягивается до первоначальной высоты.
    private void Step8(){ }
    private void _8_Step_Completed(object sender, EventArgs e)
    {
        LetsGo();
    }

    public void LetsGo()
    {
        Step1();
    }
}

Разработка метода управления и создание кода класса

Внутри контейнера Grid элементы обычно позиционируются по столбцам и строкам. Если не создавать таблицу, можно точно управлять расположением детей Grid изменением свойства Margin. Данное свойство определяется как структура Thickness, имеющая double значения Left, Top, Right, Bottom. Эти значения устанавливают размер поля с каждой стороны элемента, соответственно названию.

Но Margin имеет относительные величины и всегда используется вместе с выравниванием HorizontalAlignment и VerticalAlignment. В этом случае, например, если HorizontalAlignment равен Left отсчёт координат по горизонтали от левого края, если равен Right, отсчёт начинается от правого края. Аналогично и с вертикальным выравниванием. Если не учитывать эти выравнивающие факторы, при движении элементов, возможно возникновение непредсказуемых артефактов.

Теперь все факторы учтены, каркас класса создан, наполняем кодом методы WormAnimation:
class WormAnimation
{
    private readonly Storyboard storyboard = new Storyboard();
    private readonly FrameworkElement parent;
    private readonly FrameworkElement worm;
    private readonly double wormFirstHeight;
    private double time = 0.8;

    public WormAnimation(FrameworkElement parent, FrameworkElement fe, int framerate)
    {
        this.parent = parent;
        worm = fe;
        wormFirstHeight = worm.Height;

        // Частота кадров.
        Timeline.SetDesiredFrameRate(storyboard, framerate);
    }

    // Ширина увеличивается до правого края.
    // Высота равна первоначальному размеру.
    private void Step1()
    {
        worm.Height = wormFirstHeight;

        var width = WidthAnimation(worm.ActualWidth, parent.ActualWidth);
        storyboard.Children.Add(width);

        storyboard.Completed += Step1_Completed;
        storyboard.Begin(worm);
    }

    private void Step1_Completed(object sender, EventArgs e)
    {
        storyboard.Children.Clear();
        storyboard.Completed -= Step1_Completed;
        Step2();
    }


    // Занимает крайнее правое положение.
    // Уменьшается по ширине до размера первоначальной высоты. 
    private void Step2()
    {
        worm.HorizontalAlignment = HorizontalAlignment.Right;

        var width = WidthAnimation(worm.ActualWidth, wormFirstHeight);
        storyboard.Children.Add(width);

        storyboard.Completed += Step2_Completed;
        storyboard.Begin(worm);
    }


    private void Step2_Completed(object sender, EventArgs e)
    {
        storyboard.Children.Clear();
        storyboard.Completed -= Step2_Completed;
        Step3();
    }



    // Закрепляет ширину равную первоначальной высоте.
    // Растягивается по высоте вниз.
    private void Step3()
    {
        worm.Width = wormFirstHeight;

        var height = HeightAnimation(wormFirstHeight, parent.ActualHeight);
        storyboard.Children.Add(height);

        storyboard.Completed += Step3_Completed;
        storyboard.Begin(worm);
    }

    private void Step3_Completed(object sender, EventArgs e)
    {
        storyboard.Children.Clear();
        storyboard.Completed -= Step3_Completed;
        Step4();
    }


    // Занимает крайнее нижнее положение.
    // Уменьшается до первоначальной высоты.
    private void Step4()
    {
        worm.VerticalAlignment = VerticalAlignment.Bottom;

        var height = HeightAnimation(worm.ActualHeight, wormFirstHeight);
        storyboard.Children.Add(height);

        storyboard.Completed += Step4_Completed;
        storyboard.Begin(worm);
    }

    private void Step4_Completed(object sender, EventArgs e)
    {
        storyboard.Children.Clear();
        storyboard.Completed -= Step4_Completed;
        Step5();
    }


    // Высота закрепляется равной первоначальной.
    // Ширина увеличивается до крайнего левого положения.
    private void Step5()
    {
        worm.Height = wormFirstHeight;

        var width = WidthAnimation(wormFirstHeight, parent.ActualWidth);
        storyboard.Children.Add(width);

        storyboard.Completed += Step5_Completed;
        storyboard.Begin(worm);
    }

    private void Step5_Completed(object sender, EventArgs e)
    {
        storyboard.Children.Clear();
        storyboard.Completed -= Step5_Completed;
        Step6();
    }


    // Закрепляется в крайнем левом положении.
    // Ширина уменьшается до размера первоначальной высоты.
    private void Step6()
    {
        worm.HorizontalAlignment = HorizontalAlignment.Left;

        var width = WidthAnimation(worm.ActualWidth, wormFirstHeight);
        storyboard.Children.Add(width);

        storyboard.Completed += Step6_Completed;
        storyboard.Begin(worm);
    }

    private void Step6_Completed(object sender, EventArgs e)
    {
        storyboard.Children.Clear();
        storyboard.Completed -= Step6_Completed;
        Step7();
    }



    // Ширина закрепляется равной высоте.
    // Растягивается до верхнего положения.
    private void Step7()
    {
        worm.Width = wormFirstHeight;

        var height = HeightAnimation(worm.ActualHeight, parent.ActualHeight);
        storyboard.Children.Add(height);

        storyboard.Completed += Step7_Completed;
        storyboard.Begin(worm);
    }

    private void Step7_Completed(object sender, EventArgs e)
    {
        storyboard.Children.Clear();
        storyboard.Completed -= Step7_Completed;
        Step8();
    }

    // Занимает крайнее верхнее положение.
    // Высота стягивается до первоначальной высоты.
    private void Step8()
    {
        worm.VerticalAlignment = VerticalAlignment.Top;

        var height = HeightAnimation(worm.ActualHeight, wormFirstHeight);
        storyboard.Children.Add(height);

        storyboard.Completed += Step8_Completed;
        storyboard.Begin(worm);
    }

    private void Step8_Completed(object sender, EventArgs e)
    {
        storyboard.Children.Clear();
        storyboard.Completed -= Step8_Completed;

        LetsGo();
    }



    public void LetsGo()
    {
        Init();

        Step1();
    }


    #region Вспомогательные методы 

    private void Init()
    {
        storyboard.Completed -= Step1_Completed;
        storyboard.Completed -= Step2_Completed;
        storyboard.Completed -= Step3_Completed;
        storyboard.Completed -= Step4_Completed;
        storyboard.Completed -= Step5_Completed;
        storyboard.Completed -= Step6_Completed;
        storyboard.Completed -= Step7_Completed;
        storyboard.Completed -= Step8_Completed;

        storyboard.Children.Clear();
    }


    private DoubleAnimation WidthAnimation(double from, double to)
    {
        var width = new DoubleAnimation
        {
            From = from,
            To = to,
            FillBehavior = FillBehavior.HoldEnd,
            Duration = TimeSpan.FromSeconds(time)
        };
        Storyboard.SetTargetName(width, worm.Name);
        Storyboard.SetTargetProperty(width, new PropertyPath(Button.WidthProperty));

        return width;
    }

    private DoubleAnimation HeightAnimation(double from, double to)
    {
        var height = new DoubleAnimation
        {
            From = from,
            To = to,
            FillBehavior = FillBehavior.HoldEnd,
            Duration = TimeSpan.FromSeconds(time)
        };
        Storyboard.SetTargetName(height, worm.Name);
        Storyboard.SetTargetProperty(height, new PropertyPath(Button.HeightProperty));

        return height;
    }

    #endregion
}

Код главного окна

Благодаря тому, что все задачи выполнены в отдельных классах, программный код главного окна приложения получился небольшим и понятным.

Стоит отдельно описать создание группы кнопок. Множество элементов не рационально определять в XAML файле. Гораздо быстрее создать массив элементов класса Button с необходимыми настройками свойств и координатами размещения. Инициализация группы кнопок и объекта класса движения червяка происходят в конструкторе.

Код инициализации приложения:
private readonly WormAnimation wormAnimation;
readonly Button[] buttons = new Button[9];

public MainWindow()
{
    InitializeComponent();

    for(int i = 0; i < buttons.Length; i++)
    {
        buttons[i] = new Button();
        buttons[i].Width = buttons[i].Height = 24;
        buttons[i].Content = i + 1;

        // Автоматический расчёт координат размещения.
        Canvas.SetLeft(buttons[i], 0);
        Canvas.SetTop(buttons[i], 24 * i);
        // Каждая кнопка будет на поле Canvas.
        fieldMoving.Children.Add(buttons[i]);
    }

    // Контейнер - Grid. Червяка представит Border.
    // Скорость анимации 60 кадров в секунду обеспечит плавные движения.
    wormAnimation = new WormAnimation(gridAquarium, earthworm, 60);
}
Код запуска анимаций группы кнопок и имитации движения червяка:
bool horizontal = true;
private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    for (int i = 0; i < buttons.Length; i++)
    {
        double temp = (buttons[i].Width + 1) * i;
        // Смена направления движения при каждом щелчке мыши.
        if (horizontal == true) Moving.MoveTo(buttons[i], temp, 0);
        else Moving.MoveTo(buttons[i], 0, temp);
    }
    // Флаг направления изменяем на противоположный.
    horizontal = !horizontal;
}

private void gridAquarium_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    wormAnimation.LetsGo();
}

Исходник анимации движения элементов

Исходный код написан в MS Visual Studio 2019. Два вида анимаций размещены в закладках контейнера TabControl. Исходник приложения комбинирует инициализацию элементов в XAML и в программном коде. Для работы с исходным кодом рекомендуется изучить данную статью.

Скачать исходник