diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 3bae7edb00..3047b1e519 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -56,7 +56,7 @@ - + diff --git a/samples/RenderDemo/SideBar.xaml b/samples/RenderDemo/SideBar.xaml index 3af90f1844..e37b9bb5fc 100644 --- a/samples/RenderDemo/SideBar.xaml +++ b/samples/RenderDemo/SideBar.xaml @@ -47,7 +47,7 @@ - + diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 4b0f76c5d5..3a3d00b94a 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -7,7 +7,7 @@ using System.Linq; using System.Reactive.Linq; using Avalonia.Collections; using Avalonia.Data; -using Avalonia.Animation.Animators; +using Avalonia.Animation.Animators; namespace Avalonia.Animation { @@ -36,13 +36,27 @@ namespace Avalonia.Animation private Transitions _transitions; + private Dictionary _previousTransitions; + /// /// Gets or sets the property transitions for the control. /// public Transitions Transitions { - get { return _transitions ?? (_transitions = new Transitions()); } - set { SetAndRaise(TransitionsProperty, ref _transitions, value); } + get + { + if (_transitions == null) + _transitions = new Transitions(); + + if (_previousTransitions == null) + _previousTransitions = new Dictionary(); + + return _transitions; + } + set + { + SetAndRaise(TransitionsProperty, ref _transitions, value); + } } /// @@ -52,13 +66,18 @@ namespace Avalonia.Animation /// The event args. protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) { - if (e.Priority != BindingPriority.Animation && Transitions != null) + if (e.Priority != BindingPriority.Animation && Transitions != null && _previousTransitions != null) { var match = Transitions.FirstOrDefault(x => x.Property == e.Property); if (match != null) { - match.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue); + if (_previousTransitions.TryGetValue(e.Property, out var dispose)) + dispose.Dispose(); + + var instance = match.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue); + + _previousTransitions[e.Property] = instance; } } } diff --git a/src/Avalonia.Animation/Properties/AssemblyInfo.cs b/src/Avalonia.Animation/Properties/AssemblyInfo.cs index 985a8e5bfe..eb38a66a84 100644 --- a/src/Avalonia.Animation/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Animation/Properties/AssemblyInfo.cs @@ -3,7 +3,10 @@ using Avalonia.Metadata; using System.Reflection; +using System.Runtime.CompilerServices; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Easings")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")] \ No newline at end of file +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")] + +[assembly: InternalsVisibleTo("Avalonia.LeakTests")] diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index eff2c4e9f3..10ea6bf523 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -30,13 +30,16 @@ namespace Avalonia.Animation { var interpVal = (double)t.Ticks / _duration.Ticks; - if (interpVal > 1d || interpVal < 0d) + // Clamp interpolation value. + if (interpVal >= 1d | interpVal < 0d) { + PublishNext(1d); PublishCompleted(); - return; } - - PublishNext(interpVal); + else + { + PublishNext(interpVal); + } } protected override void Unsubscribed() diff --git a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs new file mode 100644 index 0000000000..f1b4b0d071 --- /dev/null +++ b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Avalonia.Data; +using Xunit; +using Avalonia.Animation.Easings; + +namespace Avalonia.Animation.UnitTests +{ + public class TransitionsTests + { + [Fact] + public void Check_Transitions_Interpolation_Negative_Bounds_Clamp() + { + var clock = new MockGlobalClock(); + + using (UnitTestApplication.Start(new TestServices(globalClock: clock))) + { + var border = new Border + { + Transitions = + { + new DoubleTransition + { + Duration = TimeSpan.FromSeconds(1), + Property = Border.OpacityProperty, + } + } + }; + + border.Opacity = 0; + + clock.Pulse(TimeSpan.FromSeconds(0)); + clock.Pulse(TimeSpan.FromSeconds(-0.5)); + + Assert.Equal(0, border.Opacity); + } + } + + [Fact] + public void Check_Transitions_Interpolation_Positive_Bounds_Clamp() + { + var clock = new MockGlobalClock(); + + using (UnitTestApplication.Start(new TestServices(globalClock: clock))) + { + var border = new Border + { + Transitions = + { + new DoubleTransition + { + Duration = TimeSpan.FromSeconds(1), + Property = Border.OpacityProperty, + } + } + }; + + border.Opacity = 0; + + clock.Pulse(TimeSpan.FromSeconds(0)); + clock.Pulse(TimeSpan.FromMilliseconds(1001)); + + Assert.Equal(0, border.Opacity); + } + } + } +} diff --git a/tests/Avalonia.LeakTests/TransitionTests.cs b/tests/Avalonia.LeakTests/TransitionTests.cs new file mode 100644 index 0000000000..c7add1fe11 --- /dev/null +++ b/tests/Avalonia.LeakTests/TransitionTests.cs @@ -0,0 +1,60 @@ +using System; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.UnitTests; +using JetBrains.dotMemoryUnit; +using Xunit; +using Xunit.Abstractions; + +namespace Avalonia.LeakTests +{ + [DotMemoryUnit(FailIfRunWithoutSupport = false)] + public class TransitionTests + { + public TransitionTests(ITestOutputHelper atr) + { + DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); + } + + [Fact(Skip = "TODO: Fix this leak")] + public void Transition_On_StyledProperty_Is_Freed() + { + var clock = new MockGlobalClock(); + + using (UnitTestApplication.Start(new TestServices(globalClock: clock))) + { + Func run = () => + { + var border = new Border + { + Transitions = + { + new DoubleTransition + { + Duration = TimeSpan.FromSeconds(1), + Property = Border.OpacityProperty, + } + } + }; + + border.Opacity = 0; + + clock.Pulse(TimeSpan.FromSeconds(0)); + clock.Pulse(TimeSpan.FromSeconds(0.5)); + + Assert.Equal(0.5, border.Opacity); + + clock.Pulse(TimeSpan.FromSeconds(1)); + + Assert.Equal(0, border.Opacity); + return border; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + } + } +} diff --git a/tests/Avalonia.UnitTests/MockGlobalClock.cs b/tests/Avalonia.UnitTests/MockGlobalClock.cs new file mode 100644 index 0000000000..b53e5acc01 --- /dev/null +++ b/tests/Avalonia.UnitTests/MockGlobalClock.cs @@ -0,0 +1,10 @@ +using System; +using Avalonia.Animation; + +namespace Avalonia.UnitTests +{ + public class MockGlobalClock : ClockBase, IGlobalClock + { + public new void Pulse(TimeSpan systemTime) => base.Pulse(systemTime); + } +} diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index d68f1d167a..f7a878feba 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -16,6 +16,7 @@ using System.Reactive.Concurrency; using System.Collections.Generic; using Avalonia.Controls; using System.Reflection; +using Avalonia.Animation; namespace Avalonia.UnitTests { @@ -58,6 +59,7 @@ namespace Avalonia.UnitTests public TestServices( IAssetLoader assetLoader = null, IFocusManager focusManager = null, + IGlobalClock globalClock = null, IInputManager inputManager = null, Func keyboardDevice = null, IKeyboardNavigationHandler keyboardNavigation = null, @@ -75,6 +77,7 @@ namespace Avalonia.UnitTests { AssetLoader = assetLoader; FocusManager = focusManager; + GlobalClock = globalClock; InputManager = inputManager; KeyboardDevice = keyboardDevice; KeyboardNavigation = keyboardNavigation; @@ -93,6 +96,7 @@ namespace Avalonia.UnitTests public IAssetLoader AssetLoader { get; } public IInputManager InputManager { get; } public IFocusManager FocusManager { get; } + public IGlobalClock GlobalClock { get; } public Func KeyboardDevice { get; } public IKeyboardNavigationHandler KeyboardNavigation { get; } public Func MouseDevice { get; } @@ -109,6 +113,7 @@ namespace Avalonia.UnitTests public TestServices With( IAssetLoader assetLoader = null, IFocusManager focusManager = null, + IGlobalClock globalClock = null, IInputManager inputManager = null, Func keyboardDevice = null, IKeyboardNavigationHandler keyboardNavigation = null, @@ -127,6 +132,7 @@ namespace Avalonia.UnitTests return new TestServices( assetLoader: assetLoader ?? AssetLoader, focusManager: focusManager ?? FocusManager, + globalClock: globalClock ?? GlobalClock, inputManager: inputManager ?? InputManager, keyboardDevice: keyboardDevice ?? KeyboardDevice, keyboardNavigation: keyboardNavigation ?? KeyboardNavigation, diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index 4802278c1e..3578471397 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -12,6 +12,7 @@ using Avalonia.Threading; using System.Reactive.Disposables; using System.Reactive.Concurrency; using Avalonia.Input.Platform; +using Avalonia.Animation; namespace Avalonia.UnitTests { @@ -52,6 +53,7 @@ namespace Avalonia.UnitTests AvaloniaLocator.CurrentMutable .Bind().ToConstant(Services.AssetLoader) .Bind().ToConstant(Services.FocusManager) + .Bind().ToConstant(Services.GlobalClock) .BindToSelf(this) .Bind().ToConstant(Services.InputManager) .Bind().ToConstant(Services.KeyboardDevice?.Invoke())