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())