【问题标题】:WPF Frame animation to slide in/out pages is not working properly with data triggers用于滑入/滑出页面的 WPF 帧动画无法与数据触发器一起正常工作
【发布时间】:2020-11-03 15:28:05
【问题描述】:

目标

对于我的应用程序,我尝试在 WPF 中创建一个“单窗口”应用程序。因此框架的内容(如下所示)是通过数据绑定设置的。为了使这个过程动画化,我从 ViewModel 中触发 AnimateFrameOutToLeft- 和 AnimateFrameInToLeft- 属性。这应该会产生新页面“滑入”或“滑出”的效果。 更改框架内容的过程步骤(在 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>

编辑(从 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-Property 定义在与上述命令相同的类中

【问题讨论】:

    标签: c# wpf xaml animation mvvm


    【解决方案1】:

    由于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)持有的“属性值锁定”不会影响退出动画。
    您应该利用DataTrigger 的自动状态跟踪,并将第二个滑入动画移动到DataTrigger.ExitActions 集合中。

    在以下示例中,Frame.ContentTemplate 用于动画内容,而不是 Frame 本身。此示例使用DoubleAnimation 来为框架的ContentControl.RenderTransform 属性(设置为TranslateTransform)设置动画,而不是ThicknessAnimation:

    触发动画

    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。然后在滑入式触发器的EnterActions 集合中添加一个StopStoryboard,它以前BeginStoryboard 为目标:

    <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>
    

    版本 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;
      }
    }
    

    【讨论】:

    • 我在绑定到 AnimateFrameInToLeft-Property 时遇到问题。有没有比命名窗口和通过{Binding DataContext.AnimateFrameInToLeft, ElementName=name} 访问属性更好的解决方案?我已经尝试使用相关来源,但没有奏效。
    • 这个属性是在MainWindow上定义的?然后使用{Binding RelativeSource={RelativeSource AncestorType=MainWindow}, Path=AnimateFrameInToLeft}。
    • 属性定义在ShellView(父窗口)的DataContext中。我试过这个:{Binding Path=AnimateFrameToLeft, RelativeSource={RelativeSource AncestorType={x:Type local:ShellView}}}。但是结果还是这样:System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='ProjectNamespace.ShellView', AncestorLevel='1''. BindingExpression:Path=AnimateFrameToLeft; DataItem=null; target element is 'ContentControl' (Name=''); target property is 'NoTarget' (type 'Object')
    • 如果 DataContext 与 Frame 相同,则可以使用 {Binding AnimateFrameInToLeft}。否则你必须明确引用DataContext:{Binding Path=DataContext.AnimateFrameToLeft, RelativeSource={RelativeSource AncestorType={x:Type local:ShellView}}}
    • 我添加了第三个版本。源自Frame 的快速实现。当您直接设置新的Content 或使用NavigationService 时,动画会自动触发。
    猜你喜欢
    • 2011-10-29
    • 2016-02-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-02-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多