Browse Source

Merge pull request #6183 from AvaloniaUI/feature/gradient-animations

Gradient animations
Jumar Macato 4 years ago
parent
commit
baff713b70

+ 149 - 0
samples/RenderDemo/Pages/AnimationsPage.xaml

@@ -161,6 +161,151 @@
           </Animation>
         </Style.Animations>
       </Style>
+
+      <Style Selector="Border.Rect7">
+        <Style.Animations>
+          <Animation Duration="0:0:3"
+                     IterationCount="Infinite"
+                     PlaybackDirection="Alternate">
+            <KeyFrame Cue="0%">
+              <Setter Property="Background" Value="Red" />
+            </KeyFrame>
+            <KeyFrame Cue="30%">
+              <Setter Property="Background">
+                <LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </LinearGradientBrush>
+              </Setter>
+            </KeyFrame>
+            <KeyFrame Cue="60%">
+              <Setter Property="Background" Value="Blue" />
+            </KeyFrame>
+            <KeyFrame Cue="100%">
+              <Setter Property="Background">
+                <LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
+                  <GradientStop Offset="0" Color="Green"/>
+                  <GradientStop Offset="1" Color="Yellow"/>
+                </LinearGradientBrush>
+              </Setter>
+            </KeyFrame>
+          </Animation>
+        </Style.Animations>
+      </Style>
+
+      <Style Selector="Border.Rect8">
+        <Style.Animations>
+          <Animation Duration="0:0:3"
+                     IterationCount="Infinite"
+                     PlaybackDirection="Alternate">
+            <KeyFrame Cue="0%">
+              <Setter Property="Background">
+                <LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </LinearGradientBrush>
+              </Setter>
+            </KeyFrame>
+            <KeyFrame Cue="50%">
+              <Setter Property="Background">
+                <LinearGradientBrush StartPoint="100%,0%" EndPoint="0%,100%">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="0.25" Color="Blue"/>
+                  <GradientStop Offset="0.5" Color="Blue"/>
+                  <GradientStop Offset="0.75" Color="Green"/>
+                  <GradientStop Offset="1" Color="Yellow"/>
+                </LinearGradientBrush>
+              </Setter>
+            </KeyFrame>
+            <KeyFrame Cue="100%">
+              <Setter Property="Background">
+                <LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </LinearGradientBrush>
+              </Setter>
+            </KeyFrame>
+          </Animation>
+        </Style.Animations>
+      </Style>
+
+      <Style Selector="Border.Rect9">
+        <Style.Animations>
+          <Animation Duration="0:0:3"
+                     IterationCount="Infinite"
+                     PlaybackDirection="Alternate">
+            <KeyFrame Cue="0%">
+              <Setter Property="Background">
+                <ConicGradientBrush Center="50%,50%" Angle="0">
+                  <GradientStop Offset="0" Color="Blue"/>
+                  <GradientStop Offset="0.5" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </ConicGradientBrush>
+              </Setter>
+            </KeyFrame>
+            <KeyFrame Cue="100%">
+              <Setter Property="Background">
+                <ConicGradientBrush Center="50%,70%" Angle="90">
+                  <GradientStop Offset="0" Color="Green"/>
+                  <GradientStop Offset="0.25" Color="Yellow"/>
+                  <GradientStop Offset="0.5" Color="Red"/>
+                  <GradientStop Offset="0.75" Color="Blue"/>
+                  <GradientStop Offset="1" Color="Green"/>
+                </ConicGradientBrush>
+              </Setter>
+            </KeyFrame>
+          </Animation>
+        </Style.Animations>
+      </Style>
+
+      <Style Selector="Border.Rect10">
+        <Style.Animations>
+          <Animation Duration="0:0:3"
+                     IterationCount="Infinite"
+                     PlaybackDirection="Normal">
+            <KeyFrame Cue="0%">
+              <Setter Property="Background">
+                <RadialGradientBrush Center="0%,100%" Radius="0.8">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </RadialGradientBrush>
+              </Setter>
+            </KeyFrame>
+            <KeyFrame Cue="25%">
+              <Setter Property="Background">
+                <RadialGradientBrush Center="0%,0%" Radius="1">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </RadialGradientBrush>
+              </Setter>
+            </KeyFrame>
+            <KeyFrame Cue="50%">
+              <Setter Property="Background">
+                <RadialGradientBrush Center="100%,0%" Radius="0.8">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </RadialGradientBrush>
+              </Setter>
+            </KeyFrame>
+            <KeyFrame Cue="75%">
+              <Setter Property="Background">
+                <RadialGradientBrush Center="100%,100%" Radius="1">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </RadialGradientBrush>
+              </Setter>
+            </KeyFrame>
+            <KeyFrame Cue="100%">
+              <Setter Property="Background">
+                <RadialGradientBrush Center="0%,100%" Radius="0.8">
+                  <GradientStop Offset="0" Color="Red"/>
+                  <GradientStop Offset="1" Color="Blue"/>
+                </RadialGradientBrush>
+              </Setter>
+            </KeyFrame>
+          </Animation>
+        </Style.Animations>
+      </Style>
     </Styles>
   </UserControl.Styles>
   <Grid>
@@ -181,6 +326,10 @@
         <Border Classes="Test Rect6" Background="Red"/>
         <Border Classes="Test Shadow" CornerRadius="10" Child="{x:Null}" />
         <Border Classes="Test Shadow" CornerRadius="0 30 60 0" Child="{x:Null}" />
+        <Border Classes="Test Rect7" Child="{x:Null}" />
+        <Border Classes="Test Rect8" Child="{x:Null}" />
+        <Border Classes="Test Rect9" Child="{x:Null}" />
+        <Border Classes="Test Rect10" Child="{x:Null}" />
       </WrapPanel>
     </StackPanel>
   </Grid>

+ 80 - 4
samples/RenderDemo/Pages/TransitionsPage.xaml

@@ -167,13 +167,80 @@
       <Style Selector="Border.Rect11:pointerover">
         <Setter Property="Background" >
           <LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
-            <LinearGradientBrush.GradientStops>
-              <GradientStop Offset="0" Color="Red"/>
-              <GradientStop Offset="1" Color="Blue"/>
-            </LinearGradientBrush.GradientStops>
+            <GradientStop Offset="0" Color="Red"/>
+            <GradientStop Offset="1" Color="Blue"/>
           </LinearGradientBrush>
         </Setter>
       </Style>
+
+      <Style Selector="Border.Rect12">
+        <Setter Property="Transitions">
+          <Transitions>
+            <BrushTransition Property="Background" Duration="0:0:0.5" />
+          </Transitions>
+        </Setter>
+        <Setter Property="Background" >
+          <LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
+            <GradientStop Offset="0" Color="Red"/>
+            <GradientStop Offset="1" Color="Blue"/>
+          </LinearGradientBrush>
+        </Setter>
+      </Style>
+
+      <Style Selector="Border.Rect12:pointerover">
+        <Setter Property="Background" >
+          <LinearGradientBrush StartPoint="100%,0%" EndPoint="0%,100%">
+            <GradientStop Offset="0" Color="Green"/>
+            <GradientStop Offset="1" Color="Yellow"/>
+          </LinearGradientBrush>
+        </Setter>
+      </Style>
+
+      <Style Selector="Border.Rect13">
+        <Setter Property="Transitions">
+          <Transitions>
+            <BrushTransition Property="Background" Duration="0:0:0.5" />
+          </Transitions>
+        </Setter>
+        <Setter Property="Background" >
+          <ConicGradientBrush Center="50%,50%" Angle="0">
+            <GradientStop Offset="0" Color="Red"/>
+            <GradientStop Offset="1" Color="Blue"/>
+          </ConicGradientBrush>
+        </Setter>
+      </Style>
+
+      <Style Selector="Border.Rect13:pointerover">
+        <Setter Property="Background" >
+          <ConicGradientBrush Center="70%,70%" Angle="90">
+            <GradientStop Offset="0" Color="Green"/>
+            <GradientStop Offset="1" Color="Yellow"/>
+          </ConicGradientBrush>
+        </Setter>
+      </Style>
+
+      <Style Selector="Border.Rect14">
+        <Setter Property="Transitions">
+          <Transitions>
+            <BrushTransition Property="Background" Duration="0:0:0.5" />
+          </Transitions>
+        </Setter>
+        <Setter Property="Background" >
+          <RadialGradientBrush Center="50%,50%" Radius="0.5">
+            <GradientStop Offset="0" Color="Red"/>
+            <GradientStop Offset="1" Color="Blue"/>
+          </RadialGradientBrush>
+        </Setter>
+      </Style>
+
+      <Style Selector="Border.Rect14:pointerover">
+        <Setter Property="Background" >
+          <RadialGradientBrush Center="30%,30%" Radius="0.2">
+            <GradientStop Offset="0" Color="Green"/>
+            <GradientStop Offset="1" Color="Yellow"/>
+          </RadialGradientBrush>
+        </Setter>
+      </Style>
     </Styles>
   </UserControl.Styles>
 
@@ -202,6 +269,15 @@
 
         <Border Classes="Test Rect10" />
         <Border Classes="Test Rect11" />
+
+        <Border Classes="Test Rect12" Child="{x:Null}"/>
+        <Border Classes="Test Rect13" Child="{x:Null}"/>
+        <Border Classes="Test Rect14" Child="{x:Null}"/>
+        
+        <Border Classes="Test Rect14" />
+        <Border Classes="Test Rect14" />
+        <Border Classes="Test Rect14" />
+        <Border Classes="Test Rect14" />
       </WrapPanel>
     </StackPanel>
   </Grid>

+ 118 - 20
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
 {
     /// <summary>
@@ -12,10 +15,8 @@ namespace Avalonia.Animation.Animators
     /// redirect them to the properly registered
     /// animators in this class.
     /// </summary>
-    public class BaseBrushAnimator : Animator<IBrush>
+    public class BaseBrushAnimator : Animator<IBrush?>
     {
-        private IAnimator _targetAnimator;
-
         private static readonly List<(Func<Type, bool> Match, Type AnimatorType)> _brushAnimators =
             new List<(Func<Type, bool> Match, Type AnimatorType)>();
 
@@ -31,7 +32,7 @@ namespace Avalonia.Animation.Animators
         /// The type of the animator to instantiate.
         /// </typeparam>
         public static void RegisterBrushAnimator<TAnimator>(Func<Type, bool> condition)
-            where TAnimator : IAnimator
+            where TAnimator : IAnimator, new()
         {
             _brushAnimators.Insert(0, (condition, typeof(TAnimator)));
         }
@@ -40,30 +41,127 @@ namespace Avalonia.Animation.Animators
         public override IDisposable Apply(Animation animation, Animatable control, IClock clock,
             IObservable<bool> 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);
+        /// <summary>
+        /// Fallback implementation of <see cref="IBrush"/> animation.
+        /// </summary>
+        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;
         }
 
-        /// <inheritdoc/>
-        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;
+        }
     }
 }

+ 123 - 0
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
+{
+    /// <summary>
+    /// Animator that handles <see cref="SolidColorBrush"/> values. 
+    /// </summary>
+    public class GradientBrushAnimator : Animator<IGradientBrush?>
+    {
+        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<IGradientBrush?> instance)
+        {
+            return control.Bind((AvaloniaProperty<IBrush?>)Property, instance, BindingPriority.Animation);
+        }
+
+        private IReadOnlyList<ImmutableGradientStop> InterpolateStops(double progress, IReadOnlyList<IGradientStop> oldValue, IReadOnlyList<IGradientStop> 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<ImmutableGradientStop> CreateStopsFromSolidColorBrush(ISolidColorBrush solidColorBrush, IReadOnlyList<IGradientStop> 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;
+            }
+        }
+    }
+}

+ 20 - 0
src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs

@@ -0,0 +1,20 @@
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="RelativePoint"/> properties.
+    /// </summary>
+    public class RelativePointAnimator : Animator<RelativePoint>
+    {
+        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);
+        }
+    }
+}

+ 11 - 9
src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs

@@ -3,37 +3,39 @@ using Avalonia.Data;
 using Avalonia.Media;
 using Avalonia.Media.Immutable;
 
+#nullable enable
+
 namespace Avalonia.Animation.Animators
 {
     /// <summary>
     /// Animator that handles <see cref="SolidColorBrush"/> values. 
     /// </summary>
-    public class ISolidColorBrushAnimator : Animator<ISolidColorBrush>
+    public class ISolidColorBrushAnimator : Animator<ISolidColorBrush?>
     {
-        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<ISolidColorBrush> instance)
+        public override IDisposable BindAnimation(Animatable control, IObservable<ISolidColorBrush?> instance)
         {
-            return control.Bind((AvaloniaProperty<IBrush>)Property, instance, BindingPriority.Animation);
+            return control.Bind((AvaloniaProperty<IBrush?>)Property, instance, BindingPriority.Animation);
         }
     }
     
-    [Obsolete]    
-    public class SolidColorBrushAnimator : Animator<SolidColorBrush>
+    [Obsolete("Use ISolidColorBrushAnimator instead")]
+    public class SolidColorBrushAnimator : Animator<SolidColorBrush?>
     {    
-        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));

+ 27 - 17
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
 {
     /// <summary>
     /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="IBrush"/> type.
-    /// Only values of <see cref="ISolidColorBrush"/> will transition correctly at the moment.
     /// </summary>
     public class BrushTransition : Transition<IBrush?>
     {
-        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<IBrush?> DoTransition(IObservable<double> 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<ISolidColorBrush, ISolidColorBrushAnimator>(
-                    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<IGradientBrush?, GradientBrushAnimator>(s_gradientAnimator, progress, Easing, oldGradient, newGradient);
+                }
+                else if (newValue is ISolidColorBrush newSolidColorBrushToConvert)
+                {
+                    var convertedSolidColorBrush = GradientBrushAnimator.ConvertSolidColorBrushToGradient(oldGradient, newSolidColorBrushToConvert);
+                    return new AnimatorTransitionObservable<IGradientBrush?, GradientBrushAnimator>(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<IGradientBrush?, GradientBrushAnimator>(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<ISolidColorBrush?, ISolidColorBrushAnimator>(s_solidColorBrushAnimator, progress, Easing, oldSolidColorBrush, newSolidColorBrush);
             }
 
-            return brush as ISolidColorBrush;
+            return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue);
         }
 
-        private class IncompatibleTransitionObservable : TransitionObservableBase<IBrush?>
+        private sealed class IncompatibleTransitionObservable : TransitionObservableBase<IBrush?>
         {
             private readonly IBrush? _from;
             private readonly IBrush? _to;
@@ -52,7 +62,7 @@ namespace Avalonia.Animation
 
             protected override IBrush? ProduceValue(double progress)
             {
-                return progress < 0.5 ? _from : _to;
+                return progress >= 0.5 ? _to : _from;
             }
         }
     }

+ 11 - 0
src/Avalonia.Visuals/Animation/Transitions/RelativePointTransition.cs

@@ -0,0 +1,11 @@
+using Avalonia.Animation.Animators;
+
+namespace Avalonia.Animation
+{
+    /// <summary>
+    /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="RelativePoint"/> type.
+    /// </summary>  
+    public class RelativePointTransition : AnimatorDrivenTransition<RelativePoint, RelativePointAnimator>
+    {
+    }
+}

+ 2 - 0
src/Avalonia.Visuals/Media/GradientBrush.cs

@@ -2,6 +2,8 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.ComponentModel;
+
+using Avalonia.Animation.Animators;
 using Avalonia.Collections;
 using Avalonia.Metadata;
 

+ 0 - 1
src/Avalonia.Visuals/Media/SolidColorBrush.cs

@@ -16,7 +16,6 @@ namespace Avalonia.Media
 
         static SolidColorBrush()
         {
-            BaseBrushAnimator.RegisterBrushAnimator<ISolidColorBrushAnimator>(match => typeof(ISolidColorBrush).IsAssignableFrom(match));
             AffectsRender<SolidColorBrush>(ColorProperty);
         }
 

+ 7 - 0
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 @@ namespace Avalonia
 
         private readonly RelativeUnit _unit;
 
+        static RelativePoint()
+        {
+            Animation.Animation.RegisterAnimator<RelativePointAnimator>(prop => typeof(RelativePoint).IsAssignableFrom(prop.PropertyType));
+        }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="RelativePoint"/> struct.
         /// </summary>