diff --git a/samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs b/samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs
index 4505c11e95..03ffb44b4a 100644
--- a/samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs
@@ -216,7 +216,6 @@ namespace ControlCatalog.ViewModels
{
var animation = new Animation
{
- FillMode = FillMode.Forward,
Children =
{
new KeyFrame
@@ -247,7 +246,6 @@ namespace ControlCatalog.ViewModels
to.IsVisible = true;
var animation = new Animation
{
- FillMode = FillMode.Forward,
Children =
{
new KeyFrame
diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs
index d47a7a7809..f30920ed2e 100644
--- a/src/Avalonia.Controls/ContentControl.cs
+++ b/src/Avalonia.Controls/ContentControl.cs
@@ -11,7 +11,7 @@ using Avalonia.Metadata;
namespace Avalonia.Controls
{
///
- /// Displays according to a .
+ /// Displays according to an .
///
[TemplatePart("PART_ContentPresenter", typeof(IContentPresenter))]
public class ContentControl : TemplatedControl, IContentControl, IContentPresenterHost
diff --git a/src/Avalonia.Controls/TransitioningContentControl.cs b/src/Avalonia.Controls/TransitioningContentControl.cs
index 2e0a36ad19..edc1b10d93 100644
--- a/src/Avalonia.Controls/TransitioningContentControl.cs
+++ b/src/Avalonia.Controls/TransitioningContentControl.cs
@@ -1,33 +1,30 @@
using System;
using System.Threading;
+using System.Threading.Tasks;
using Avalonia.Animation;
+using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
-using Avalonia.Threading;
+using Avalonia.Data;
namespace Avalonia.Controls;
///
-/// Displays according to a .
-/// Uses to move between the old and new content values.
+/// Displays according to an ,
+/// using a to move between the old and new content.
///
public class TransitioningContentControl : ContentControl
{
- private CancellationTokenSource? _lastTransitionCts;
- private object? _currentContent;
+ private CancellationTokenSource? _currentTransition;
+ private ContentPresenter? _transitionPresenter;
+ private Optional _transitionFrom;
///
/// Defines the property.
///
public static readonly StyledProperty PageTransitionProperty =
- AvaloniaProperty.Register(nameof(PageTransition),
- new CrossFade(TimeSpan.FromSeconds(0.125)));
-
- ///
- /// Defines the property.
- ///
- public static readonly DirectProperty CurrentContentProperty =
- AvaloniaProperty.RegisterDirect(nameof(CurrentContent),
- o => o.CurrentContent);
+ AvaloniaProperty.Register(
+ nameof(PageTransition),
+ defaultValue: new ImmutableCrossFade(TimeSpan.FromMilliseconds(125)));
///
/// Gets or sets the animation played when content appears and disappears.
@@ -38,74 +35,79 @@ public class TransitioningContentControl : ContentControl
set => SetValue(PageTransitionProperty, value);
}
- ///
- /// Gets the content currently displayed on the screen.
- ///
- public object? CurrentContent
+ protected override Size ArrangeOverride(Size finalSize)
{
- get => _currentContent;
- private set => SetAndRaise(CurrentContentProperty, ref _currentContent, value);
- }
+ var result = base.ArrangeOverride(finalSize);
- protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
- {
- base.OnAttachedToVisualTree(e);
+ if (_transitionFrom.HasValue)
+ {
+ _currentTransition?.Cancel();
+
+ if (_transitionPresenter is not null &&
+ Presenter is Visual presenter &&
+ PageTransition is { } transition &&
+ (_transitionFrom.Value is not Visual v || v.VisualParent is null))
+ {
+ _transitionPresenter.Content = _transitionFrom.Value;
+ _transitionPresenter.IsVisible = true;
+ _transitionFrom = Optional.Empty;
+
+ var cancel = new CancellationTokenSource();
+ _currentTransition = cancel;
+
+ transition.Start(_transitionPresenter, presenter, true, cancel.Token).ContinueWith(x =>
+ {
+ if (!cancel.IsCancellationRequested)
+ {
+ _transitionPresenter.Content = null;
+ _transitionPresenter.IsVisible = false;
+ }
+ }, TaskScheduler.FromCurrentSynchronizationContext());
+ }
+
+ _transitionFrom = Optional.Empty;
+ }
- Dispatcher.UIThread.Post(() => UpdateContentWithTransition(Content));
+ return result;
}
- protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ protected override bool RegisterContentPresenter(IContentPresenter presenter)
{
- base.OnDetachedFromVisualTree(e);
-
- _lastTransitionCts?.Cancel();
+ if (!base.RegisterContentPresenter(presenter) &&
+ presenter is ContentPresenter p &&
+ p.Name == "PART_TransitionContentPresenter")
+ {
+ _transitionPresenter = p;
+ _transitionPresenter.IsVisible = false;
+ return true;
+ }
+
+ return false;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
- if (change.Property == ContentProperty)
+ if (change.Property == ContentProperty &&
+ _transitionPresenter is not null &&
+ Presenter is Visual &&
+ PageTransition is not null)
{
- Dispatcher.UIThread.Post(() => UpdateContentWithTransition(Content));
- }
- else if (change.Property == CurrentContentProperty)
- {
- UpdateLogicalTree(change.OldValue, change.NewValue);
+ _transitionFrom = change.GetOldValue();
+ InvalidateArrange();
}
}
- protected override void ContentChanged(AvaloniaPropertyChangedEventArgs e)
+ private class ImmutableCrossFade : IPageTransition
{
- // We do nothing becuse we should not remove old Content until the animation is over
- }
+ private readonly CrossFade _inner;
- ///
- /// Updates the content with transitions.
- ///
- /// New content to set.
- private async void UpdateContentWithTransition(object? content)
- {
- if (VisualRoot is null)
- {
- return;
- }
-
- _lastTransitionCts?.Cancel();
- _lastTransitionCts = new CancellationTokenSource();
- var localToken = _lastTransitionCts.Token;
-
- if (PageTransition != null)
- await PageTransition.Start(this, null, true, localToken);
+ public ImmutableCrossFade(TimeSpan duration) => _inner = new CrossFade(duration);
- if (localToken.IsCancellationRequested)
+ public Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
{
- return;
+ return _inner.Start(from, to, cancellationToken);
}
-
- CurrentContent = content;
-
- if (PageTransition != null)
- await PageTransition.Start(null, this, true, localToken);
}
}
diff --git a/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml b/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml
index f1971fe236..e53fc1aad4 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml
@@ -3,16 +3,27 @@
-
+
+
+
+
diff --git a/src/Avalonia.Themes.Simple/Controls/TransitioningContentControl.xaml b/src/Avalonia.Themes.Simple/Controls/TransitioningContentControl.xaml
index 63185e11e6..fa8d749b1f 100644
--- a/src/Avalonia.Themes.Simple/Controls/TransitioningContentControl.xaml
+++ b/src/Avalonia.Themes.Simple/Controls/TransitioningContentControl.xaml
@@ -5,16 +5,27 @@
-
+
+
+
+
diff --git a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs
index aaa1de4da4..77fc207554 100644
--- a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs
@@ -1,66 +1,243 @@
using System;
-using Avalonia.LogicalTree;
-using Avalonia.UnitTests;
-using Xunit;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.Headless;
+using Avalonia.Layout;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+using Xunit;
+
+#nullable enable
namespace Avalonia.Controls.UnitTests
{
public class TransitioningContentControlTests
{
[Fact]
- public void Old_Content_Shuold_Be_Removed__From_Logical_Tree_After_Out_Animation()
+ public void Transition_Should_Not_Be_Run_When_First_Shown()
+ {
+ using var app = Start();
+ var (target, transition) = CreateTarget("foo");
+
+ Assert.Equal(0, transition.StartCount);
+ }
+
+ [Fact]
+ public void TransitionContentPresenter_Should_Initially_Be_Hidden()
+ {
+ using var app = Start();
+ var (target, transition) = CreateTarget("foo");
+ var transitionPresenter = GetTransitionContentPresenter(target);
+
+ Assert.False(transitionPresenter.IsVisible);
+ }
+
+ [Fact]
+ public void Transition_Should_Be_Run_On_Layout()
+ {
+ using var app = Start();
+ var (target, transition) = CreateTarget("foo");
+
+ target.Content = "bar";
+ Assert.Equal(0, transition.StartCount);
+
+ Layout(target);
+ Assert.Equal(1, transition.StartCount);
+ }
+
+ [Fact]
+ public void Control_Transition_Should_Be_Run_On_Layout()
+ {
+ using var app = Start();
+ var (target, transition) = CreateTarget(new Button());
+
+ target.Content = new Canvas();
+ Assert.Equal(0, transition.StartCount);
+
+ Layout(target);
+ Assert.Equal(1, transition.StartCount);
+ }
+
+ [Fact]
+ public void ContentPresenters_Should_Be_Setup_For_Transition()
+ {
+ using var app = Start();
+ var (target, transition) = CreateTarget("foo");
+ var transitionPresenter = GetTransitionContentPresenter(target);
+
+ target.Content = "bar";
+ Layout(target);
+
+ Assert.True(transitionPresenter.IsVisible);
+ Assert.Equal("bar", target.Presenter!.Content);
+ Assert.Equal("foo", transitionPresenter.Content);
+ }
+
+ [Fact]
+ public void TransitionContentPresenter_Should_Be_Hidden_When_Transition_Completes()
+ {
+ using var app = Start();
+ using var sync = UnitTestSynchronizationContext.Begin();
+ var (target, transition) = CreateTarget("foo");
+ var transitionPresenter = GetTransitionContentPresenter(target);
+
+ target.Content = "bar";
+ Layout(target);
+ Assert.True(transitionPresenter.IsVisible);
+
+ transition.Complete();
+ sync.ExecutePostedCallbacks();
+
+ Assert.False(transitionPresenter.IsVisible);
+ }
+
+ [Fact]
+ public void Transition_Should_Be_Canceled_If_Content_Changes_While_Running()
{
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ using var app = Start();
+ using var sync = UnitTestSynchronizationContext.Begin();
+ var (target, transition) = CreateTarget("foo");
+ var transitionPresenter = GetTransitionContentPresenter(target);
+
+ target.Content = "bar";
+ Layout(target);
+ target.Content = "baz";
+
+ Assert.Equal(0, transition.CancelCount);
+
+ Layout(target);
+
+ Assert.Equal(1, transition.CancelCount);
+ }
+
+ [Fact]
+ public void New_Transition_Should_Be_Started_If_Content_Changes_While_Running()
+ {
+ using var app = Start();
+ using var sync = UnitTestSynchronizationContext.Begin();
+ var (target, transition) = CreateTarget("foo");
+ var transitionPresenter = GetTransitionContentPresenter(target);
+
+ target.Content = "bar";
+ Layout(target);
+
+ target.Content = "baz";
+
+ var startedRaised = 0;
+
+ transition.Started += (from, to, forward) =>
{
- var testTransition = new TestTransition();
+ var fromPresenter = Assert.IsType(from);
+ var toPresenter = Assert.IsType(to);
+
+ Assert.Same(transitionPresenter, fromPresenter);
+ Assert.Same(target.Presenter, toPresenter);
+ Assert.Equal("bar", fromPresenter.Content);
+ Assert.Equal("baz", toPresenter.Content);
+ Assert.True(forward);
+ Assert.Equal(1, transition.CancelCount);
- var target = new TransitioningContentControl();
- target.PageTransition = testTransition;
+ ++startedRaised;
+ };
- var root = new TestRoot() { Child = target };
+ Layout(target);
+ sync.ExecutePostedCallbacks();
+
+ Assert.Equal(1, startedRaised);
+ Assert.Equal("baz", target.Presenter!.Content);
+ Assert.Equal("bar", transitionPresenter.Content);
+ }
- var oldControl = new Control();
- var newControl = new Control();
+ private static IDisposable Start()
+ {
+ return UnitTestApplication.Start(
+ TestServices.MockThreadingInterface.With(
+ fontManagerImpl: new HeadlessFontManagerStub(),
+ renderInterface: new HeadlessPlatformRenderInterface(),
+ textShaperImpl: new HeadlessTextShaperStub()));
+ }
- target.Content = oldControl;
- Threading.Dispatcher.UIThread.RunJobs();
+ private static (TransitioningContentControl, TestTransition) CreateTarget(object content)
+ {
+ var transition = new TestTransition();
+ var target = new TransitioningContentControl
+ {
+ Content = content,
+ PageTransition = transition,
+ Template = CreateTemplate(),
+ };
- Assert.Equal(target, oldControl.GetLogicalParent());
- Assert.Equal(null, newControl.GetLogicalParent());
+ var root = new TestRoot(target);
+ root.LayoutManager.ExecuteInitialLayoutPass();
+ return (target, transition);
+ }
- testTransition.BeginTransition += isFrom =>
+ private static IControlTemplate CreateTemplate()
+ {
+ return new FuncControlTemplate((x, ns) =>
+ {
+ return new Panel
{
- // Old out
- if (isFrom)
+ Children =
{
- Assert.Equal(target, oldControl.GetLogicalParent());
- Assert.Equal(null, newControl.GetLogicalParent());
- }
- // New in
- else
- {
- Assert.Equal(null, oldControl.GetLogicalParent());
- Assert.Equal(target, newControl.GetLogicalParent());
+ new ContentPresenter
+ {
+ Name = "PART_ContentPresenter",
+ [!ContentPresenter.ContentProperty] = x[!ContentControl.ContentProperty],
+ },
+ new ContentPresenter
+ {
+ Name = "PART_TransitionContentPresenter",
+ },
}
};
+ });
+ }
- target.Content = newControl;
- Threading.Dispatcher.UIThread.RunJobs();
- }
+ private static ContentPresenter GetTransitionContentPresenter(TransitioningContentControl target)
+ {
+ return Assert.IsType(target
+ .GetTemplateChildren()
+ .First(x => x.Name == "PART_TransitionContentPresenter"));
}
- }
- public class TestTransition : IPageTransition
- {
- public event Action BeginTransition;
- public Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken)
+ private void Layout(Control c)
{
- bool isFrom = from != null && to == null;
- BeginTransition?.Invoke(isFrom);
- return Task.CompletedTask;
+ (c.GetVisualRoot() as ILayoutRoot)?.LayoutManager.ExecuteLayoutPass();
+ }
+
+ private class TestTransition : IPageTransition
+ {
+ private TaskCompletionSource? _tcs;
+
+ public int StartCount { get; private set; }
+ public int FinishCount { get; private set; }
+ public int CancelCount { get; private set; }
+
+ public event Action? Started;
+
+ public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
+ {
+ ++StartCount;
+ Started?.Invoke(from, to, forward);
+ if (_tcs is not null)
+ throw new InvalidOperationException("Transition already running");
+ _tcs = new TaskCompletionSource();
+ cancellationToken.Register(() => _tcs.TrySetResult());
+ await _tcs.Task;
+ _tcs = null;
+
+ if (!cancellationToken.IsCancellationRequested)
+ ++FinishCount;
+ else
+ ++CancelCount;
+ }
+
+ public void Complete() => _tcs!.TrySetResult();
}
}
}