diff --git a/samples/ControlCatalog/Converter/MathSubtractConverter.cs b/samples/ControlCatalog/Converter/MathSubtractConverter.cs
new file mode 100644
index 0000000000..009a56c0ec
--- /dev/null
+++ b/samples/ControlCatalog/Converter/MathSubtractConverter.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace ControlCatalog.Converter;
+
+public class MathSubtractConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return (double)value - (double)parameter;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException();
+ }
+}
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index facce2aa82..f3b52428ca 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -145,6 +145,9 @@
+
+
+
diff --git a/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml
new file mode 100644
index 0000000000..a410577d27
--- /dev/null
+++ b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 8
+
+
+
+
+ The TransitioningContentControl control allows you to show a page transition whenever the Content changes.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml.cs b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml.cs
new file mode 100644
index 0000000000..2851a6c890
--- /dev/null
+++ b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml.cs
@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+ public partial class TransitioningContentControlPage : UserControl
+ {
+ public TransitioningContentControlPage()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs b/samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs
new file mode 100644
index 0000000000..b092a07f4a
--- /dev/null
+++ b/samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs
@@ -0,0 +1,309 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using Avalonia.Styling;
+using Avalonia.VisualTree;
+using MiniMvvm;
+
+namespace ControlCatalog.ViewModels
+{
+ public class TransitioningContentControlPageViewModel : ViewModelBase
+ {
+ public TransitioningContentControlPageViewModel()
+ {
+ var assetLoader = AvaloniaLocator.Current.GetService();
+
+ var images = new string[]
+ {
+ "delicate-arch-896885_640.jpg",
+ "hirsch-899118_640.jpg",
+ "maple-leaf-888807_640.jpg"
+ };
+
+ foreach (var image in images)
+ {
+ var path = $"avares://ControlCatalog/Assets/{image}";
+ Images.Add(new Bitmap(assetLoader.Open(new Uri(path))));
+ }
+
+ SetupTransitions();
+
+ SelectedTransition = PageTransitions[1];
+ SelectedImage = Images[0];
+ }
+
+ public List PageTransitions { get; } = new List();
+
+ public List Images { get; } = new List();
+
+
+ private Bitmap? _SelectedImage;
+
+ ///
+ /// Gets or Sets the selected image
+ ///
+ public Bitmap? SelectedImage
+ {
+ get { return _SelectedImage; }
+ set { this.RaiseAndSetIfChanged(ref _SelectedImage, value); }
+ }
+
+
+ private PageTransition _SelectedTransition;
+
+ ///
+ /// Gets or sets the transition to play
+ ///
+ public PageTransition SelectedTransition
+ {
+ get { return _SelectedTransition; }
+ set { this.RaiseAndSetIfChanged(ref _SelectedTransition, value); }
+ }
+
+
+
+ private bool _ClipToBounds;
+
+ ///
+ /// Gets or sets if the content should be clipped to bounds
+ ///
+ public bool ClipToBounds
+ {
+ get { return _ClipToBounds; }
+ set { this.RaiseAndSetIfChanged(ref _ClipToBounds, value); }
+ }
+
+
+ private int _Duration = 500;
+
+ ///
+ /// Gets or Sets the duration
+ ///
+ public int Duration
+ {
+ get { return _Duration; }
+ set
+ {
+ this.RaiseAndSetIfChanged(ref _Duration , value);
+ SetupTransitions();
+ }
+ }
+
+ private void SetupTransitions()
+ {
+ if (PageTransitions.Count == 0)
+ {
+ PageTransitions.AddRange(new[]
+ {
+ new PageTransition("None"),
+ new PageTransition("CrossFade"),
+ new PageTransition("Slide horizontally"),
+ new PageTransition("Slide vertically"),
+ new PageTransition("Composite"),
+ new PageTransition("Custom")
+ });
+ }
+
+ PageTransitions[1].Transition = new CrossFade(TimeSpan.FromMilliseconds(Duration));
+ PageTransitions[2].Transition = new PageSlide(TimeSpan.FromMilliseconds(Duration), PageSlide.SlideAxis.Horizontal);
+ PageTransitions[3].Transition = new PageSlide(TimeSpan.FromMilliseconds(Duration), PageSlide.SlideAxis.Vertical);
+
+ var compositeTransition = new CompositePageTransition();
+ compositeTransition.PageTransitions.Add(PageTransitions[1].Transition);
+ compositeTransition.PageTransitions.Add(PageTransitions[2].Transition);
+ compositeTransition.PageTransitions.Add(PageTransitions[3].Transition);
+ PageTransitions[4].Transition = compositeTransition;
+
+ PageTransitions[5].Transition = new CustomTransition(TimeSpan.FromMilliseconds(Duration));
+ }
+
+ public void NextImage()
+ {
+ var index = Images.IndexOf(SelectedImage) + 1;
+
+ if (index >= Images.Count)
+ {
+ index = 0;
+ }
+
+ SelectedImage = Images[index];
+ }
+
+ public void PrevImage()
+ {
+ var index = Images.IndexOf(SelectedImage) - 1;
+
+ if (index < 0)
+ {
+ index = Images.Count-1;
+ }
+
+ SelectedImage = Images[index];
+ }
+ }
+
+ public class PageTransition : ViewModelBase
+ {
+ public PageTransition(string displayTitle)
+ {
+ DisplayTitle = displayTitle;
+ }
+
+ public string DisplayTitle { get; }
+
+
+ private IPageTransition _Transition;
+
+ ///
+ /// Gets or sets the transition
+ ///
+ public IPageTransition Transition
+ {
+ get { return _Transition; }
+ set { this.RaiseAndSetIfChanged(ref _Transition, value); }
+ }
+
+ public override string ToString()
+ {
+ return DisplayTitle;
+ }
+
+ }
+
+ public class CustomTransition : IPageTransition
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CustomTransition()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public CustomTransition(TimeSpan duration)
+ {
+ Duration = duration;
+ }
+
+ ///
+ /// Gets the duration of the animation.
+ ///
+ public TimeSpan Duration { get; set; }
+
+ public async Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
+ var tasks = new List();
+ var parent = GetVisualParent(from, to);
+ var scaleProperty = ScaleTransform.ScaleYProperty;
+
+ if (from != null)
+ {
+ var animation = new Animation
+ {
+ FillMode = FillMode.Forward,
+ Children =
+ {
+ new KeyFrame
+ {
+ Setters = { new Setter { Property = scaleProperty, Value = 1d } },
+ Cue = new Cue(0d)
+ },
+ new KeyFrame
+ {
+ Setters =
+ {
+ new Setter
+ {
+ Property = scaleProperty,
+ Value = 0d
+ }
+ },
+ Cue = new Cue(1d)
+ }
+ },
+ Duration = Duration
+ };
+ tasks.Add(animation.RunAsync(from, null, cancellationToken));
+ }
+
+ if (to != null)
+ {
+ to.IsVisible = true;
+ var animation = new Animation
+ {
+ FillMode = FillMode.Forward,
+ Children =
+ {
+ new KeyFrame
+ {
+ Setters =
+ {
+ new Setter
+ {
+ Property = scaleProperty,
+ Value = 0d
+ }
+ },
+ Cue = new Cue(0d)
+ },
+ new KeyFrame
+ {
+ Setters = { new Setter { Property = scaleProperty, Value = 1d } },
+ Cue = new Cue(1d)
+ }
+ },
+ Duration = Duration
+ };
+ tasks.Add(animation.RunAsync(to, null, cancellationToken));
+ }
+
+ await Task.WhenAll(tasks);
+
+ if (from != null && !cancellationToken.IsCancellationRequested)
+ {
+ from.IsVisible = false;
+ }
+ }
+
+ ///
+ /// Gets the common visual parent of the two control.
+ ///
+ /// The from control.
+ /// The to control.
+ /// The common parent.
+ ///
+ /// The two controls do not share a common parent.
+ ///
+ ///
+ /// Any one of the parameters may be null, but not both.
+ ///
+ private static IVisual GetVisualParent(IVisual? from, IVisual? to)
+ {
+ var p1 = (from ?? to)!.VisualParent;
+ var p2 = (to ?? from)!.VisualParent;
+
+ if (p1 != null && p2 != null && p1 != p2)
+ {
+ throw new ArgumentException("Controls for PageSlide must have same parent.");
+ }
+
+ return p1 ?? throw new InvalidOperationException("Cannot determine visual parent.");
+ }
+ }
+}
diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
index 1aa7f8ea04..e0dfa49a44 100644
--- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
+++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
@@ -22,7 +22,7 @@
40
- 200
+ 220
36
36
32