Browse Source

Implement TransitionContentControl.TransitionCompleted (#15172)

pull/15258/head
Julien Lebosquain 2 years ago
committed by GitHub
parent
commit
eac0e43656
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 39
      src/Avalonia.Controls/TransitionCompletedEventArgs.cs
  2. 40
      src/Avalonia.Controls/TransitioningContentControl.cs
  3. 74
      tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs

39
src/Avalonia.Controls/TransitionCompletedEventArgs.cs

@ -0,0 +1,39 @@
using Avalonia.Interactivity;
namespace Avalonia.Controls;
/// <summary>
/// Represents the event arguments for <see cref="TransitioningContentControl.TransitionCompletedEvent"/>.
/// </summary>
public class TransitionCompletedEventArgs : RoutedEventArgs
{
/// <summary>
/// Initializes a new instance of <see cref="TransitionCompletedEventArgs"/>.
/// </summary>
/// <param name="from">The content that was transitioned from.</param>
/// <param name="to">The content that was transitioned to.</param>
/// <param name="hasRunToCompletion">Whether the transition ran to completion.</param>
public TransitionCompletedEventArgs(object? from, object? to, bool hasRunToCompletion)
: base(TransitioningContentControl.TransitionCompletedEvent)
{
From = from;
To = to;
HasRunToCompletion = hasRunToCompletion;
}
/// <summary>
/// Gets the content that was transitioned from.
/// </summary>
public object? From { get; }
/// <summary>
/// Gets the content that was transitioned to.
/// </summary>
public object? To { get; }
/// <summary>
/// Gets whether the transition ran to completion.
/// If false, the transition may have completed instantly or been cancelled.
/// </summary>
public bool HasRunToCompletion { get; }
}

40
src/Avalonia.Controls/TransitioningContentControl.cs

@ -4,7 +4,7 @@ using System.Threading.Tasks;
using Avalonia.Animation;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Interactivity;
namespace Avalonia.Controls;
@ -36,6 +36,14 @@ public class TransitioningContentControl : ContentControl
nameof(IsTransitionReversed),
defaultValue: false);
/// <summary>
/// Defines the <see cref="TransitionCompleted"/> routed event.
/// </summary>
public static readonly RoutedEvent<TransitionCompletedEventArgs> TransitionCompletedEvent =
RoutedEvent.Register<TransitioningContentControl, TransitionCompletedEventArgs>(
nameof(TransitionCompleted),
RoutingStrategies.Direct);
/// <summary>
/// Gets or sets the animation played when content appears and disappears.
/// </summary>
@ -55,6 +63,15 @@ public class TransitioningContentControl : ContentControl
set => SetValue(IsTransitionReversedProperty, value);
}
/// <summary>
/// Raised when the old content isn't needed anymore by the control, because the transition has completed.
/// </summary>
public event EventHandler<TransitionCompletedEventArgs> TransitionCompleted
{
add => AddHandler(TransitionCompletedEvent, value);
remove => RemoveHandler(TransitionCompletedEvent, value);
}
protected override Size ArrangeOverride(Size finalSize)
{
var result = base.ArrangeOverride(finalSize);
@ -64,7 +81,7 @@ public class TransitioningContentControl : ContentControl
_currentTransition?.Cancel();
if (_presenter2 is not null &&
Presenter is Visual presenter &&
Presenter is { } presenter &&
PageTransition is { } transition)
{
_shouldAnimate = false;
@ -74,9 +91,14 @@ public class TransitioningContentControl : ContentControl
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(x =>
transition.Start(from, to, !IsTransitionReversed, cancel.Token).ContinueWith(task =>
{
OnTransitionCompleted(new TransitionCompletedEventArgs(
fromContent, toContent, task.Status == TaskStatus.RanToCompletion && !cancel.IsCancellationRequested));
if (!cancel.IsCancellationRequested)
{
HideOldPresenter();
@ -134,13 +156,17 @@ public class TransitioningContentControl : ContentControl
}
var currentPresenter = _isFirstFull ? _presenter2 : Presenter;
var fromContent = _lastPresenter?.Content;
var toContent = Content;
if (_lastPresenter != null &&
_lastPresenter != currentPresenter &&
_lastPresenter.Content == Content)
_lastPresenter.Content == toContent)
{
_lastPresenter.Content = null;
}
currentPresenter.Content = Content;
currentPresenter.Content = toContent;
currentPresenter.IsVisible = true;
_lastPresenter = currentPresenter;
@ -154,6 +180,7 @@ public class TransitioningContentControl : ContentControl
else
{
HideOldPresenter();
OnTransitionCompleted(new TransitionCompletedEventArgs(fromContent, toContent, false));
}
}
@ -167,6 +194,9 @@ public class TransitioningContentControl : ContentControl
}
}
private void OnTransitionCompleted(TransitionCompletedEventArgs e)
=> RaiseEvent(e);
private class ImmutableCrossFade : IPageTransition
{
private readonly CrossFade _inner;

74
tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -127,6 +128,36 @@ namespace Avalonia.Controls.UnitTests
Assert.False(presenter2.IsVisible);
}
[Fact]
public void TransitionCompleted_Should_Be_Raised_When_Content_Changes()
{
using var app = Start();
using var sync = UnitTestSynchronizationContext.Begin();
var (target, transition) = CreateTarget("foo");
var completedTransitions = new List<TransitionCompletedEventArgs>();
target.TransitionCompleted += (_, e) => completedTransitions.Add(e);
target.Content = "bar";
Layout(target);
VerifyCompletedTransitions();
transition.Complete();
sync.ExecutePostedCallbacks();
VerifyCompletedTransitions(new TransitionCompletedEventArgs("foo", "bar", true));
target.Content = "foo";
Layout(target);
VerifyCompletedTransitions(new TransitionCompletedEventArgs("foo", "bar", true));
transition.Complete();
sync.ExecutePostedCallbacks();
VerifyCompletedTransitions(new("foo", "bar", true), new("bar", "foo", true));
void VerifyCompletedTransitions(params TransitionCompletedEventArgs[] expected)
=> Assert.Equal(expected, completedTransitions, TransitionCompletedEventArgsComparer.Instance);
}
[Fact]
public void Transition_Should_Be_Canceled_If_Content_Changes_While_Running()
{
@ -183,6 +214,30 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal("bar", presenter2.Content);
}
[Fact]
public void TransitionCompleted_Should_Be_Raised_If_Content_Changes_While_Running()
{
using var app = Start();
using var sync = UnitTestSynchronizationContext.Begin();
var (target, _) = CreateTarget("foo");
var completedTransitions = new List<TransitionCompletedEventArgs>();
target.TransitionCompleted += (_, e) => completedTransitions.Add(e);
target.Content = "bar";
Layout(target);
sync.ExecutePostedCallbacks();
VerifyCompletedTransitions();
target.Content = "baz";
Layout(target);
sync.ExecutePostedCallbacks();
VerifyCompletedTransitions(new TransitionCompletedEventArgs("foo", "bar", false));
void VerifyCompletedTransitions(params TransitionCompletedEventArgs[] expected)
=> Assert.Equal(expected, completedTransitions, TransitionCompletedEventArgsComparer.Instance);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
@ -350,5 +405,24 @@ namespace Avalonia.Controls.UnitTests
public void Complete() => _tcs!.TrySetResult();
}
private sealed class TransitionCompletedEventArgsComparer : IEqualityComparer<TransitionCompletedEventArgs>
{
public static TransitionCompletedEventArgsComparer Instance { get; } = new();
public bool Equals(TransitionCompletedEventArgs? x, TransitionCompletedEventArgs? y)
{
if (ReferenceEquals(x, y))
return true;
if (x is null || y is null)
return false;
return x.From == y.From && x.To == y.To && x.HasRunToCompletion == y.HasRunToCompletion;
}
public int GetHashCode(TransitionCompletedEventArgs obj)
=> HashCode.Combine(obj.From, obj.To, obj.HasRunToCompletion);
}
}
}

Loading…
Cancel
Save