Browse Source

Reflection free implementation with automatic convertion from solid color brush to gradient

pull/6183/head
Max Katz 5 years ago
parent
commit
a885e673c8
  1. 34
      samples/RenderDemo/Pages/AnimationsPage.xaml
  2. 129
      src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs
  3. 65
      src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs
  4. 18
      src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs
  5. 40
      src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs
  6. 1
      src/Avalonia.Visuals/Media/GradientBrush.cs
  7. 1
      src/Avalonia.Visuals/Media/SolidColorBrush.cs

34
samples/RenderDemo/Pages/AnimationsPage.xaml

@ -168,6 +168,9 @@
IterationCount="Infinite" IterationCount="Infinite"
PlaybackDirection="Alternate"> PlaybackDirection="Alternate">
<KeyFrame Cue="0%"> <KeyFrame Cue="0%">
<Setter Property="Background" Value="Red" />
</KeyFrame>
<KeyFrame Cue="30%">
<Setter Property="Background"> <Setter Property="Background">
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%"> <LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Offset="0" Color="Red"/> <GradientStop Offset="0" Color="Red"/>
@ -175,6 +178,9 @@
</LinearGradientBrush> </LinearGradientBrush>
</Setter> </Setter>
</KeyFrame> </KeyFrame>
<KeyFrame Cue="60%">
<Setter Property="Background" Value="Blue" />
</KeyFrame>
<KeyFrame Cue="100%"> <KeyFrame Cue="100%">
<Setter Property="Background"> <Setter Property="Background">
<LinearGradientBrush StartPoint="100%,0%" EndPoint="0%,100%"> <LinearGradientBrush StartPoint="100%,0%" EndPoint="0%,100%">
@ -188,6 +194,31 @@
</Style> </Style>
<Style Selector="Border.Rect8"> <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="100%,100%">
<GradientStop Offset="0" Color="Red"/>
<GradientStop Offset="1" Color="Blue"/>
</LinearGradientBrush>
</Setter>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Background">
<LinearGradientBrush StartPoint="100%,0%" EndPoint="0%,100%">
<GradientStop Offset="0" Color="Green"/>
<GradientStop Offset="1" Color="Yellow"/>
</LinearGradientBrush>
</Setter>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.Rect9">
<Style.Animations> <Style.Animations>
<Animation Duration="0:0:3" <Animation Duration="0:0:3"
IterationCount="Infinite" IterationCount="Infinite"
@ -212,7 +243,7 @@
</Style.Animations> </Style.Animations>
</Style> </Style>
<Style Selector="Border.Rect9"> <Style Selector="Border.Rect10">
<Style.Animations> <Style.Animations>
<Animation Duration="0:0:3" <Animation Duration="0:0:3"
IterationCount="Infinite" IterationCount="Infinite"
@ -259,6 +290,7 @@
<Border Classes="Test Rect7" Child="{x:Null}" /> <Border Classes="Test Rect7" Child="{x:Null}" />
<Border Classes="Test Rect8" Child="{x:Null}" /> <Border Classes="Test Rect8" Child="{x:Null}" />
<Border Classes="Test Rect9" Child="{x:Null}" /> <Border Classes="Test Rect9" Child="{x:Null}" />
<Border Classes="Test Rect10" Child="{x:Null}" />
</WrapPanel> </WrapPanel>
</StackPanel> </StackPanel>
</Grid> </Grid>

129
src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Disposables; using System.Diagnostics.CodeAnalysis;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Media; using Avalonia.Media;
@ -16,8 +17,6 @@ namespace Avalonia.Animation.Animators
/// </summary> /// </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 = private static readonly List<(Func<Type, bool> Match, Type AnimatorType)> _brushAnimators =
new List<(Func<Type, bool> Match, Type AnimatorType)>(); new List<(Func<Type, bool> Match, Type AnimatorType)>();
@ -42,41 +41,127 @@ namespace Avalonia.Animation.Animators
public override IDisposable Apply(Animation animation, Animatable control, IClock clock, public override IDisposable Apply(Animation animation, Animatable control, IClock clock,
IObservable<bool> match, Action onComplete) IObservable<bool> match, Action onComplete)
{ {
_targetAnimator = CreateAnimatorFromType(this[0].Value.GetType()); if (TryCreateCustomRegisteredAnimator(out var animator)
|| TryCreateGradientAnimator(out animator)
|| TryCreateSolidColorBrushAnimator(out animator))
{
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);
}
/// <summary>
/// Fallback implementation of <see cref="IBrush"/> animation.
/// </summary>
public override IBrush? Interpolate(double progress, IBrush? oldValue, IBrush? newValue) => progress >= 0.5 ? newValue : oldValue;
if (_targetAnimator != null) private bool TryCreateGradientAnimator([NotNullWhen(true)] out IAnimator? animator)
{
IGradientBrush? firstGradient = null;
foreach (var keyframe in this)
{ {
foreach (var keyframe in this) if (keyframe.Value is IGradientBrush gradientBrush)
{ {
_targetAnimator.Add(keyframe); firstGradient = gradientBrush;
break;
} }
}
if (firstGradient is null)
{
animator = null;
return false;
}
_targetAnimator.Property = this.Property; var gradientAnimator = new IGradientBrushAnimator();
gradientAnimator.Property = Property;
return _targetAnimator.Apply(animation, control, clock, match, onComplete); foreach (var keyframe in this)
{
if (keyframe.Value is ISolidColorBrush solidColorBrush)
{
gradientAnimator.Add(new AnimatorKeyFrame(typeof(IGradientBrushAnimator), keyframe.Cue, keyframe.KeySpline)
{
Value = IGradientBrushAnimator.ConvertSolidColorBrushToGradient(firstGradient, solidColorBrush)
});
}
else if (keyframe.Value is IGradientBrush)
{
gradientAnimator.Add(new AnimatorKeyFrame(typeof(IGradientBrushAnimator), keyframe.Cue, keyframe.KeySpline)
{
Value = keyframe.Value
});
}
else
{
animator = null;
return false;
}
} }
Logger.TryGet(LogEventLevel.Error, LogArea.Animations)?.Log( animator = gradientAnimator;
this, return true;
"The animation's keyframe values didn't match any brush animators registered in BaseBrushAnimator.");
return Disposable.Empty;
} }
/// <inheritdoc/> private bool TryCreateSolidColorBrushAnimator([NotNullWhen(true)] out IAnimator? animator)
public override IBrush? Interpolate(double progress, IBrush? oldValue, IBrush? newValue) => null; {
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;
}
}
internal static IAnimator? CreateAnimatorFromType(Type type) animator = solidColorBrushAnimator;
return true;
}
private bool TryCreateCustomRegisteredAnimator([NotNullWhen(true)] out IAnimator? animator)
{ {
foreach (var (match, animatorType) in _brushAnimators) if (_brushAnimators.Count > 0)
{ {
if (!match(type)) var firstKeyType = this[0].Value.GetType();
continue; foreach (var (match, animatorType) in _brushAnimators)
{
if (!match(firstKeyType))
continue;
return (IAnimator)Activator.CreateInstance(animatorType); 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;
}
}
} }
return null; animator = null;
return false;
} }
} }
} }

65
src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs

@ -1,27 +1,28 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
#nullable enable
namespace Avalonia.Animation.Animators namespace Avalonia.Animation.Animators
{ {
/// <summary> /// <summary>
/// Animator that handles <see cref="SolidColorBrush"/> values. /// Animator that handles <see cref="SolidColorBrush"/> values.
/// </summary> /// </summary>
public class IGradientBrushAnimator : Animator<IGradientBrush> public class IGradientBrushAnimator : Animator<IGradientBrush?>
{ {
private static readonly RelativePointAnimator s_relativePointAnimator = new RelativePointAnimator(); private static readonly RelativePointAnimator s_relativePointAnimator = new RelativePointAnimator();
private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator(); private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator();
public override IGradientBrush Interpolate(double progress, IGradientBrush oldValue, IGradientBrush newValue) public override IGradientBrush? Interpolate(double progress, IGradientBrush? oldValue, IGradientBrush? newValue)
{ {
if (oldValue is null || newValue is null if (oldValue is null || newValue is null
|| oldValue.GradientStops.Count != oldValue.GradientStops.Count) || oldValue.GradientStops.Count != newValue.GradientStops.Count)
{ {
return progress >= 1 ? newValue : oldValue; return progress >= 0.5 ? newValue : oldValue;
} }
switch (oldValue) switch (oldValue)
@ -52,23 +53,59 @@ namespace Avalonia.Animation.Animators
s_relativePointAnimator.Interpolate(progress, oldLinear.EndPoint, newLinear.EndPoint)); s_relativePointAnimator.Interpolate(progress, oldLinear.EndPoint, newLinear.EndPoint));
default: default:
return progress >= 1 ? newValue : oldValue; return progress >= 0.5 ? newValue : oldValue;
} }
} }
public override IDisposable BindAnimation(Animatable control, IObservable<IGradientBrush> instance) public override IDisposable BindAnimation(Animatable control, IObservable<IGradientBrush?> instance)
{ {
return control.Bind((AvaloniaProperty<IBrush>)Property, instance, BindingPriority.Animation); return control.Bind((AvaloniaProperty<IBrush?>)Property, instance, BindingPriority.Animation);
} }
private IReadOnlyList<ImmutableGradientStop> InterpolateStops(double progress, IReadOnlyList<IGradientStop> oldValue, IReadOnlyList<IGradientStop> newValue) private IReadOnlyList<ImmutableGradientStop> InterpolateStops(double progress, IReadOnlyList<IGradientStop> oldValue, IReadOnlyList<IGradientStop> newValue)
{ {
// pool var stops = new ImmutableGradientStop[oldValue.Count];
return oldValue for (int index = 0; index < oldValue.Count; index++)
.Zip(newValue, (f, s) => new ImmutableGradientStop( {
s_doubleAnimator.Interpolate(progress, f.Offset, s.Offset), stops[index] = new ImmutableGradientStop(
ColorAnimator.InterpolateCore(progress, f.Color, s.Color))) s_doubleAnimator.Interpolate(progress, oldValue[index].Offset, newValue[index].Offset),
.ToArray(); ColorAnimator.InterpolateCore(progress, oldValue[index].Color, newValue[index].Color));
}
return stops;
}
internal static IGradientBrush ConvertSolidColorBrushToGradient(IGradientBrush gradientBrush, ISolidColorBrush solidColorBrush)
{
switch (gradientBrush)
{
case IRadialGradientBrush oldRadial:
return new ImmutableRadialGradientBrush(
CreateStopsFromSolidColorBrush(solidColorBrush, oldRadial), solidColorBrush.Opacity,
oldRadial.SpreadMethod, oldRadial.Center, oldRadial.GradientOrigin, oldRadial.Radius);
case IConicGradientBrush oldConic:
return new ImmutableConicGradientBrush(
CreateStopsFromSolidColorBrush(solidColorBrush, oldConic), solidColorBrush.Opacity,
oldConic.SpreadMethod, oldConic.Center, oldConic.Angle);
case ILinearGradientBrush oldLinear:
return new ImmutableLinearGradientBrush(
CreateStopsFromSolidColorBrush(solidColorBrush, oldLinear), 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, IGradientBrush baseGradient)
{
var stops = new ImmutableGradientStop[baseGradient.GradientStops.Count];
for (int index = 0; index < baseGradient.GradientStops.Count; index++)
{
stops[index] = new ImmutableGradientStop(baseGradient.GradientStops[index].Offset, solidColorBrush.Color);
}
return stops;
}
} }
} }
} }

18
src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs

@ -3,37 +3,39 @@ using Avalonia.Data;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
#nullable enable
namespace Avalonia.Animation.Animators namespace Avalonia.Animation.Animators
{ {
/// <summary> /// <summary>
/// Animator that handles <see cref="SolidColorBrush"/> values. /// Animator that handles <see cref="SolidColorBrush"/> values.
/// </summary> /// </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) if (oldValue is null || newValue is null)
{ {
return progress >= 1 ? newValue : oldValue; return progress >= 0.5 ? newValue : oldValue;
} }
return new ImmutableSolidColorBrush(ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color)); 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("Use ISolidColorBrushAnimator instead")] [Obsolete("Use ISolidColorBrushAnimator instead")]
public class SolidColorBrushAnimator : Animator<SolidColorBrush> 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) 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)); return new SolidColorBrush(ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color));

40
src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs

@ -1,5 +1,4 @@
using System; using System;
using System.Linq;
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
using Avalonia.Animation.Easings; using Avalonia.Animation.Easings;
@ -14,34 +13,43 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public class BrushTransition : Transition<IBrush?> public class BrushTransition : Transition<IBrush?>
{ {
private static readonly IGradientBrushAnimator s_gradientAnimator = new IGradientBrushAnimator();
private static readonly ISolidColorBrushAnimator s_solidColorBrushAnimator = new ISolidColorBrushAnimator();
public override IObservable<IBrush?> DoTransition(IObservable<double> progress, IBrush? oldValue, IBrush? newValue) public override IObservable<IBrush?> DoTransition(IObservable<double> progress, IBrush? oldValue, IBrush? newValue)
{ {
var type = oldValue?.GetType() ?? newValue?.GetType(); if (oldValue is null || newValue is null)
if (type == null)
{ {
return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue);
} }
var animator = BaseBrushAnimator.CreateAnimatorFromType(type); if (oldValue is IGradientBrush oldGradient)
if (animator == null)
{ {
return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); if (newValue is IGradientBrush newGradient)
{
return new AnimatorTransitionObservable<IGradientBrush?, IGradientBrushAnimator>(s_gradientAnimator, progress, Easing, oldGradient, newGradient);
}
else if (newValue is ISolidColorBrush newSolidColorBrushToConvert)
{
var convertedSolidColorBrush = IGradientBrushAnimator.ConvertSolidColorBrushToGradient(oldGradient, newSolidColorBrushToConvert);
return new AnimatorTransitionObservable<IGradientBrush?, IGradientBrushAnimator>(s_gradientAnimator, progress, Easing, oldGradient, convertedSolidColorBrush);
}
}
else if (newValue is IGradientBrush newGradient && oldValue is ISolidColorBrush oldSolidColorBrushToConvert)
{
var convertedSolidColorBrush = IGradientBrushAnimator.ConvertSolidColorBrushToGradient(newGradient, oldSolidColorBrushToConvert);
return new AnimatorTransitionObservable<IGradientBrush?, IGradientBrushAnimator>(s_gradientAnimator, progress, Easing, convertedSolidColorBrush, newGradient);
} }
var animatorType = animator.GetType(); if (oldValue is ISolidColorBrush oldSolidColorBrush && newValue is ISolidColorBrush newSolidColorBrush)
var animatorGenericArgument = animatorType.BaseType.GetGenericArguments().FirstOrDefault() ?? type;
var observableType = typeof(AnimatorTransitionObservable<,>).MakeGenericType(animatorGenericArgument, animatorType);
var observable = Activator.CreateInstance(observableType, animator, progress, Easing, oldValue, newValue) as IObservable<IBrush>;
if (observable == null)
{ {
return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); return new AnimatorTransitionObservable<ISolidColorBrush?, ISolidColorBrushAnimator>(s_solidColorBrushAnimator, progress, Easing, oldSolidColorBrush, newSolidColorBrush);
} }
return observable; 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? _from;
private readonly IBrush? _to; private readonly IBrush? _to;
@ -54,7 +62,7 @@ namespace Avalonia.Animation
protected override IBrush? ProduceValue(double progress) protected override IBrush? ProduceValue(double progress)
{ {
return progress < 0.5 ? _from : _to; return progress >= 0.5 ? _to : _from;
} }
} }
} }

1
src/Avalonia.Visuals/Media/GradientBrush.cs

@ -30,7 +30,6 @@ namespace Avalonia.Media
static GradientBrush() static GradientBrush()
{ {
BaseBrushAnimator.RegisterBrushAnimator<IGradientBrushAnimator>(match => typeof(IGradientBrush).IsAssignableFrom(match));
GradientStopsProperty.Changed.Subscribe(GradientStopsChanged); GradientStopsProperty.Changed.Subscribe(GradientStopsChanged);
AffectsRender<LinearGradientBrush>(SpreadMethodProperty); AffectsRender<LinearGradientBrush>(SpreadMethodProperty);
} }

1
src/Avalonia.Visuals/Media/SolidColorBrush.cs

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

Loading…
Cancel
Save