c# - WPF动画: How to slide between multiple elements within a stackpanel?

标签 c# wpf storyboard

我目前正在寻找一种方法来制作带有多个用户控件的 wpf 窗口,这些控件依次滑入和滑出可见区域,类似于“Stellaris”启动器(这是我能想到的最好的例子找到我想要的):

Stellaris Launcher

我以前使用过this Question成功创建一个带有 2 个滑入和滑出的视觉元素的窗口,但我无法找出超过 2 个元素的最佳实践。

我的计划是使用 4 个 Storyboard,从当前位置滑动到堆栈面板中每个控件的位置,如下所示:

            <Grid Grid.Column="1">
            <Grid.Resources>
                <Storyboard x:Key="SlideFirst">
                    <DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"
                                     From="{Binding CurrentPosition}" To="0" Duration="0:0:0:3" />

                </Storyboard>
                <Storyboard x:Key="SlideSecond">
                    <DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"
                                     From="{Binding CurrentPosition}" To="650" Duration="0:0:0:3" />

                </Storyboard>
                <Storyboard x:Key="SlideThird">
                    <DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"
                                     From="{Binding CurrentPosition}" To="1300" Duration="0:0:0:3" />

                </Storyboard>
                <Storyboard x:Key="SlideForth">
                    <DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"
                                     From="{Binding CurrentPosition}" To="1950" Duration="0:0:0:3" />

                </Storyboard>
            </Grid.Resources>
            <StackPanel>
                <StackPanel.Style>
                    <Style TargetType="StackPanel">
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding CurrentControl}" Value="0">
                                <DataTrigger.EnterActions>
                                    <BeginStoryboard Storyboard="{StaticResource SlideFirst}" />
                                </DataTrigger.EnterActions>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </StackPanel.Style>

但这会导致异常:

InvalidOperationException: Cannot freeze this Storyboard timeline tree for use across threads.

理论上,我可以为每种可能的状态(1->2、1->3、1->4、2->1、2->3 ...)制作一个 Storyboard,但这已经是4 个控件的 12 个 Storyboard。一定有更简单的方法。

如何使用 Storyboard根据当前位置在多个元素之间滑动?

最佳答案

您应该创建一个?用户控制or custom控制that hosts a列表框 to display the sections and the buttons to navigate between them. You then animate the ScrollViewer` 导航到所选部分。

这使得实现动态化,意味着您在添加新部分时不必添加新的动画等。

  1. 创建抽象基类或接口(interface),例如SectionItem 。它是所有部分项(数据模型)的模板,包含通用属性和逻辑。
  2. 每个部分(例如新闻、DLC、模组)都实现此基类/接口(interface),并添加到公共(public)集合中,例如Sections在 View 模型中。
  3. 创建 UserControl或定制Control SectionsViewSectionsView承载导航按钮并将显示各个部分或 SectionItem项目。按下按钮时,将执行该部分的动画导航。
  4. 这个SectionView暴露 ItemsSource绑定(bind)到 View 模型的 Sections 的属性收藏。
  5. 创建 DataTemplate对于每个 SectionItem 。该模板定义了实际部分的外观。这些模板将添加到 ResourceDictionary SectionView的.
  6. ScrollViewer 制作动画ListBoxSectionsView必须实现DependencyProperty例如NavigationOffset 。这是必要的,因为ScrollViewer仅提供修改其偏移量的方法。

创建部分项目

每个项目都必须扩展基类 SectionItem :

SectionItem.cs

public abstract class SectionItem : INotifyPropertyChanged
{
  public SectionItem(Section id)
  {
    this.id = id;
  }

  private Section id;   
  public Section Id
  {
    get => this.id;
    set 
    { 
      this.id = value; 
      OnPropertyChanged();
    }
  }

  private string title;   
  public string Title
  {
    get => this.title;
    set 
    { 
      this.title = value; 
      OnPropertyChanged();
    }
  }

  public event PropertyChangedEventHandler PropertyChanged;
  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }
}

实现实际剖面模型

class DlcSection : SectionItem
{
  public DlcSection(Section id) : base(id)
  {
  }
}

class SettingsSection : SectionItem
{
  public SettingsSection(Section id) : base(id)
  {
  }
}

class NewsSection : SectionItem
{
  public NewsSection(Section id) : base(id)
  {
  }
}

enum用作 SectionItem 的节 ID和CommandParameter

Section.cs

public enum Section
{
  None = 0,
  Dlc,
  Settings,
  News
}

实现SectionsView

SectionsView延伸UserControl (或 Control )并封装 SectionItem 的显示项目及其导航。为了触发导航,它公开了一个路由命令 NavigateToSectionRoutedCommand :

SectionsView.xaml.cs

public partial class SectionsView : UserControl
{
  #region Routed commands

  public static readonly RoutedUICommand NavigateToSectionRoutedCommand = new RoutedUICommand(
    "Navigates to section by section ID which is an enum value of the enumeration 'Section'.",
    nameof(SectionsView.NavigateToSectionRoutedCommand),
    typeof(SectionsView));

  #endregion Routed commands

  public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
    "ItemsSource",
    typeof(IEnumerable),
    typeof(SectionsView),
    new PropertyMetadata(default(IEnumerable)));

  public IEnumerable ItemsSource
  {
    get => (IEnumerable) GetValue(SectionsView.ItemsSourceProperty);
    set => SetValue(SectionsView.ItemsSourceProperty, value);
  }

  public static readonly DependencyProperty NavigationOffsetProperty = DependencyProperty.Register(
    "NavigationOffset",
    typeof(double),
    typeof(SectionsView),
    new PropertyMetadata(default(double), SectionNavigator.OnNavigationOffsetChanged));

  public double NavigationOffset
  {
    get => (double) GetValue(SectionsView.NavigationOffsetProperty);
    set => SetValue(SectionsView.NavigationOffsetProperty, value);
  }

  private ScrollViewer Navigator { get; set; }

  public SectionsView()
  {
    InitializeComponent();

    this.Loaded += OnLoaded;
  }

  private void OnLoaded(object sender, RoutedEventArgs e)
  {
    if (TryFindVisualChildElement(this.SectionItemsView, out ScrollViewer scrollViewer))
    {
      this.Navigator = scrollViewer;
    }
  }

  private static void OnNavigationOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    (d as SectionsView).Navigator.ScrollToVerticalOffset((double) e.NewValue);
  }

  private void NavigateToSection_OnExecuted(object sender, ExecutedRoutedEventArgs e)
  {
    SectionItem targetSection = this.SectionItemsView.Items
      .Cast<SectionItem>()
      .FirstOrDefault(section => section.Id == (Section) e.Parameter);
    if (targetSection == null)
    {
      return;
    }

    double verticalOffset = 0;
    if (this.Navigator.CanContentScroll)
    {
      verticalOffset = this.SectionItemsView.Items.IndexOf(targetSection);
    }
    else
    {
      var sectionContainer =
        this.SectionItemsView.ItemContainerGenerator.ContainerFromItem(targetSection) as UIElement;
      Point absoluteContainerPosition = sectionContainer.TransformToAncestor(this.Navigator).Transform(new Point());
      verticalOffset = this.Navigator.VerticalOffset + absoluteContainerPosition.Y;
    }

    var navigationAnimation = this.Resources["NavigationAnimation"] as DoubleAnimation;
    navigationAnimation.From = this.Navigator.VerticalOffset;
    navigationAnimation.To = verticalOffset;
    BeginAnimation(SectionNavigator.NavigationOffsetProperty, navigationAnimation);
  }

  private void NavigateToSection_OnCanExecute(object sender, CanExecuteRoutedEventArgs e)
  {
    e.CanExecute = e.Parameter is Section;
  }

  private bool TryFindVisualChildElement<TChild>(DependencyObject parent, out TChild resultElement)
    where TChild : DependencyObject
  {
    resultElement = null;
    for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
    {
      DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);

      if (childElement is Popup popup)
      {
        childElement = popup.Child;
      }

      if (childElement is TChild)
      {
        resultElement = childElement as TChild;
        return true;
      }

      if (TryFindVisualChildElement(childElement, out resultElement))
      {
        return true;
      }
    }

    return false;
  }
}

SectionsView.xaml

<UserControl x:Class="SectionsView">
  <UserControl.Resources>
   
    <!-- Animation can be changed, but name must remain the same -->
    <DoubleAnimation x:Key="NavigationAnimation" Storyboard.TargetName="Root" Storyboard.TargetProperty="NavigationOffset"
                     Duration="0:0:0.3">
      <DoubleAnimation.EasingFunction>
        <PowerEase EasingMode="EaseIn" Power="5" />
      </DoubleAnimation.EasingFunction>
    </DoubleAnimation>

    <!-- DataTemplates for different section items -->
    <DataTemplate DataType="{x:Type local:DlcSection}">
      <Grid Height="200" Background="Green">
        <TextBlock Text="{Binding Title}" FontSize="18" />
      </Grid>
    </DataTemplate>

    <DataTemplate DataType="{x:Type local:SettingsSection}">
      <Grid Height="200" Background="OrangeRed">
        <TextBlock Text="{Binding Title}" FontSize="18" />
      </Grid>
    </DataTemplate>

    <DataTemplate DataType="{x:Type viewModels:NewsSection}">
      <Grid Height="200" Background="Yellow">
        <TextBlock Text="{Binding Title}" FontSize="18" />
      </Grid>
    </DataTemplate>
  </UserControl.Resources>

  <UserControl.CommandBindings>
    <CommandBinding Command="{x:Static local:SectionNavigator.NavigateToSectionRoutedCommand}"
                    Executed="NavigateToSection_OnExecuted" CanExecute="NavigateToSection_OnCanExecute" />
  </UserControl.CommandBindings>

  <Grid>
    <StackPanel>
      <Button Content="Navigate" Command="{x:Static local:SectionNavigator.NavigateToSectionRoutedCommand}"
              CommandParameter="{x:Static local:Section.News}" />
      <Button Content="Navigate" Command="{x:Static local:SectionNavigator.NavigateToSectionRoutedCommand}"
              CommandParameter="{x:Static local:Section.Dlc}" />
      <Button Content="Navigate" Command="{x:Static local:SectionNavigator.NavigateToSectionRoutedCommand}"
              CommandParameter="{x:Static local:Section.Settings}" />

      <!-- ScrollViewer.CanContentScroll is set to False to enable smooth scrolling for large (high) items -->
      <ListBox x:Name="SectionItemsView" 
               Height="250"
               ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:SectionNavigator}, Path=Sections}"
               ScrollViewer.CanContentScroll="False" />
    </StackPanel>
  </Grid>
</UserControl>

用法

ViewModel.cs

class ViewModel : INotifyPropertyChanged
{
  public ObservableCollection<SectionItem> Sections { get; set; }

  public ViewModel()
  {
    this.Sections = new ObservableCollection<SectionItem>
    {
      new NewsSection(Section.News) {Title = "News"},
      new DlcSection(Section.Dlc) {Title = "DLC"},
      new SettingsSection(Section.Settings) {Title = "Settings"}
    };
  }
}

MainWindow.xaml

<Window>
  <Window.Resources>
    <ViewModel />
  </Window.Resources>

  <SectionsView ItemsSource="{Binding Sections}" />
</Window>

关于c# - WPF动画: How to slide between multiple elements within a stackpanel?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62991128/

相关文章:

c# - WPF 应用程序中的异常记录,MVVMLight SimpleIoC 隐藏异常而不是抛出它们

ios - QuickDialog 与 Storyboard 的集成

ios - Collection View定位像instagram app swift

c# - 如何获取 System.Xml.Linq.XNode 的名称?

由 C# 数组选择

c# - 自定义授权属性在 WebAPI 中不起作用

c# - 在c#中运行单元测试时如何处理InvalidoperationException

c# - 当某些条件为 True 时,对 WPF 中的数据网格单元格进行动画处理?

wpf - 将样式从资源应用到 ListView.ItemContainerStyle

ios - 呈现 View 时导航栏不显示?