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. ///