using System; using System.Threading; using System.Threading.Tasks; using Avalonia.Animation; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Interactivity; namespace Avalonia.Controls; /// /// Displays according to an , /// using a to move between the old and new content. /// public class TransitioningContentControl : ContentControl { private CancellationTokenSource? _currentTransition; private ContentPresenter? _lastPresenter; private ContentPresenter? _presenter2; private bool _isFirstFull; private bool _shouldAnimate; /// /// Defines the property. /// public static readonly StyledProperty PageTransitionProperty = AvaloniaProperty.Register( nameof(PageTransition), defaultValue: new ImmutableCrossFade(TimeSpan.FromMilliseconds(125))); /// /// Defines the property. /// public static readonly StyledProperty IsTransitionReversedProperty = AvaloniaProperty.Register( nameof(IsTransitionReversed), defaultValue: false); /// /// Defines the routed event. /// public static readonly RoutedEvent TransitionCompletedEvent = RoutedEvent.Register( nameof(TransitionCompleted), RoutingStrategies.Direct); /// /// Gets or sets the animation played when content appears and disappears. /// public IPageTransition? PageTransition { get => GetValue(PageTransitionProperty); set => SetValue(PageTransitionProperty, value); } /// /// Gets or sets a value indicating whether the control will be animated in the reverse direction. /// /// May not apply to all transitions. public bool IsTransitionReversed { get => GetValue(IsTransitionReversedProperty); set => SetValue(IsTransitionReversedProperty, value); } /// /// Raised when the old content isn't needed anymore by the control, because the transition has completed. /// public event EventHandler TransitionCompleted { add => AddHandler(TransitionCompletedEvent, value); remove => RemoveHandler(TransitionCompletedEvent, value); } protected override Size ArrangeOverride(Size finalSize) { var result = base.ArrangeOverride(finalSize); if (_shouldAnimate) { _currentTransition?.Cancel(); if (_presenter2 is not null && Presenter is { } presenter && PageTransition is { } transition) { _shouldAnimate = false; var cancel = new CancellationTokenSource(); _currentTransition = cancel; var from = _isFirstFull ? _presenter2 : presenter; var to = _isFirstFull ? presenter : _presenter2; var fromContent = from.Content; var toContent = to.Content; transition.Start(from, to, !IsTransitionReversed, cancel.Token).ContinueWith(task => { OnTransitionCompleted(new TransitionCompletedEventArgs( fromContent, toContent, task.Status == TaskStatus.RanToCompletion && !cancel.IsCancellationRequested)); if (!cancel.IsCancellationRequested) { HideOldPresenter(); } }, TaskScheduler.FromCurrentSynchronizationContext()); } _shouldAnimate = false; } return result; } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); UpdateContent(false); } protected override bool RegisterContentPresenter(ContentPresenter presenter) { if (base.RegisterContentPresenter(presenter)) { return true; } if (presenter is ContentPresenter p && p.Name == "PART_ContentPresenter2") { _presenter2 = p; _presenter2.IsVisible = false; UpdateContent(false); return true; } return false; } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Property == ContentProperty) { UpdateContent(true); return; } base.OnPropertyChanged(change); } private void UpdateContent(bool withTransition) { if (VisualRoot is null || _presenter2 is null || Presenter is null) { return; } var currentPresenter = _isFirstFull ? _presenter2 : Presenter; var fromContent = _lastPresenter?.Content; var toContent = Content; if (_lastPresenter != null && _lastPresenter != currentPresenter && _lastPresenter.Content == toContent) { _lastPresenter.Content = null; } currentPresenter.Content = toContent; currentPresenter.IsVisible = true; _lastPresenter = currentPresenter; _isFirstFull = !_isFirstFull; if (PageTransition is not null && withTransition) { _shouldAnimate = true; InvalidateArrange(); } else { HideOldPresenter(); OnTransitionCompleted(new TransitionCompletedEventArgs(fromContent, toContent, false)); } } private void HideOldPresenter() { var oldPresenter = _isFirstFull ? _presenter2 : Presenter; if (oldPresenter is not null) { oldPresenter.Content = null; oldPresenter.IsVisible = false; } } private void OnTransitionCompleted(TransitionCompletedEventArgs e) => RaiseEvent(e); private class ImmutableCrossFade : IPageTransition { private readonly CrossFade _inner; public ImmutableCrossFade(TimeSpan duration) => _inner = new CrossFade(duration); public Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) { return _inner.Start(from, to, cancellationToken); } } }