Анимация WPF Frame для вставки / вывода страниц не работает должным образом с триггерами данных - PullRequest
2 голосов
/ 14 июля 2020

Цель

В моем приложении я пытаюсь создать «однооконное» приложение в WPF. Поэтому содержимое фрейма (показанного ниже) устанавливается через привязки данных. Чтобы оживить этот процесс, я вызываю свойства AnimateFrameOutToLeft - и AnimateFrameInToLeft - из ViewModel. Это должно создать эффект «скольжения» новых страниц. Этапы процесса изменения содержимого кадра (в ViewModel):

  1. AnimateFrameOutToLeft = true; AnimateFrameOutToLeft = false;
  2. Изменить содержимое кадра
  3. AnimateFrameInToLeft = true; AnimateFrameInToLeft = false;

Проблема

В этом порядке (относительно триггеров данных) анимация «скольжения» страницы не показывается. Непрозрачность кадра равна 0. При измененном порядке анимация "скольжения" страницы не отображается. Почему это так? И как я могу это решить?

Подходы

  • Я отлаживал триггеры данных с настраиваемыми присоединенными свойствами: все они запускаются правильно
  • Я отлаживал анимации (CompleteEvent ) с настраиваемыми прикрепленными свойствами: работает должным образом
  • Изменен порядок триггеров данных: при втором запуске AnimateFrameOutToLeft анимация не отображается

ShellView.xaml

<Frame Margin="0" Background="White" Content="{Binding FrameContent, Mode=OneWay}" Focusable="False">
    <Frame.Style>
        <Style TargetType="{x:Type Frame}">
            <Style.Triggers>
                <DataTrigger Binding="{Binding AnimateFrameInToLeft}" Value="True">
                    <DataTrigger.EnterActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <ThicknessAnimation Duration="0:0:0.25" Storyboard.TargetProperty="Margin" From="50,0,0,0" To="0" FillBehavior="HoldEnd"/>
                                <DoubleAnimation Duration="0:0:0.25" Storyboard.TargetProperty="Opacity" From="0" To="1" FillBehavior="HoldEnd"/>
                            </Storyboard>
                        </BeginStoryboard>
                    </DataTrigger.EnterActions>
                </DataTrigger>
                <DataTrigger Binding="{Binding AnimateFrameOutToLeft}" Value="True">
                    <DataTrigger.EnterActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <ThicknessAnimation Duration="0:0:0.25" Storyboard.TargetProperty="Margin" From="0" To="0,0,50,0" FillBehavior="HoldEnd"/>
                                <DoubleAnimation Duration="0:0:0.25" Storyboard.TargetProperty="Opacity" From="1" To="0" FillBehavior="HoldEnd"/>
                            </Storyboard>
                        </BeginStoryboard>
                    </DataTrigger.EnterActions>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Frame.Style>
   ...
</Frame>

Edit (ссылка на проблемы привязки из ControlTemplate)

ShellView.xaml

<Window ...>
    <Window.InputBindings>
        <KeyBinding Key="Esc" Command="{Binding KeyPressedCommand}" CommandParameter="Esc"/>
    <!-- this binding works -->
    </Window.InputBindings>
    <Grid>
        <Frame Margin="0" Background="White" Content="{Binding FrameContent, Mode=OneWay}" Focusable="False">
            <Frame.ContentTemplate>
                <ItemContainerTemplate>
                    <ContentControl Content="{Binding}">
                        <ContentControl.Style>
                            <Style TargetType="{x:Type ContentControl}">
                                <Setter Property="RenderTransform">
                                    <Setter.Value>
                                        <TranslateTransform/>
                                    </Setter.Value>
                                </Setter>
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding Path=DataContext.AnimateFrameToLeft, RelativeSource={RelativeSource AncestorType={x:Type local:ShellView}}}"  Value="True">
                                <!-- this binding does not work -->
...

Свойство AnimateFrameToLeft определяется в том же классе, что и команда выше

1 Ответ

1 голос
/ 14 июля 2020

Предыдущая анимация блокирует следующую анимацию (или анимированные свойства) из-за FillBehavior.HoldEnd.

Другая ошибка заключается в том, как вы манипулируете свойством Frame.Margin. Обратите внимание, что если вы хотите сдвинуться влево, ваш ThicknessAnimation должен анимировать Margin от 0 до -50,0,0,0 (вместо 0,0,50,0). Это потому, что положительное значение FrameworkElement.Margin влияет только на окружающие элементы, а не на сам элемент. Это означает, что, поскольку справа от Frame нет соседнего элемента, вы не увидите никакого эффекта (в противном случае правый сосед был бы сдвинут вправо на 50 пикселей). Чтобы повлиять на Frame, вам нужно будет использовать отрицательный Margin, чтобы «перетащить» его. Рекомендуемый подход - анимировать TranslateTransform вместо толщины поля.

Также обратите внимание, что анимация самого Frame может быть не лучшим решением. Если вы покажете элементы управления навигацией на Frame, было бы довольно странно видеть, как все движется туда-сюда. Вместо этого вы должны анимировать Frame.Content напрямую.

Версия 1 (рекомендуется)

DataTrigger основывается на состоянии свойства. Он запускается при изменении состояния и выполняет DataTrigger.EnterActions. Как только состояние вернется в исходное состояние, будет выполнено DataTrigger.ExitActions. «Блокировка значения свойства», удерживаемая анимацией входа (которая использует FillBehavior.HoldEnd), не влияет на анимацию выхода. Вы должны использовать автоматическое отслеживание состояния c с помощью DataTrigger и переместить вторую скользящую анимацию в коллекцию DataTrigger.ExitActions.

В следующем примере Frame.ContentTemplate используется для анимации содержание, а не сам Frame. Вместо ThicknessAnimation в этом примере используется DoubleAnimation для анимации свойства ContentControl.RenderTransform кадра (для которого установлено значение TranslateTransform):

Запуск анимации

private async void ChangePage_OnButtonClick(object sender, RoutedEventArgs e)
{
  // Slide current Frame.Content out to the left
  AnimateFrameOutToLeft = true;

  
  await Task.Delay(5000);

  // Slide new Frame.Content in from left to right left
  AnimateFrameOutToLeft = false;
}

Определение анимации

<Frame x:Name="Frame" NavigationUIVisibility="Visible">
  <Frame.ContentTemplate>
    <ItemContainerTemplate>
      <ContentControl Content="{Binding}">
        <ContentControl.Style>
          <Style TargetType="ContentControl">
            <Setter Property="RenderTransform">
              <Setter.Value>
                <TranslateTransform />
              </Setter.Value>
            </Setter>

            <Style.Triggers>
              <DataTrigger Binding="{Binding AnimateFrameOutToLeft}" Value="True">
                <DataTrigger.EnterActions>
                  <BeginStoryboard>
                    <Storyboard>
                      <DoubleAnimation Storyboard.TargetProperty="RenderTransform.(TranslateTransform.X)" 
                                       Duration="0:0:0.25"
                                       To="-50" 
                                       FillBehavior="HoldEnd" />
                      <DoubleAnimation Storyboard.TargetProperty="Opacity"
                                       Duration="0:0:0.25" 
                                       From="1" To="0"
                                       FillBehavior="HoldEnd" />
                    </Storyboard>
                  </BeginStoryboard>
                </DataTrigger.EnterActions>

                <DataTrigger.ExitActions>
                  <BeginStoryboard>
                    <Storyboard>
                      <DoubleAnimation Storyboard.TargetProperty="RenderTransform.(TranslateTransform.X)"
                                       Duration="0:0:0.25"
                                       To="0" 
                                       FillBehavior="HoldEnd" />
                        <DoubleAnimation Storyboard.TargetProperty="Opacity"
                                         Duration="0:0:0.25" 
                                         From="0" To="1"
                                         FillBehavior="HoldEnd" />
                    </Storyboard>
                  </BeginStoryboard>
                </DataTrigger.ExitActions>
              </DataTrigger>
            </Style.Triggers>
          </Style>
        </ContentControl.Style>
      </ContentControl>
    </ItemContainerTemplate>
  </Frame.ContentTemplate>
</Frame>

Версия 2

Свойства Frame.Margin и Frame.Opacity все еще заблокированы предыдущей анимацией, которая все еще выполняется из-за настройки FillBehavior.HoldEnd. Вы должны остановить предыдущую анимацию перед выполнением следующей на временной шкале.

Дайте начальному BeginStoryboard имя, например, SlideOutStoryboard. Затем добавьте StopStoryboard, который нацелен на прежний BeginStoryboard, в коллекцию EnterActions выдвижного триггера:

<Frame.Style>
  <Style TargetType="{x:Type Frame}">
    <Style.Triggers>
      <DataTrigger Binding="{Binding AnimateFrameOutToLeft}" Value="True">
        <DataTrigger.EnterActions>
          <BeginStoryboard x:Name="SlideOutStoryboard">
            <Storyboard>
              <ThicknessAnimation Duration="0:0:0.25" Storyboard.TargetProperty="Margin" 
                                  From="0" To="-50,0,0,0" 
                                  FillBehavior="HoldEnd" />
              <DoubleAnimation Duration="0:0:0.25" Storyboard.TargetProperty="Opacity" 
                               From="1" To="0"
                               FillBehavior="HoldEnd" />
            </Storyboard>
          </BeginStoryboard>
        </DataTrigger.EnterActions>
      </DataTrigger>

      <DataTrigger Binding="{Binding AnimateFrameInToLeft}" Value="True">
        <DataTrigger.EnterActions>

          <!-- Stop the previous BeginStoryBoard "SlideOutStoryboard" -->
          <StopStoryboard BeginStoryboardName="SlideOutStoryboard" />

          <BeginStoryboard>
            <Storyboard>
              <ThicknessAnimation Duration="0:0:0.25" Storyboard.TargetProperty="Margin" 
                                  From="-50,0,0,0" To="0,0,0,0" 
                                  FillBehavior="HoldEnd" />
              <DoubleAnimation Duration="0:0:0.25" Storyboard.TargetProperty="Opacity" 
                               From="0" To="1"
                               FillBehavior="HoldEnd" />
            </Storyboard>
          </BeginStoryboard>
        </DataTrigger.EnterActions>
      </DataTrigger>
    </Style.Triggers>
  </Style>
</Frame.Style>

Version 3

Это расширяющая реализация Frame. Он используется как стандартный Frame, за исключением того, что автоматически анимирует Content при навигации (или Content изменение):

public class AnimatedFrame : Frame
{
  private bool IsAnimating { get; set; }
  private UIElement NextContent { get; set; }
  private UIElement PreviousContent { get; set; }
  private Action PreviousContentTransformCleanupDelegate { get; set; }
  private Action NextContentTransformCleanupDelegate { get; set; }

  public AnimatedFrame() => this.Navigating += OnNavigating;

  private void OnNavigating(object sender, NavigatingCancelEventArgs e)
  {
    if (this.IsAnimating 
      || !(e.Content is UIElement nextContent 
        && this.Content is UIElement))
    {
      return;
    }

    e.Cancel = true;
    this.PreviousContent = this.Content as UIElement;
    this.NextContent = nextContent;
    AnimateToNextContent();
  }

  private void AnimateToNextContent()
  {
    PrepareAnimation();
    StartPreviousContentAnimation();
  }

  private void PrepareAnimation()
  {
    this.IsAnimating = true;


    Transform originalPreviousContentTransform = this.PreviousContent.RenderTransform;
    this.PreviousContent.RenderTransform = new TranslateTransform(0, 0);
    this.PreviousContentTransformCleanupDelegate =
      () => this.PreviousContent.RenderTransform = originalPreviousContentTransform;


    Transform originalNextContentTransform = this.NextContent.RenderTransform;
    this.NextContent.RenderTransform = new TranslateTransform(0, 0);
    this.NextContentTransformCleanupDelegate = () => this.NextContent.RenderTransform = originalNextContentTransform;
  }

  private void StartPreviousContentAnimation()
  {
    var unloadAnimation = new Storyboard();
    DoubleAnimation slideOutAnimation = CreateSlideOutAnimation();
    unloadAnimation.Children.Add(slideOutAnimation);

    DoubleAnimation fadeOutAnimation = CreateFadeOutAnimation();
    unloadAnimation.Children.Add(fadeOutAnimation);
    unloadAnimation.Completed += StartNextContentAnimation_OnPreviousContentAnimationCompleted;

    unloadAnimation.Begin();
  }

  private void StartNextContentAnimation_OnPreviousContentAnimationCompleted(object sender, EventArgs e)
  {
    this.Content = this.NextContent;

    var loadAnimation = new Storyboard();
    DoubleAnimation slideInAnimation = CreateSlideInAnimation();
    loadAnimation.Children.Add(slideInAnimation);

    DoubleAnimation fadeInAnimation = CreateFadeInAnimation();
    loadAnimation.Children.Add(fadeInAnimation);
    loadAnimation.Completed += Cleanup_OnAnimationsCompleted;

    loadAnimation.Begin();
  }

  private void Cleanup_OnAnimationsCompleted(object sender, EventArgs e)
  {
    this.PreviousContentTransformCleanupDelegate.Invoke();
    this.NextContentTransformCleanupDelegate.Invoke();
    this.IsAnimating = false;
  }

  private DoubleAnimation CreateFadeOutAnimation()
  {
    var fadeOutAnimation = new DoubleAnimation(1, 0, new Duration(TimeSpan.FromMilliseconds(250)), FillBehavior.HoldEnd)
      {BeginTime = TimeSpan.Zero};
    Storyboard.SetTarget(fadeOutAnimation, this.PreviousContent);
    Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(nameof(UIElement.Opacity)));
    return fadeOutAnimation;
  }

  private DoubleAnimation CreateSlideOutAnimation()
  {
    var slideOutAnimation = new DoubleAnimation(
        0,
        -50,
        new Duration(TimeSpan.FromMilliseconds(250)),
        FillBehavior.HoldEnd)
      {BeginTime = TimeSpan.Zero};

    Storyboard.SetTarget(slideOutAnimation, this.PreviousContent);
    Storyboard.SetTargetProperty(
      slideOutAnimation,
      new PropertyPath(
        $"{nameof(UIElement.RenderTransform)}.({nameof(TranslateTransform)}.{nameof(TranslateTransform.X)})"));
    return slideOutAnimation;
  }

  private DoubleAnimation CreateFadeInAnimation()
  {
    var fadeInAnimation = new DoubleAnimation(0, 1, new Duration(TimeSpan.FromMilliseconds(250)), FillBehavior.HoldEnd);
    Storyboard.SetTarget(fadeInAnimation, this.NextContent);
    Storyboard.SetTargetProperty(fadeInAnimation, new PropertyPath(nameof(UIElement.Opacity)));
    return fadeInAnimation;
  }

  private DoubleAnimation CreateSlideInAnimation()
  {
    var slideInAnimation = new DoubleAnimation(
      -50,
      0,
      new Duration(TimeSpan.FromMilliseconds(250)),
      FillBehavior.HoldEnd);

    Storyboard.SetTarget(slideInAnimation, this.NextContent);
    Storyboard.SetTargetProperty(
      slideInAnimation,
      new PropertyPath(
        $"{nameof(UIElement.RenderTransform)}.({nameof(TranslateTransform)}.{nameof(TranslateTransform.X)})"));
    return slideInAnimation;
  }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...