Окей, комментарий @Squidward @Athari на самом деле правильный: в Mahapps.Metro есть нужный контрол с несколькими предопределёнными анимациями. Но давайте напишем такой контрол самостоятельно.
Перед нами стоят две проблемы. Во-первых, мы должны запомнить старый контент перед тем, как он исчезнет, чтобы правильно показать его на время анимации. Во-вторых, для показа одновременно и старого, и нового контента нам нужны два ContentPresenter'а.
Для этого воспользуемся промежуточным контролом, который будет содержать эти самые два ContentPresenter'а.
Итак, создаём новый контрол, назовём его AnimatableContentPresenter. Мы создаёт custom control, а не UserControl. (Это делается через Проект → Add → New Item... → WPF → Custom Control (WPF) в Visual Studio.)
Для начала, код с комментариями.
// объявляем, что в шаблоне должна быть предоставлена анимация, которую мы запустим
[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;
}
}
Теперь нам нужен шаблон в Themes\Generic.xaml (его вам уже, скорее всего, положил мастер создания нового контрола).
<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>
Отлично, большая часть работы готова. Теперь нам нужно представить это как стиль для чего-нибудь — например, для ContentControl'а. (Это штука наподобие ContentPresenter'аб немного более высокоуровневая.)
Кладём в нашем окне (или ресурсах приложения) стиль:
<Style TargetType="ContentControl" x:Key="LeftToRightAnimatedContentControl">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<local:AnimatableContentPresenter Content="{Binding}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
Испытываем.
<ContentControl Content="{Binding}"
Style="{StaticResource LeftToRightAnimatedContentControl}"/>
Получаем вот такую анимацию:
