Browse Source

Implemented a PageTransition base class.

Implements the functionality common to all page transitions. Also:

- Don't inherit from `PageSlide` in `Rotate3DTransition`
- Don't use a nested enum for `PageSlide.Orientation`
fixes/11167-pagetransition-flicker
Steven Kirk 3 years ago
parent
commit
6ea251c165
  1. 4
      samples/ControlCatalog/Pages/CarouselPage.xaml.cs
  2. 4
      samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs
  3. 178
      src/Avalonia.Base/Animation/CrossFade.cs
  4. 221
      src/Avalonia.Base/Animation/PageSlide.cs
  5. 214
      src/Avalonia.Base/Animation/PageTransition.cs
  6. 146
      src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs
  7. 18
      tests/Avalonia.Base.UnitTests/Animation/CrossFadeTests.cs
  8. 9
      tests/Avalonia.Base.UnitTests/Animation/PageSlideTests.cs

4
samples/ControlCatalog/Pages/CarouselPage.xaml.cs

@ -41,13 +41,13 @@ namespace ControlCatalog.Pages
_carousel.PageTransition = null;
break;
case 1:
_carousel.PageTransition = new PageSlide(TimeSpan.FromSeconds(0.25), _orientation.SelectedIndex == 0 ? PageSlide.SlideAxis.Horizontal : PageSlide.SlideAxis.Vertical);
_carousel.PageTransition = new PageSlide(TimeSpan.FromSeconds(0.25), _orientation.SelectedIndex == 0 ? PageSlideAxis.Horizontal : PageSlideAxis.Vertical);
break;
case 2:
_carousel.PageTransition = new CrossFade(TimeSpan.FromSeconds(0.25));
break;
case 3:
_carousel.PageTransition = new Rotate3DTransition(TimeSpan.FromSeconds(0.5), _orientation.SelectedIndex == 0 ? PageSlide.SlideAxis.Horizontal : PageSlide.SlideAxis.Vertical);
_carousel.PageTransition = new Rotate3DTransition(TimeSpan.FromSeconds(0.5), _orientation.SelectedIndex == 0 ? PageSlideAxis.Horizontal : PageSlideAxis.Vertical);
break;
}
}

4
samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs

@ -113,8 +113,8 @@ namespace ControlCatalog.ViewModels
}
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);
PageTransitions[2].Transition = new PageSlide(TimeSpan.FromMilliseconds(Duration), PageSlideAxis.Horizontal);
PageTransitions[3].Transition = new PageSlide(TimeSpan.FromMilliseconds(Duration), PageSlideAxis.Vertical);
var compositeTransition = new CompositePageTransition();
compositeTransition.PageTransitions.Add(PageTransitions[1].Transition!);

178
src/Avalonia.Base/Animation/CrossFade.cs

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation.Easings;
using Avalonia.Styling;
@ -10,10 +7,10 @@ namespace Avalonia.Animation
/// <summary>
/// Defines a cross-fade animation between two <see cref="Visual"/>s.
/// </summary>
public class CrossFade : IPageTransition
public class CrossFade : PageTransition
{
private readonly Animation _fadeOutAnimation;
private readonly Animation _fadeInAnimation;
private readonly Animation _fadeOut;
private readonly Animation _fadeIn;
/// <summary>
/// Initializes a new instance of the <see cref="CrossFade"/> class.
@ -28,27 +25,57 @@ namespace Avalonia.Animation
/// </summary>
/// <param name="duration">The duration of the animation.</param>
public CrossFade(TimeSpan duration)
: base(duration)
{
_fadeOutAnimation = new Animation
_fadeOut = CreateFadeAnimation(duration, 1, 0);
_fadeIn = CreateFadeAnimation(duration, 0, 1);
}
/// <summary>
/// Gets or sets element entrance easing.
/// </summary>
public Easing FadeInEasing
{
get => _fadeIn.Easing;
set => _fadeIn.Easing = value;
}
/// <summary>
/// Gets or sets element exit easing.
/// </summary>
public Easing FadeOutEasing
{
get => _fadeOut.Easing;
set => _fadeOut.Easing = value;
}
protected override Animation GetShowAnimation(Visual? from, Visual? to, bool forward) => _fadeIn;
protected override Animation GetHideAnimation(Visual? from, Visual? to, bool forward) => _fadeOut;
protected override void InvalidateCachedAnimations()
{
base.InvalidateCachedAnimations();
_fadeIn.Duration = _fadeOut.Duration = Duration;
}
private static Animation CreateFadeAnimation(TimeSpan duration, double fromOpacity, double toOpacity)
{
return new Animation
{
Duration = duration,
Children =
{
new KeyFrame()
{
Setters =
{
new Setter
{
Property = Visual.IsVisibleProperty,
Value = true,
},
new Setter
{
Property = Visual.OpacityProperty,
Value = 1d
Value = fromOpacity,
},
},
Cue = new Cue(0)
Cue = new Cue(0),
},
new KeyFrame()
{
@ -57,132 +84,13 @@ namespace Avalonia.Animation
new Setter
{
Property = Visual.OpacityProperty,
Value = 0d
Value = toOpacity,
},
},
Cue = new Cue(1)
},
}
};
_fadeInAnimation = new Animation
{
Children =
{
new KeyFrame()
{
Setters =
{
new Setter
{
Property = Visual.OpacityProperty,
Value = 0d
}
},
Cue = new Cue(0)
},
new KeyFrame()
{
Setters =
{
new Setter
{
Property = Visual.OpacityProperty,
Value = 1d
}
},
Cue = new Cue(1)
Cue = new Cue(1),
},
}
};
_fadeOutAnimation.Duration = _fadeInAnimation.Duration = duration;
}
/// <summary>
/// Gets the duration of the animation.
/// </summary>
public TimeSpan Duration
{
get => _fadeOutAnimation.Duration;
set => _fadeOutAnimation.Duration = _fadeInAnimation.Duration = value;
}
/// <summary>
/// Gets or sets element entrance easing.
/// </summary>
public Easing FadeInEasing
{
get => _fadeInAnimation.Easing;
set => _fadeInAnimation.Easing = value;
}
/// <summary>
/// Gets or sets element exit easing.
/// </summary>
public Easing FadeOutEasing
{
get => _fadeOutAnimation.Easing;
set => _fadeOutAnimation.Easing = value;
}
/// <inheritdoc cref="Start(Visual, Visual, CancellationToken)" />
public async Task Start(Visual? from, Visual? to, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
var tasks = new List<Task>();
var initialFromVisible = true;
var initialToVisible = true;
if (from != null)
{
tasks.Add(_fadeOutAnimation.RunAsync(from, null, cancellationToken));
// Make "from" control invisible: this is overridden in the fade out animation, so
// will only take effect when the animation is completed.
initialFromVisible = from.IsVisible;
from.IsVisible = false;
}
if (to != null)
{
initialToVisible = to.IsVisible;
to.IsVisible = true;
tasks.Add(_fadeInAnimation.RunAsync(to, null, cancellationToken));
}
await Task.WhenAll(tasks);
if (cancellationToken.IsCancellationRequested)
{
if (from != null)
from.IsVisible = initialFromVisible;
if (to != null)
to.IsVisible = initialToVisible;
}
}
/// <summary>
/// Starts the animation.
/// </summary>
/// <param name="from">
/// The control that is being transitioned away from. May be null.
/// </param>
/// <param name="to">
/// The control that is being transitioned to. May be null.
/// </param>
/// <param name="forward">
/// Unused for cross-fades.
/// </param>
/// <param name="cancellationToken">allowed cancel transition</param>
/// <returns>
/// A <see cref="Task"/> that tracks the progress of the animation.
/// </returns>
Task IPageTransition.Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
{
return Start(from, to, cancellationToken);
}
}
}

221
src/Avalonia.Base/Animation/PageSlide.cs

@ -1,32 +1,32 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation.Easings;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.VisualTree;
namespace Avalonia.Animation
{
/// <summary>
/// The axis on which a <see cref="PageSlide"/> should occur
/// </summary>
public enum PageSlideAxis
{
Horizontal,
Vertical
}
/// <summary>
/// Transitions between two pages by sliding them horizontally or vertically.
/// </summary>
public class PageSlide : IPageTransition
public class PageSlide : PageTransition
{
/// <summary>
/// The axis on which the PageSlide should occur
/// </summary>
public enum SlideAxis
{
Horizontal,
Vertical
}
private readonly Animation _slideOut;
private readonly Animation _slideIn;
/// <summary>
/// Initializes a new instance of the <see cref="PageSlide"/> class.
/// </summary>
public PageSlide()
: this(TimeSpan.Zero)
{
}
@ -35,128 +35,78 @@ namespace Avalonia.Animation
/// </summary>
/// <param name="duration">The duration of the animation.</param>
/// <param name="orientation">The axis on which the animation should occur</param>
public PageSlide(TimeSpan duration, SlideAxis orientation = SlideAxis.Horizontal)
public PageSlide(TimeSpan duration, PageSlideAxis orientation = PageSlideAxis.Horizontal)
: base(duration)
{
Duration = duration;
Orientation = orientation;
var property = orientation == PageSlideAxis.Horizontal ?
TranslateTransform.XProperty : TranslateTransform.YProperty;
_slideIn = CreateSlideAnimation(duration, property);
_slideOut = CreateSlideAnimation(duration, property);
}
/// <summary>
/// Gets the duration of the animation.
/// Gets the direction of the animation.
/// </summary>
public TimeSpan Duration { get; set; }
public PageSlideAxis Orientation
{
get
{
return _slideIn.Children[0].Setters[0].Property == TranslateTransform.XProperty ?
PageSlideAxis.Horizontal : PageSlideAxis.Vertical;
}
set
{
if (Orientation != value)
{
var property = value == PageSlideAxis.Horizontal ?
TranslateTransform.XProperty : TranslateTransform.YProperty;
_slideIn.Children[0].Setters[0].Property = property;
_slideIn.Children[1].Setters[0].Property = property;
_slideOut.Children[0].Setters[0].Property = property;
_slideOut.Children[1].Setters[0].Property = property;
}
}
}
/// <summary>
/// Gets the duration of the animation.
/// </summary>
public SlideAxis Orientation { get; set; }
/// <summary>
/// Gets or sets element entrance easing.
/// </summary>
public Easing SlideInEasing { get; set; } = new LinearEasing();
public Easing SlideInEasing
{
get => _slideIn.Easing;
set => _slideIn.Easing = value;
}
/// <summary>
/// Gets or sets element exit easing.
/// </summary>
public Easing SlideOutEasing { get; set; } = new LinearEasing();
/// <inheritdoc />
public virtual async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
public Easing SlideOutEasing
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
get => _slideOut.Easing;
set => _slideOut.Easing = value;
}
var tasks = new List<Task>();
protected override Animation GetHideAnimation(Visual? from, Visual? to, bool forward)
{
var parent = GetVisualParent(from, to);
var distance = Orientation == SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height;
var translateProperty = Orientation == SlideAxis.Horizontal ? TranslateTransform.XProperty : TranslateTransform.YProperty;
var initialFromVisible = true;
var initialToVisible = true;
if (from != null)
{
var animation = new Animation
{
Easing = SlideOutEasing,
Children =
{
new KeyFrame
{
Setters =
{
new Setter { Property = Visual.IsVisibleProperty, Value = true },
new Setter { Property = translateProperty, Value = 0d },
},
Cue = new Cue(0d)
},
new KeyFrame
{
Setters =
{
new Setter
{
Property = translateProperty,
Value = forward ? -distance : distance
}
},
Cue = new Cue(1d)
}
},
Duration = Duration
};
tasks.Add(animation.RunAsync(from, null, cancellationToken));
// Make "from" control invisible: this is overridden in the fade out animation, so
// will only take effect when the animation is completed.
initialFromVisible = from.IsVisible;
from.IsVisible = false;
}
if (to != null)
{
var animation = new Animation
{
Easing = SlideInEasing,
Children =
{
new KeyFrame
{
Setters =
{
new Setter
{
Property = translateProperty,
Value = forward ? distance : -distance
}
},
Cue = new Cue(0d)
},
new KeyFrame
{
Setters = { new Setter { Property = translateProperty, Value = 0d } },
Cue = new Cue(1d)
}
},
Duration = Duration
};
initialToVisible = to.IsVisible;
to.IsVisible = true;
tasks.Add(animation.RunAsync(to, null, cancellationToken));
}
var distance = Orientation == PageSlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height;
_slideOut.Children[1].Setters[0].Value = forward ? -distance : distance;
return _slideOut;
}
await Task.WhenAll(tasks);
protected override Animation GetShowAnimation(Visual? from, Visual? to, bool forward)
{
var parent = GetVisualParent(from, to);
var distance = Orientation == PageSlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height;
_slideIn.Children[0].Setters[0].Value = forward ? distance : -distance;
return _slideIn;
}
if (cancellationToken.IsCancellationRequested)
{
if (from != null)
from.IsVisible = initialFromVisible;
if (to != null)
to.IsVisible = initialToVisible;
}
protected override void InvalidateCachedAnimations()
{
base.InvalidateCachedAnimations();
_slideIn.Duration = _slideOut.Duration = Duration;
}
/// <summary>
@ -171,7 +121,7 @@ namespace Avalonia.Animation
/// <remarks>
/// Any one of the parameters may be null, but not both.
/// </remarks>
protected static Visual GetVisualParent(Visual? from, Visual? to)
internal static Visual GetVisualParent(Visual? from, Visual? to)
{
var p1 = (from ?? to)!.VisualParent;
var p2 = (to ?? from)!.VisualParent;
@ -183,5 +133,40 @@ namespace Avalonia.Animation
return p1 ?? throw new InvalidOperationException("Cannot determine visual parent.");
}
private static Animation CreateSlideAnimation(TimeSpan duration, AvaloniaProperty property)
{
return new Animation
{
Duration = duration,
Children =
{
new KeyFrame
{
Setters =
{
new Setter
{
Property = property,
Value = 0.0,
},
},
Cue = new Cue(0)
},
new KeyFrame
{
Setters =
{
new Setter
{
Property = property,
Value = 0.0
}
},
Cue = new Cue(1)
}
},
};
}
}
}

214
src/Avalonia.Base/Animation/PageTransition.cs

@ -0,0 +1,214 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Styling;
namespace Avalonia.Animation
{
/// <summary>
/// Base class for animations that transition between two controls.
/// </summary>
public abstract class PageTransition : IPageTransition
{
private TimeSpan _duration;
private Animation? _hideVisibilityAnimation;
private Animation? _showVisibilityAnimation;
/// <summary>
/// Initializes a new instance of the <see cref="PageTransition"/> class.
/// </summary>
public PageTransition()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PageTransition"/> class.
/// </summary>
/// <param name="duration">The duration of the animation.</param>
public PageTransition(TimeSpan duration)
{
_duration = duration;
}
/// <summary>
/// Gets or sets the duration of the animation.
/// </summary>
public TimeSpan Duration
{
get => _duration;
set
{
if (_duration != value)
{
_duration = value;
InvalidateCachedAnimations();
}
}
}
/// <summary>
/// Starts the animation.
/// </summary>
/// <param name="from">
/// The control that is being transitioned away from. May be null.
/// </param>
/// <param name="to">
/// The control that is being transitioned to. May be null.
/// </param>
/// <param name="cancellationToken">
/// Animation cancellation.
/// </param>
/// <returns>
/// A <see cref="Task"/> that tracks the progress of the animation.
/// </returns>
/// <remarks>
/// The <paramref name="from"/> and <paramref name="to"/> controls will be made visible
/// and <paramref name="from"/> transitioned to <paramref name="to"/>. At the end of the
/// animation (when the returned task completes), <paramref name="from"/> will be made
/// invisible but all other properties involved in the transition will have been left
/// unchanged.
/// </remarks>
public Task Start(Visual? from, Visual? to, CancellationToken cancellationToken)
{
return Start(from, to, true, cancellationToken);
}
/// <summary>
/// Starts the animation.
/// </summary>
/// <param name="from">
/// The control that is being transitioned away from. May be null.
/// </param>
/// <param name="to">
/// The control that is being transitioned to. May be null.
/// </param>
/// <param name="forward">
/// If the animation is bidirectional, controls the direction of the animation.
/// </param>
/// <param name="cancellationToken">
/// Animation cancellation.
/// </param>
/// <returns>
/// A <see cref="Task"/> that tracks the progress of the animation.
/// </returns>
/// <remarks>
/// The <paramref name="from"/> and <paramref name="to"/> controls will be made visible
/// and <paramref name="from"/> transitioned to <paramref name="to"/>. At the end of the
/// animation (when the returned task completes), <paramref name="from"/> will be made
/// invisible but all other properties involved in the transition will have been left
/// unchanged.
/// </remarks>
public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
return;
List<Task>? tasks = null;
if (from is not null)
{
tasks ??= new();
tasks.Add(GetHideVisibilityAnimation().RunAsync(from, null, cancellationToken));
tasks.Add(GetHideAnimation(from, to, forward).RunAsync(from, null, cancellationToken));
}
if (to is not null)
{
tasks ??= new();
tasks.Add(GetShowAnimation(from, to, forward).RunAsync(to, null, cancellationToken));
tasks.Add(GetShowVisibilityAnimation().RunAsync(to, null, cancellationToken));
}
if (tasks is not null)
await Task.WhenAll(tasks);
}
/// <summary>
/// When implemented in a derived class, returns the animation used to transition away from
/// the <paramref name="from"/> control.
/// </summary>
/// <param name="from">
/// The control that is being transitioned away from. May be null.
/// </param>
/// <param name="to">
/// The control that is being transitioned to. May be null.
/// </param>
/// <param name="forward">
/// If the animation is bidirectional, controls the direction of the animation.
/// </param>
protected abstract Animation GetHideAnimation(Visual? from, Visual? to, bool forward);
/// <summary>
/// When implemented in a derived class, returns the animation used to transition to the
/// <paramref name="to"/> control.
/// </summary>
/// <param name="from">
/// The control that is being transitioned away from. May be null.
/// </param>
/// <param name="to">
/// The control that is being transitioned to. May be null.
/// </param>
/// <param name="forward">
/// If the animation is bidirectional, controls the direction of the animation.
/// </param>
protected abstract Animation GetShowAnimation(Visual? from, Visual? to, bool forward);
/// <summary>
/// Called when a property that affects the animation is changed.
/// </summary>
protected virtual void InvalidateCachedAnimations()
{
if (_hideVisibilityAnimation is not null)
_hideVisibilityAnimation.Duration = _duration;
if (_showVisibilityAnimation is not null)
_showVisibilityAnimation.Duration = _duration;
}
private Animation GetHideVisibilityAnimation()
{
return _hideVisibilityAnimation ??= CreateIsVisibleAnimation(Duration, false);
}
private Animation GetShowVisibilityAnimation()
{
return _showVisibilityAnimation ??= CreateIsVisibleAnimation(Duration, true);
}
private static Animation CreateIsVisibleAnimation(TimeSpan duration, bool endState)
{
return new()
{
Duration = duration,
FillMode = FillMode.Forward,
Children =
{
new KeyFrame()
{
Setters =
{
new Setter
{
Property = Visual.IsVisibleProperty,
Value = true,
},
},
Cue = new Cue(0)
},
new KeyFrame()
{
Setters =
{
new Setter
{
Property = Visual.IsVisibleProperty,
Value = endState,
},
},
Cue = new Cue(1)
}
}
};
}
}
}

146
src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs

@ -1,26 +1,33 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation.Easings;
using Avalonia.Media;
using Avalonia.Styling;
namespace Avalonia.Animation;
public class Rotate3DTransition: PageSlide
public class Rotate3DTransition : PageTransition
{
/// <summary>
/// Initializes a new instance of the <see cref="Rotate3DTransition"/>
/// </summary>
public Rotate3DTransition()
: this(TimeSpan.Zero)
{
}
/// <summary>
/// Creates a new instance of the <see cref="Rotate3DTransition"/>
/// Initializes a new instance of the <see cref="Rotate3DTransition"/>
/// </summary>
/// <param name="duration">How long the rotation should take place</param>
/// <param name="orientation">The orientation of the rotation</param>
/// <param name="depth">Defines the depth of the 3D Effect. If null, depth will be calculated automatically from the width or height of the common parent of the visual being rotated</param>
public Rotate3DTransition(TimeSpan duration, SlideAxis orientation = SlideAxis.Horizontal, double? depth = null)
: base(duration, orientation)
public Rotate3DTransition(TimeSpan duration, PageSlideAxis orientation = PageSlideAxis.Horizontal, double? depth = null)
: base(duration)
{
Orientation = orientation;
Depth = depth;
}
/// <summary>
/// Defines the depth of the 3D Effect. If null, depth will be calculated automatically from the width or height
/// of the common parent of the visual being rotated.
@ -28,33 +35,67 @@ public class Rotate3DTransition: PageSlide
public double? Depth { get; set; }
/// <summary>
/// Creates a new instance of the <see cref="Rotate3DTransition"/>
/// Gets the direction of the animation.
/// </summary>
public Rotate3DTransition() { }
public PageSlideAxis Orientation { get; set; }
/// <inheritdoc />
public override async Task Start(Visual? @from, Visual? to, bool forward, CancellationToken cancellationToken)
/// <summary>
/// Gets or sets element entrance easing.
/// </summary>
public Easing SlideInEasing { get; set; } = new LinearEasing();
/// <summary>
/// Gets or sets element exit easing.
/// </summary>
public Easing SlideOutEasing { get; set; } = new LinearEasing();
protected override Animation GetHideAnimation(Visual? from, Visual? to, bool forward)
{
if (cancellationToken.IsCancellationRequested)
var parent = PageSlide.GetVisualParent(from, to);
return new Animation
{
return;
}
Easing = SlideOutEasing,
Duration = Duration,
Children =
{
CreateKeyFrame(parent, 0d, 0d, 2),
CreateKeyFrame(parent, 0.5d, 45d * (forward ? -1 : 1), 1),
CreateKeyFrame(parent, 1d, 90d * (forward ? -1 : 1), 1, isVisible: false)
},
};
}
protected override Animation GetShowAnimation(Visual? from, Visual? to, bool forward)
{
var parent = PageSlide.GetVisualParent(from, to);
return new Animation
{
Easing = SlideInEasing,
Duration = Duration,
Children =
{
CreateKeyFrame(parent, 0d, 90d * (forward ? 1 : -1), 1),
CreateKeyFrame(parent, 0.5d, 45d * (forward ? 1 : -1), 1),
CreateKeyFrame(parent, 1d, 0d, 2)
},
};
}
var tasks = new Task[from != null && to != null ? 2 : 1];
var parent = GetVisualParent(from, to);
private KeyFrame CreateKeyFrame(Visual parent, double cue, double rotation, int zIndex, bool isVisible = true)
{
var (rotateProperty, center) = Orientation switch
{
SlideAxis.Vertical => (Rotate3DTransform.AngleXProperty, parent.Bounds.Height),
SlideAxis.Horizontal => (Rotate3DTransform.AngleYProperty, parent.Bounds.Width),
PageSlideAxis.Vertical => (Rotate3DTransform.AngleXProperty, parent.Bounds.Height),
PageSlideAxis.Horizontal => (Rotate3DTransform.AngleYProperty, parent.Bounds.Width),
_ => throw new ArgumentOutOfRangeException()
};
var depthSetter = new Setter {Property = Rotate3DTransform.DepthProperty, Value = Depth ?? center};
var centerZSetter = new Setter {Property = Rotate3DTransform.CenterZProperty, Value = -center / 2};
var depthSetter = new Setter { Property = Rotate3DTransform.DepthProperty, Value = Depth ?? center };
var centerZSetter = new Setter { Property = Rotate3DTransform.CenterZProperty, Value = -center / 2 };
KeyFrame CreateKeyFrame(double cue, double rotation, int zIndex, bool isVisible = true) =>
new() {
Setters =
return new()
{
Setters =
{
new Setter { Property = Visual.IsVisibleProperty, Value = isVisible },
new Setter { Property = rotateProperty, Value = rotation },
@ -62,62 +103,7 @@ public class Rotate3DTransition: PageSlide
centerZSetter,
depthSetter
},
Cue = new Cue(cue)
};
var initialFromVisible = true;
var initialToVisible = true;
if (from != null)
{
var animation = new Animation
{
Easing = SlideOutEasing,
Duration = Duration,
Children =
{
CreateKeyFrame(0d, 0d, 2),
CreateKeyFrame(0.5d, 45d * (forward ? -1 : 1), 1),
CreateKeyFrame(1d, 90d * (forward ? -1 : 1), 1, isVisible: false)
}
};
tasks[0] = animation.RunAsync(from, null, cancellationToken);
// Make "from" control invisible: this is overridden in the fade out animation, so
// will only take effect when the animation is completed.
initialFromVisible = from.IsVisible;
from.IsVisible = false;
}
if (to != null)
{
to.IsVisible = true;
var animation = new Animation
{
Easing = SlideInEasing,
Duration = Duration,
Children =
{
CreateKeyFrame(0d, 90d * (forward ? 1 : -1), 1),
CreateKeyFrame(0.5d, 45d * (forward ? 1 : -1), 1),
CreateKeyFrame(1d, 0d, 2)
}
};
initialToVisible = to.IsVisible;
to.IsVisible = true;
tasks[from != null ? 1 : 0] = animation.RunAsync(to, null, cancellationToken);
}
await Task.WhenAll(tasks);
if (cancellationToken.IsCancellationRequested)
{
if (from != null)
from.IsVisible = initialFromVisible;
if (to != null)
to.IsVisible = initialToVisible;
}
Cue = new Cue(cue)
};
}
}

18
tests/Avalonia.Base.UnitTests/Animation/CrossFadeTests.cs

@ -69,6 +69,8 @@ namespace Avalonia.Base.UnitTests.Animation
from.PropertyChanged += (s, e) =>
{
if (e.Property == Visual.IsVisibleProperty)
fromState.Add((to.Opacity, e.GetNewValue<bool>()));
if (e.Property == Visual.OpacityProperty)
fromState.Add((e.GetNewValue<double>(), from.IsVisible));
};
@ -93,9 +95,10 @@ namespace Avalonia.Base.UnitTests.Animation
// Run the last frame.
clock.Pulse(TimeSpan.FromMilliseconds(time));
// Check that opacity is reset to default value (1.0) but control is not visible.
Assert.Equal(10, fromState.Count);
Assert.Equal((1.0, false), fromState[9]);
// Control should be hidden before transparency being reset.
Assert.Equal(11, fromState.Count);
Assert.Equal((0.9, false), fromState[9]);
Assert.Equal((1.0, false), fromState[10]);
}
[Fact]
@ -169,8 +172,13 @@ namespace Avalonia.Base.UnitTests.Animation
time += 100;
}
// First frame should be of a fully transparent visible control.
Assert.Equal((0.0, true), toState[0]);
// Control should be made transparent before shown.
Assert.Equal((0.0, false), toState[0]);
Assert.Equal((0.0, true), toState[1]);
// Control should remain visible.
Assert.True(task.IsCompleted);
Assert.True(to.IsVisible);
}
private static IDisposable Start()

9
tests/Avalonia.Base.UnitTests/Animation/PageSlideTests.cs

@ -60,6 +60,8 @@ namespace Avalonia.Base.UnitTests.Animation
from.PropertyChanged += (s, e) =>
{
if (e.Property == Visual.IsVisibleProperty)
fromState.Add((from.GetValue(TranslateTransform.XProperty), e.GetNewValue<bool>()));
if (e.Property == TranslateTransform.XProperty)
fromState.Add((e.GetNewValue<double>(), from.IsVisible));
};
@ -84,9 +86,10 @@ namespace Avalonia.Base.UnitTests.Animation
// Run the last frame.
clock.Pulse(TimeSpan.FromMilliseconds(time));
// Check that X translate is reset to default value (0.0) but control is not visible.
Assert.Equal(10, fromState.Count);
Assert.Equal((0.0, false), fromState[9]);
// Control should be hidden before translate being reset.
Assert.Equal(11, fromState.Count);
Assert.Equal((-900.0, false), fromState[9]);
Assert.Equal((0.0, false), fromState[10]);
}
[Fact]

Loading…
Cancel
Save