Советы и рекомендации по анимации

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

Общие проблемы

Анимация расположения полосы прокрутки или ползунка замораживает их

Если вы анимируете положение полосы прокрутки или ползунка с помощью анимации, у которой свойство FillBehavior имеет значение HoldEnd (значение по умолчанию), то пользователь больше не сможет двигать полосу прокрутки или ползунок. Это происходит из-за того, что, хотя анимация завершена, она по-прежнему переопределяет базовое значение целевого свойства. Чтобы анимация не переопределяла текущее значение свойства, удалите ее или задайте для ее свойства FillBehavior значение Stop. Дополнительные сведения и пример см. в разделе Установка значения свойства после его анимации с помощью раскадровки.

Анимация выходных данных анимации ничего не делает

Невозможна анимация объекта, являющегося выходным объектом другой анимации. Например, если используется ObjectAnimationUsingKeyFrames для анимации свойства Fill прямоугольника Rectangle с RadialGradientBrush на SolidColorBrush, вы не можете анимировать никакие свойства RadialGradientBrush или SolidColorBrush.

Невозможно изменить значение свойства после его анимации

В некоторых случаях может получиться так, что вы не сможете изменить значение свойства после его анимации, даже после завершения анимации. Это происходит из-за того, что, хотя анимация завершена, она по-прежнему переопределяет базовое значение свойства. Чтобы анимация не переопределяла текущее значение свойства, удалите ее или задайте для ее свойства FillBehavior значение Stop. Дополнительные сведения и пример см. в разделе Установка значения свойства после его анимации с помощью раскадровки.

Изменение временной шкалы ничего не делает

Хотя большинство свойств временной шкалы Timeline можно анимировать и привязать к данным, изменение значений свойств активной временной шкалы Timeline не дает видимого эффекта. Причина заключается в том, что при начале Timeline система расчета времени делает копию Timeline и использует ее для создания объекта Clock. Изменение исходного объекта не влияет на сделанную системой копию.

Чтобы временная шкала Timeline отражала изменения, нужно создать ее часы повторно и заменить ими ранее созданные часы. Автоматическое обновление часов не поддерживается. Ниже показано несколько способов применения изменений временной шкалы.

  • Если временная шкала является раскадровкой Storyboard или принадлежит ей, для показа изменений раскадровку нужно применить повторно с помощью BeginStoryboard или метода Begin. Это имеет побочный эффект в виде перезапуска анимации. В коде можно использовать метод Seek для возврата раскадровки в предыдущее положение.

  • Если анимация применяется непосредственно к свойству с помощью метода BeginAnimation, нужно вызвать метод BeginAnimation еще раз и передать ему измененную анимацию.

  • При работе непосредственно на уровне часов создайте и примените новый набор часов и используйте его для замены предыдущего набора созданных часов.

Дополнительные сведения о временных шкалах и часах см. в разделе Общие сведения об анимации и системе управления временем.

FillBehavior.Stop не работает должным образом

Иногда при установке свойства FillBehavior в значение Stop видимых изменений не происходит, например, когда одна анимация "переходит" в другую анимацию, поскольку ее параметр HandoffBehavior имеет значение SnapshotAndReplace.

В следующем примере создаются Canvas, Rectangle и TranslateTransform. Объект TranslateTransform анимируется для перемещения прямоугольника Rectangle вокруг Canvas.

<Canvas Width="600" Height="200">
  <Rectangle 
    Canvas.Top="50" Canvas.Left="0" 
    Width="50" Height="50" Fill="Red">
    <Rectangle.RenderTransform>
      <TranslateTransform 
        x:Name="MyTranslateTransform" 
        X="0" Y="0" />
    </Rectangle.RenderTransform>
  </Rectangle>
</Canvas>

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

FillBehavior="Stop" и HandoffBehavior с несколькими анимациями

Иногда кажется, что анимация игнорирует свое свойство FillBehavior при ее замене второй анимацией. Рассмотрим следующий пример, где создаются два объекта Storyboard, которые используются для анимации того же объекта TranslateTransform, что был показан в предыдущем примере.

Первая раскадровка Storyboard, B1, анимирует свойство X объекта TranslateTransform от 0 до 350, что перемещает прямоугольник на 350 пикселей вправо. Когда анимация доходит до конца и прекращает воспроизведение, свойство X возвращается в исходное значение 0. В результате прямоугольник перемещается вправо на 350 пикселей, а затем перепрыгивает обратно в исходное положение.

<Button Content="Start Storyboard B1">
  <Button.Triggers>
    <EventTrigger RoutedEvent="Button.Click">
      <BeginStoryboard>
        <Storyboard x:Name="B1">
          <DoubleAnimation 
            Storyboard.TargetName="MyTranslateTransform"
            Storyboard.TargetProperty="X"
            From="0" To="350" Duration="0:0:5"
            FillBehavior="Stop"
            />
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Button.Triggers>
</Button>

Вторая раскадровка Storyboard, B2, также анимирует свойство X того же объекта TranslateTransform. Так как устанавливается только свойство To анимации в этой раскадровке Storyboard, анимация использует текущее значение анимируемого свойства в качестве своего начального значения.


<!-- Animates the same object and property as the preceding
     Storyboard. -->
<Button Content="Start Storyboard B2">
  <Button.Triggers>
    <EventTrigger RoutedEvent="Button.Click">
      <BeginStoryboard>
        <Storyboard x:Name="B2">
          <DoubleAnimation 
            Storyboard.TargetName="MyTranslateTransform"
            Storyboard.TargetProperty="X"
            To="500" Duration="0:0:5" 
            FillBehavior="Stop" />
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Button.Triggers>
</Button>

При нажатии второй кнопки, пока воспроизводится первая раскадровка Storyboard, можно ожидать следующего поведения:

  1. Первая раскадровка заканчивается и отправляет прямоугольник в исходное положение, так как свойство FillBehavior анимации имеет значение Stop.

  2. Вторая раскадровка вступает в силу и анимируется от текущей позиции, которая теперь имеет значение 0, до 500.

Но этого не происходит. Прямоугольник не прыгает обратно; он продолжает перемещаться вправо. Это происходит потому, что вторая анимация использует текущее значение первой анимации в качестве своего начального значения и анимируется от этого значения до 500. Когда вторая анимация заменяет первую из-за использования SnapshotAndReplaceHandoffBehavior, свойство FillBehavior первой анимации не имеет значения.

FillBehavior и завершенное событие

В следующем примере показан другой сценарий, когда StopFillBehavior не имеет видимого эффекта. В этом примере снова используется раскадровка для анимации свойства X объекта TranslateTransform от 0 до 350. Однако на этот раз в примере выполняется регистрация для события Completed.

<Button Content="Start Storyboard C">
  <Button.Triggers>
    <EventTrigger RoutedEvent="Button.Click">
      <BeginStoryboard>
        <Storyboard Completed="StoryboardC_Completed">
          <DoubleAnimation 
            Storyboard.TargetName="MyTranslateTransform"
            Storyboard.TargetProperty="X"
            From="0" To="350" Duration="0:0:5"
            FillBehavior="Stop" />
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Button.Triggers>
</Button>

Обработчик события Completed запускает другую раскадровку Storyboard, которая анимирует то же самое свойство от его текущего значения до 500.

private void StoryboardC_Completed(object sender, EventArgs e)
{

    Storyboard translationAnimationStoryboard =
        (Storyboard)this.Resources["TranslationAnimationStoryboardResource"];
    translationAnimationStoryboard.Begin(this);
}
Private Sub StoryboardC_Completed(ByVal sender As Object, ByVal e As EventArgs)

    Dim translationAnimationStoryboard As Storyboard = CType(Me.Resources("TranslationAnimationStoryboardResource"), Storyboard)
    translationAnimationStoryboard.Begin(Me)
End Sub

Ниже приведена разметка, определяющая вторую раскадровку Storyboard в качестве ресурса.

<Page.Resources>
  <Storyboard x:Key="TranslationAnimationStoryboardResource">
    <DoubleAnimation 
      Storyboard.TargetName="MyTranslateTransform"
      Storyboard.TargetProperty="X"
      To="500" Duration="0:0:5" />
  </Storyboard>
</Page.Resources>

При запуске Storyboard можно ожидать, что свойство X объекта TranslateTransform будет анимироваться от 0 до 350, затем вернется к 0 после завершения (так как его параметр FillBehavior имеет значение Stop), а затем будет анимироваться от 0 до 500. Вместо этого объект TranslateTransform анимируется от 0 до 350, а затем до 500.

Это происходит из-за порядка, в котором WPF создает события, и из-за того, что значения свойства кэшируются и не вычисляются повторно, кроме случаев, когда свойство становится недействительным. Событие Completed обрабатывается первым, так как его активация вызывается корневой временной шкалой (первой раскадровкой Storyboard). В этот момент свойство X все еще возвращает свое анимированное значение, так как оно еще не стало недействительным. Вторая раскадровка Storyboard использует кэшированное значение в качестве своего начального значения и начинает анимацию.

Производительность

Анимация продолжает выполняться после ухода со страницы

При уходе со страницы Page, на которой есть работающие анимации, они продолжают выполняться, пока для Page не будет выполнена сборка мусора. В зависимости от используемой системы навигации страница, с которой ушли, может оставаться в памяти неограниченное количество времени, постоянно потребляя ресурсы на анимацию. Это наиболее заметно, когда страница содержит постоянно выполняющиеся анимации ("фоновые").

По этой причине рекомендуется использовать событие Unloaded для удаления анимаций при уходе со страницы.

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

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

  • Чтобы удалить анимации из конкретного свойства, используйте метод BeginAnimation(DependencyProperty, AnimationTimeline). Укажите в качестве первого параметра анимируемое свойство, а в качестве второго параметра — null. Это удалит из свойства все часы анимации.

Дополнительные сведения о различных способах анимации свойств см. в разделе Общие сведения о методах анимации свойств.

Использование составного HandoffBehavior потребляет системные ресурсы

При применении Storyboard, AnimationTimeline или AnimationClock к свойству с помощью ComposeHandoffBehavior любые объекты Clock, ранее связанные с этим свойством, продолжают потреблять ресурсы системы; система управления временем не удаляет эти часы автоматически.

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

  • Чтобы удалить все часы из свойства, используйте метод ApplyAnimationClock(DependencyProperty, AnimationClock) или BeginAnimation(DependencyProperty, AnimationTimeline) анимированного объекта. Укажите в качестве первого параметра анимируемое свойство, а в качестве второго параметра — null. Это удалит из свойства все часы анимации.

  • Чтобы удалить определенный AnimationClock из списка часов, используйте свойствоController объекта AnimationClock для извлечения ClockController, а затем вызовите метод Remove объекта ClockController. Обычно это делается в обработчике событий Completed для часов. Обратите внимание, что только корневые часы могут управляться через ClockController; свойство Controller дочерних часов возвратит null. Также обратите внимание, что событие Completed не будет вызвано, если эффективная продолжительность часов — forever (не ограничена). В этом случае пользователю потребуется определить, когда нужно вызывать Remove.

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

Дополнительные сведения об объектах часов см. в разделе Общие сведения об анимации и системе управления временем.

См. также