Ok comment @Squidward @Athari is actually correct: Mahapps.Metro has the necessary controls with several predefined animations. But let's write such control independently.
We face two problems. First, we must remember the old content before it disappears, in order to correctly show it during the animation. Secondly, to display both old and new content, we need two ContentPresenter
's.
To do this, use the intermediate control, which will contain these same two ContentPresenter
's.
So, create a new control, let's call it AnimatableContentPresenter
. We create custom control, not UserControl
. (This is done through Project → Add → New Item ... → WPF → Custom Control (WPF) in Visual Studio.)
For starters, the code with comments.
// объявляем, что в шаблоне должна быть предоставлена анимация, которую мы запустим [TemplatePart(Name = "PART_Animation", Type = typeof(Storyboard))] public class AnimatableContentPresenter : Control { static AnimatableContentPresenter() { DefaultStyleKeyProperty.OverrideMetadata( typeof(AnimatableContentPresenter), new FrameworkPropertyMetadata(typeof(AnimatableContentPresenter))); } Storyboard animation; // текущая анимация bool isAnimationRunning = false; #region dp object Content, on change OnContentChanged public object Content { get { return (object)GetValue(ContentProperty); } set { SetValue(ContentProperty, value); } } public static readonly DependencyProperty ContentProperty = DependencyProperty.Register( "Content", typeof(object), typeof(AnimatableContentPresenter), new PropertyMetadata(OnContentChangedStatic)); static void OnContentChangedStatic( DependencyObject d, DependencyPropertyChangedEventArgs e) { var self = (AnimatableContentPresenter)d; self.OnContentChanged(e.OldValue, e.NewValue); } #endregion #region dp object PreviousContent public object PreviousContent { get { return (object)GetValue(PreviousContentProperty); } set { SetValue(PreviousContentProperty, value); } } public static readonly DependencyProperty PreviousContentProperty = DependencyProperty.Register( "PreviousContent", typeof(object), typeof(AnimatableContentPresenter)); #endregion // когда Content поменяется... void OnContentChanged(object oldContent, object newContent) { if (isAnimationRunning) animation?.Stop(); // ... запомним старый Content в PreviousContent PreviousContent = oldContent; // и перезапустим анимацию if (animation != null) { animation.Begin(); isAnimationRunning = true; } } // при появлении шаблона, вычитаем из него анимацию public override void OnApplyTemplate() { base.OnApplyTemplate(); if (animation != null) animation.Completed -= OnAnimationCompleted; if (isAnimationRunning) { // TODO: начать новую анимацию там, где предыдущая завершилась? animation?.Stop(); } animation = (Storyboard)Template.FindName("PART_Animation", this); if (animation != null) // подпишемся на завершение анимации animation.Completed += OnAnimationCompleted; } // по окончанию анимации... private void OnAnimationCompleted(object sender, EventArgs e) { // выбросим старый контент PreviousContent = null; // сбросим эффект анимации animation.Remove(); isAnimationRunning = false; } }
Now we need a template in Themes\Generic.xaml
(most likely, the master of creating a new control has put it to you already).
<Style TargetType="{x:Type local:AnimatableContentPresenter}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:AnimatableContentPresenter}"> <!-- несколько трюков с layout manager'ом, чтобы избежать умножения через конвертер --> <Grid Name="Root" ClipToBounds="True"> <Grid HorizontalAlignment="Left"> <!-- ширина вдвое больше Root --> <Grid.ColumnDefinitions> <ColumnDefinition Width="{Binding ActualWidth, ElementName=Root}"/> <ColumnDefinition Width="{Binding ActualWidth, ElementName=Root}"/> </Grid.ColumnDefinitions> <!-- растянем на всю ширину --> <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.ColumnSpan="2"> <!-- старый контент --> <ContentPresenter Content="{TemplateBinding PreviousContent}" Style="{x:Null}" Width="{Binding ActualWidth, ElementName=Root}"/> <!-- текущий контент --> <ContentPresenter Content="{TemplateBinding Content}" Style="{x:Null}" Width="{Binding ActualWidth, ElementName=Root}"/> <!-- анимируемая распорка --> <Grid Width="{Binding ActualWidth, ElementName=Root}" Name="Strut"> <Grid.Resources> <Storyboard x:Key="Animation" x:Name="PART_Animation"> <DoubleAnimation Storyboard.TargetName="Strut" Storyboard.TargetProperty="Width" From="0" Duration="0:0:0.4"> <DoubleAnimation.EasingFunction> <ExponentialEase EasingMode="EaseInOut" Exponent="1.2"/> </DoubleAnimation.EasingFunction> </DoubleAnimation> </Storyboard> </Grid.Resources> </Grid> </StackPanel> </Grid> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style>
Great, most of the work is done. Now we need to present it as a style for something — for example, for ContentControl
. (This is ContentPresenter
like ContentPresenter
'ab is a bit higher level.)
Put in our window (or application resources) style:
<Style TargetType="ContentControl" x:Key="LeftToRightAnimatedContentControl"> <Setter Property="ContentTemplate"> <Setter.Value> <DataTemplate> <local:AnimatableContentPresenter Content="{Binding}"/> </DataTemplate> </Setter.Value> </Setter> </Style>
We are experiencing.
<ContentControl Content="{Binding}" Style="{StaticResource LeftToRightAnimatedContentControl}"/>
We get the following animation:
