Анимация на основе кадра

75

Наряду с системой анимации, основанной на изменении свойств, WPF предоставляет способ создания анимации на основе кадра, не используя ничего помимо кода. Все, что понадобится — реагировать на событие CompositionTarget.Rendering, которое возбуждается для получения содержимого каждого кадра. Это довольно низкоуровневый подход, который стоит применять только в том случае, когда выясняется, что стандартная модель анимации на основе изменения свойств не подходит для реализации существующего сценария (например, при построении простой игры с прокруткой экрана, создании анимации на основе физических законов либо моделировании таких эффектов, как огонь, снег или пузыри).

Основная техника для построения анимации на базе кадра проста. Нужно просто присоединить обработчик событий к статическому событию CompositionTarget.Rendering. После этого WPF начнет непрерывно вызывать этот обработчик. (До тех пор, пока код отображения выполняется достаточно быстро, WPF будет вызывать его 60 раз в секунду.) В обработчике события визуализации создание и управление элементами в окне полностью возлагается на вас. Другими словами, всей работой понадобится управлять самостоятельно. Когда анимация завершится, обработчик события следует отключить.

Ниже показан достаточно простой пример. Здесь случайное количество кружков падают от верхней к нижней границе Canvas. Они падают с разной (случайно выбранной) скоростью, но по мере движения скорость каждого возрастает в одинаковой степени. Анимация завершается, когда все круги упадут вниз.

В данном примере каждый падающий кружок представлен элементом Ellipse. Специальный класс по имени EllipseInfo сохраняет ссылку на эллипс и отслеживает детали, существенные для его физической модели. В данном случае здесь присутствует только одна единица информации — смещение эллипса по оси Y. (Этот класс может быть легко расширен добавлением скорости по оси X, дополнительной информации относительно ускорения и т.п.)

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

По щелчку на кнопке коллекция очищается, и обработчик события присоединяется к событию CompositionTarget.Rendering. Если эллипс не существует, код визуализации создаст его автоматически. Он создает случайное количество эллипсов (в данном случае — от 20 до 100) и устанавливает для каждого из них одинаковый размер и цвет. Эллипсы помещаются в верхнюю часть Canvas, но их смещение по оси X задается случайным образом.

Ниже показан полный код примера:

<Grid Margin="3">
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"></RowDefinition>
      <RowDefinition></RowDefinition>
    </Grid.RowDefinitions>

    <StackPanel Orientation="Horizontal">
      <Button Margin="3" Padding="3" Click="cmdStart_Clicked">Start</Button>
      <Button Margin="3" Padding="3" Click="cmdStop_Clicked">Stop</Button>
    </StackPanel>
    <Canvas Name="canvas" Grid.Row="1" Margin="3"></Canvas>
  </Grid>
public partial class FrameBasedAnimation : System.Windows.Window
    {

        public FrameBasedAnimation()
        {
            InitializeComponent();
        }

        private bool rendering = false;
        private void cmdStart_Clicked(object sender, RoutedEventArgs e)
        {
            if (!rendering)
            {
                ellipses.Clear();
                canvas.Children.Clear();

                CompositionTarget.Rendering += RenderFrame;
                rendering = true;
            }
        }
        private void cmdStop_Clicked(object sender, RoutedEventArgs e)
        {
            StopRendering();
        }

        private void StopRendering()
        {
            CompositionTarget.Rendering -= RenderFrame;
            rendering = false;            
        }
                
        private List<EllipseInfo> ellipses = new List<EllipseInfo>();
        
        private double accelerationY = 0.1;
        private int minStartingSpeed = 1;
        private int maxStartingSpeed = 50;
        private double speedRatio = 0.1;
        private int minEllipses = 20;
        private int maxEllipses = 100;        
        private int ellipseRadius = 10;
            
        private void RenderFrame(object sender, EventArgs e)
        {
            if (ellipses.Count == 0)
            {
                // Animation just started. Create the ellipses.
                int halfCanvasWidth = (int)canvas.ActualWidth / 2;                
                
                Random rand = new Random();
                int ellipseCount = rand.Next(minEllipses, maxEllipses+1);
                for (int i = 0; i < ellipseCount; i++)
                {
                    Ellipse ellipse = new Ellipse();
                    ellipse.Fill = Brushes.LimeGreen;
                    ellipse.Width = ellipseRadius;
                    ellipse.Height = ellipseRadius;
                    Canvas.SetLeft(ellipse, halfCanvasWidth + rand.Next(-halfCanvasWidth, halfCanvasWidth));
                    Canvas.SetTop(ellipse, 0);
                    canvas.Children.Add(ellipse);

                    EllipseInfo info = new EllipseInfo(ellipse, speedRatio * rand.Next(minStartingSpeed, maxStartingSpeed));
                    ellipses.Add(info);
                }
            }
            else
            {
                for (int i = ellipses.Count-1; i >= 0; i--)                
                {
                    EllipseInfo info = ellipses[i];
                    double top = Canvas.GetTop(info.Ellipse);
                    Canvas.SetTop(info.Ellipse, top + 1 * info.VelocityY);                    

                    if (top >= (canvas.ActualHeight - ellipseRadius*2 - 10))
                    {
                        // This circle has reached the bottom.
                        // Stop animating it.
                        ellipses.Remove(info);
                    }
                    else
                    {
                        // Increase the velocity.
                        info.VelocityY += accelerationY;
                    }

                    if (ellipses.Count == 0)
                    {
                        // End the animation.
                        // There's no reason to keep calling this method
                        // if it has no work to do.
                        StopRendering();
                    }
                }
            }
        }
    }

    public class EllipseInfo
    {        
        public Ellipse Ellipse
        {
            get; set;
        }
                
        public double VelocityY
        {
            get; set;
        }

        public EllipseInfo(Ellipse ellipse, double velocityY)
        {
            VelocityY = velocityY;
            Ellipse = ellipse;
        }
    }
Пример анимации на основе кадра

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

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

Наилучший способ начать работать с анимацией на основе кадра — исследовать довольно подробный пример анимации, входящий в состав WPF SDK.

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