diff --git a/samples/RenderDemo/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml
index 21c7d68b5d6..9043bac33e7 100644
--- a/samples/RenderDemo/Pages/AnimationsPage.xaml
+++ b/samples/RenderDemo/Pages/AnimationsPage.xaml
@@ -161,6 +161,151 @@
+
+
+
+
+
+
+
+
@@ -181,6 +326,10 @@
+
+
+
+
diff --git a/samples/RenderDemo/Pages/TransitionsPage.xaml b/samples/RenderDemo/Pages/TransitionsPage.xaml
index 1985074b0f4..71b6ea0713a 100644
--- a/samples/RenderDemo/Pages/TransitionsPage.xaml
+++ b/samples/RenderDemo/Pages/TransitionsPage.xaml
@@ -167,13 +167,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -202,6 +269,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs
index 508891fd72e..ba7a3868c7b 100644
--- a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs
+++ b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs
@@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
-using System.Reactive.Disposables;
+using System.Diagnostics.CodeAnalysis;
+
using Avalonia.Logging;
using Avalonia.Media;
+#nullable enable
+
namespace Avalonia.Animation.Animators
{
///
@@ -12,10 +15,8 @@ namespace Avalonia.Animation.Animators
/// redirect them to the properly registered
/// animators in this class.
///
- public class BaseBrushAnimator : Animator
+ public class BaseBrushAnimator : Animator
{
- private IAnimator _targetAnimator;
-
private static readonly List<(Func Match, Type AnimatorType)> _brushAnimators =
new List<(Func Match, Type AnimatorType)>();
@@ -31,7 +32,7 @@ public class BaseBrushAnimator : Animator
/// The type of the animator to instantiate.
///
public static void RegisterBrushAnimator(Func condition)
- where TAnimator : IAnimator
+ where TAnimator : IAnimator, new()
{
_brushAnimators.Insert(0, (condition, typeof(TAnimator)));
}
@@ -40,30 +41,127 @@ public static void RegisterBrushAnimator(Func condition)
public override IDisposable Apply(Animation animation, Animatable control, IClock clock,
IObservable match, Action onComplete)
{
- foreach (var valueType in _brushAnimators)
+ if (TryCreateCustomRegisteredAnimator(out var animator)
+ || TryCreateGradientAnimator(out animator)
+ || TryCreateSolidColorBrushAnimator(out animator))
{
- if (!valueType.Match(this[0].Value.GetType())) continue;
+ return animator.Apply(animation, control, clock, match, onComplete);
+ }
+
+ Logger.TryGet(LogEventLevel.Error, LogArea.Animations)?.Log(
+ this,
+ "The animation's keyframe value types set is not supported.");
+
+ return base.Apply(animation, control, clock, match, onComplete);
+ }
- _targetAnimator = (IAnimator)Activator.CreateInstance(valueType.AnimatorType);
+ ///
+ /// Fallback implementation of animation.
+ ///
+ public override IBrush? Interpolate(double progress, IBrush? oldValue, IBrush? newValue) => progress >= 0.5 ? newValue : oldValue;
- foreach (var keyframe in this)
+ private bool TryCreateGradientAnimator([NotNullWhen(true)] out IAnimator? animator)
+ {
+ IGradientBrush? firstGradient = null;
+ foreach (var keyframe in this)
+ {
+ if (keyframe.Value is IGradientBrush gradientBrush)
{
- _targetAnimator.Add(keyframe);
+ firstGradient = gradientBrush;
+ break;
}
+ }
- _targetAnimator.Property = this.Property;
-
- return _targetAnimator.Apply(animation, control, clock, match, onComplete);
+ if (firstGradient is null)
+ {
+ animator = null;
+ return false;
}
- Logger.TryGet(LogEventLevel.Error, LogArea.Animations)?.Log(
- this,
- "The animation's keyframe values didn't match any brush animators registered in BaseBrushAnimator.");
-
- return Disposable.Empty;
+ var gradientAnimator = new GradientBrushAnimator();
+ gradientAnimator.Property = Property;
+
+ foreach (var keyframe in this)
+ {
+ if (keyframe.Value is ISolidColorBrush solidColorBrush)
+ {
+ gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), keyframe.Cue, keyframe.KeySpline)
+ {
+ Value = GradientBrushAnimator.ConvertSolidColorBrushToGradient(firstGradient, solidColorBrush)
+ });
+ }
+ else if (keyframe.Value is IGradientBrush)
+ {
+ gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), keyframe.Cue, keyframe.KeySpline)
+ {
+ Value = keyframe.Value
+ });
+ }
+ else
+ {
+ animator = null;
+ return false;
+ }
+ }
+
+ animator = gradientAnimator;
+ return true;
}
- ///
- public override IBrush Interpolate(double progress, IBrush oldValue, IBrush newValue) => null;
+ private bool TryCreateSolidColorBrushAnimator([NotNullWhen(true)] out IAnimator? animator)
+ {
+ var solidColorBrushAnimator = new ISolidColorBrushAnimator();
+ solidColorBrushAnimator.Property = Property;
+
+ foreach (var keyframe in this)
+ {
+ if (keyframe.Value is ISolidColorBrush)
+ {
+ solidColorBrushAnimator.Add(new AnimatorKeyFrame(typeof(ISolidColorBrushAnimator), keyframe.Cue, keyframe.KeySpline)
+ {
+ Value = keyframe.Value
+ });
+ }
+ else
+ {
+ animator = null;
+ return false;
+ }
+ }
+
+ animator = solidColorBrushAnimator;
+ return true;
+ }
+
+ private bool TryCreateCustomRegisteredAnimator([NotNullWhen(true)] out IAnimator? animator)
+ {
+ if (_brushAnimators.Count > 0)
+ {
+ var firstKeyType = this[0].Value.GetType();
+ foreach (var (match, animatorType) in _brushAnimators)
+ {
+ if (!match(firstKeyType))
+ continue;
+
+ animator = (IAnimator)Activator.CreateInstance(animatorType);
+ if (animator != null)
+ {
+ animator.Property = Property;
+ foreach (var keyframe in this)
+ {
+ animator.Add(new AnimatorKeyFrame(animatorType, keyframe.Cue, keyframe.KeySpline)
+ {
+ Value = keyframe.Value
+ });
+ }
+
+ return true;
+ }
+ }
+ }
+
+ animator = null;
+ return false;
+ }
}
}
diff --git a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs
new file mode 100644
index 00000000000..864e12413fc
--- /dev/null
+++ b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Collections.Generic;
+
+using Avalonia.Data;
+using Avalonia.Media;
+using Avalonia.Media.Immutable;
+
+#nullable enable
+
+namespace Avalonia.Animation.Animators
+{
+ ///
+ /// Animator that handles values.
+ ///
+ public class GradientBrushAnimator : Animator
+ {
+ private static readonly RelativePointAnimator s_relativePointAnimator = new RelativePointAnimator();
+ private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator();
+
+ public override IGradientBrush? Interpolate(double progress, IGradientBrush? oldValue, IGradientBrush? newValue)
+ {
+ if (oldValue is null || newValue is null)
+ {
+ return progress >= 0.5 ? newValue : oldValue;
+ }
+
+ switch (oldValue)
+ {
+ case IRadialGradientBrush oldRadial when newValue is IRadialGradientBrush newRadial:
+ return new ImmutableRadialGradientBrush(
+ InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops),
+ s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity),
+ oldValue.SpreadMethod,
+ s_relativePointAnimator.Interpolate(progress, oldRadial.Center, newRadial.Center),
+ s_relativePointAnimator.Interpolate(progress, oldRadial.GradientOrigin, newRadial.GradientOrigin),
+ s_doubleAnimator.Interpolate(progress, oldRadial.Radius, newRadial.Radius));
+
+ case IConicGradientBrush oldConic when newValue is IConicGradientBrush newConic:
+ return new ImmutableConicGradientBrush(
+ InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops),
+ s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity),
+ oldValue.SpreadMethod,
+ s_relativePointAnimator.Interpolate(progress, oldConic.Center, newConic.Center),
+ s_doubleAnimator.Interpolate(progress, oldConic.Angle, newConic.Angle));
+
+ case ILinearGradientBrush oldLinear when newValue is ILinearGradientBrush newLinear:
+ return new ImmutableLinearGradientBrush(
+ InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops),
+ s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity),
+ oldValue.SpreadMethod,
+ s_relativePointAnimator.Interpolate(progress, oldLinear.StartPoint, newLinear.StartPoint),
+ s_relativePointAnimator.Interpolate(progress, oldLinear.EndPoint, newLinear.EndPoint));
+
+ default:
+ return progress >= 0.5 ? newValue : oldValue;
+ }
+ }
+
+ public override IDisposable BindAnimation(Animatable control, IObservable instance)
+ {
+ return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation);
+ }
+
+ private IReadOnlyList InterpolateStops(double progress, IReadOnlyList oldValue, IReadOnlyList newValue)
+ {
+ var resultCount = Math.Max(oldValue.Count, newValue.Count);
+ var stops = new ImmutableGradientStop[resultCount];
+
+ for (int index = 0, oldIndex = 0, newIndex = 0; index < resultCount; index++)
+ {
+ stops[index] = new ImmutableGradientStop(
+ s_doubleAnimator.Interpolate(progress, oldValue[oldIndex].Offset, newValue[newIndex].Offset),
+ ColorAnimator.InterpolateCore(progress, oldValue[oldIndex].Color, newValue[newIndex].Color));
+
+ if (oldIndex < oldValue.Count - 1)
+ {
+ oldIndex++;
+ }
+
+ if (newIndex < newValue.Count - 1)
+ {
+ newIndex++;
+ }
+ }
+
+ return stops;
+ }
+
+ internal static IGradientBrush ConvertSolidColorBrushToGradient(IGradientBrush gradientBrush, ISolidColorBrush solidColorBrush)
+ {
+ switch (gradientBrush)
+ {
+ case IRadialGradientBrush oldRadial:
+ return new ImmutableRadialGradientBrush(
+ CreateStopsFromSolidColorBrush(solidColorBrush, oldRadial.GradientStops), solidColorBrush.Opacity,
+ oldRadial.SpreadMethod, oldRadial.Center, oldRadial.GradientOrigin, oldRadial.Radius);
+
+ case IConicGradientBrush oldConic:
+ return new ImmutableConicGradientBrush(
+ CreateStopsFromSolidColorBrush(solidColorBrush, oldConic.GradientStops), solidColorBrush.Opacity,
+ oldConic.SpreadMethod, oldConic.Center, oldConic.Angle);
+
+ case ILinearGradientBrush oldLinear:
+ return new ImmutableLinearGradientBrush(
+ CreateStopsFromSolidColorBrush(solidColorBrush, oldLinear.GradientStops), solidColorBrush.Opacity,
+ oldLinear.SpreadMethod, oldLinear.StartPoint, oldLinear.EndPoint);
+
+ default:
+ throw new NotSupportedException($"Gradient of type {gradientBrush?.GetType()} is not supported");
+ }
+
+ static IReadOnlyList CreateStopsFromSolidColorBrush(ISolidColorBrush solidColorBrush, IReadOnlyList baseStops)
+ {
+ var stops = new ImmutableGradientStop[baseStops.Count];
+ for (int index = 0; index < baseStops.Count; index++)
+ {
+ stops[index] = new ImmutableGradientStop(baseStops[index].Offset, solidColorBrush.Color);
+ }
+ return stops;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs
new file mode 100644
index 00000000000..348a2e4a352
--- /dev/null
+++ b/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs
@@ -0,0 +1,20 @@
+namespace Avalonia.Animation.Animators
+{
+ ///
+ /// Animator that handles properties.
+ ///
+ public class RelativePointAnimator : Animator
+ {
+ private static readonly PointAnimator s_pointAnimator = new PointAnimator();
+
+ public override RelativePoint Interpolate(double progress, RelativePoint oldValue, RelativePoint newValue)
+ {
+ if (oldValue.Unit != newValue.Unit)
+ {
+ return progress >= 0.5 ? newValue : oldValue;
+ }
+
+ return new RelativePoint(s_pointAnimator.Interpolate(progress, oldValue.Point, newValue.Point), oldValue.Unit);
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs
index a56cc1de8c2..7c6372aae23 100644
--- a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs
+++ b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs
@@ -3,37 +3,39 @@
using Avalonia.Media;
using Avalonia.Media.Immutable;
+#nullable enable
+
namespace Avalonia.Animation.Animators
{
///
/// Animator that handles values.
///
- public class ISolidColorBrushAnimator : Animator
+ public class ISolidColorBrushAnimator : Animator
{
- public override ISolidColorBrush Interpolate(double progress, ISolidColorBrush oldValue, ISolidColorBrush newValue)
+ public override ISolidColorBrush? Interpolate(double progress, ISolidColorBrush? oldValue, ISolidColorBrush? newValue)
{
if (oldValue is null || newValue is null)
{
- return oldValue;
+ return progress >= 0.5 ? newValue : oldValue;
}
return new ImmutableSolidColorBrush(ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color));
}
- public override IDisposable BindAnimation(Animatable control, IObservable instance)
+ public override IDisposable BindAnimation(Animatable control, IObservable instance)
{
- return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation);
+ return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation);
}
}
- [Obsolete]
- public class SolidColorBrushAnimator : Animator
+ [Obsolete("Use ISolidColorBrushAnimator instead")]
+ public class SolidColorBrushAnimator : Animator
{
- public override SolidColorBrush Interpolate(double progress, SolidColorBrush oldValue, SolidColorBrush newValue)
+ public override SolidColorBrush? Interpolate(double progress, SolidColorBrush? oldValue, SolidColorBrush? newValue)
{
if (oldValue is null || newValue is null)
{
- return oldValue;
+ return progress >= 0.5 ? newValue : oldValue;
}
return new SolidColorBrush(ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color));
diff --git a/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs
index cc5af1b4b1a..4d9c8af4d5f 100644
--- a/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs
+++ b/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs
@@ -1,4 +1,5 @@
using System;
+
using Avalonia.Animation.Animators;
using Avalonia.Animation.Easings;
using Avalonia.Media;
@@ -9,37 +10,46 @@ namespace Avalonia.Animation
{
///
/// Transition class that handles with type.
- /// Only values of will transition correctly at the moment.
///
public class BrushTransition : Transition
{
- private static readonly ISolidColorBrushAnimator s_animator = new ISolidColorBrushAnimator();
+ private static readonly GradientBrushAnimator s_gradientAnimator = new GradientBrushAnimator();
+ private static readonly ISolidColorBrushAnimator s_solidColorBrushAnimator = new ISolidColorBrushAnimator();
public override IObservable DoTransition(IObservable progress, IBrush? oldValue, IBrush? newValue)
{
- var oldSolidColorBrush = TryGetSolidColorBrush(oldValue);
- var newSolidColorBrush = TryGetSolidColorBrush(newValue);
-
- if (oldSolidColorBrush != null && newSolidColorBrush != null)
+ if (oldValue is null || newValue is null)
{
- return new AnimatorTransitionObservable(
- s_animator, progress, Easing, oldSolidColorBrush, newSolidColorBrush);
+ return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue);
}
- return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue);
- }
+ if (oldValue is IGradientBrush oldGradient)
+ {
+ if (newValue is IGradientBrush newGradient)
+ {
+ return new AnimatorTransitionObservable(s_gradientAnimator, progress, Easing, oldGradient, newGradient);
+ }
+ else if (newValue is ISolidColorBrush newSolidColorBrushToConvert)
+ {
+ var convertedSolidColorBrush = GradientBrushAnimator.ConvertSolidColorBrushToGradient(oldGradient, newSolidColorBrushToConvert);
+ return new AnimatorTransitionObservable(s_gradientAnimator, progress, Easing, oldGradient, convertedSolidColorBrush);
+ }
+ }
+ else if (newValue is IGradientBrush newGradient && oldValue is ISolidColorBrush oldSolidColorBrushToConvert)
+ {
+ var convertedSolidColorBrush = GradientBrushAnimator.ConvertSolidColorBrushToGradient(newGradient, oldSolidColorBrushToConvert);
+ return new AnimatorTransitionObservable(s_gradientAnimator, progress, Easing, convertedSolidColorBrush, newGradient);
+ }
- private static ISolidColorBrush? TryGetSolidColorBrush(IBrush? brush)
- {
- if (brush is null)
+ if (oldValue is ISolidColorBrush oldSolidColorBrush && newValue is ISolidColorBrush newSolidColorBrush)
{
- return Brushes.Transparent;
+ return new AnimatorTransitionObservable(s_solidColorBrushAnimator, progress, Easing, oldSolidColorBrush, newSolidColorBrush);
}
- return brush as ISolidColorBrush;
+ return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue);
}
- private class IncompatibleTransitionObservable : TransitionObservableBase
+ private sealed class IncompatibleTransitionObservable : TransitionObservableBase
{
private readonly IBrush? _from;
private readonly IBrush? _to;
@@ -52,7 +62,7 @@ public IncompatibleTransitionObservable(IObservable progress, Easing eas
protected override IBrush? ProduceValue(double progress)
{
- return progress < 0.5 ? _from : _to;
+ return progress >= 0.5 ? _to : _from;
}
}
}
diff --git a/src/Avalonia.Visuals/Animation/Transitions/RelativePointTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/RelativePointTransition.cs
new file mode 100644
index 00000000000..4a7bfa8384c
--- /dev/null
+++ b/src/Avalonia.Visuals/Animation/Transitions/RelativePointTransition.cs
@@ -0,0 +1,11 @@
+using Avalonia.Animation.Animators;
+
+namespace Avalonia.Animation
+{
+ ///
+ /// Transition class that handles with type.
+ ///
+ public class RelativePointTransition : AnimatorDrivenTransition
+ {
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/GradientBrush.cs b/src/Avalonia.Visuals/Media/GradientBrush.cs
index 99923b8e068..5f795d46440 100644
--- a/src/Avalonia.Visuals/Media/GradientBrush.cs
+++ b/src/Avalonia.Visuals/Media/GradientBrush.cs
@@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
+
+using Avalonia.Animation.Animators;
using Avalonia.Collections;
using Avalonia.Metadata;
diff --git a/src/Avalonia.Visuals/Media/SolidColorBrush.cs b/src/Avalonia.Visuals/Media/SolidColorBrush.cs
index fd94cbd2149..962819a1a13 100644
--- a/src/Avalonia.Visuals/Media/SolidColorBrush.cs
+++ b/src/Avalonia.Visuals/Media/SolidColorBrush.cs
@@ -16,7 +16,6 @@ public class SolidColorBrush : Brush, ISolidColorBrush
static SolidColorBrush()
{
- BaseBrushAnimator.RegisterBrushAnimator(match => typeof(ISolidColorBrush).IsAssignableFrom(match));
AffectsRender(ColorProperty);
}
diff --git a/src/Avalonia.Visuals/RelativePoint.cs b/src/Avalonia.Visuals/RelativePoint.cs
index 097ea69be45..497820ec652 100644
--- a/src/Avalonia.Visuals/RelativePoint.cs
+++ b/src/Avalonia.Visuals/RelativePoint.cs
@@ -1,5 +1,7 @@
using System;
using System.Globalization;
+
+using Avalonia.Animation.Animators;
using Avalonia.Utilities;
namespace Avalonia
@@ -45,6 +47,11 @@ public enum RelativeUnit
private readonly RelativeUnit _unit;
+ static RelativePoint()
+ {
+ Animation.Animation.RegisterAnimator(prop => typeof(RelativePoint).IsAssignableFrom(prop.PropertyType));
+ }
+
///
/// Initializes a new instance of the struct.
///