diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs
index 324ff06452..7d6df716b8 100644
--- a/src/Avalonia.Animation/Animatable.cs
+++ b/src/Avalonia.Animation/Animatable.cs
@@ -1,10 +1,10 @@
using System;
+using System.Collections;
using System.Collections.Generic;
-using System.Linq;
-using System.Reactive.Linq;
-using Avalonia.Collections;
+using System.Collections.Specialized;
using Avalonia.Data;
-using Avalonia.Animation.Animators;
+
+#nullable enable
namespace Avalonia.Animation
{
@@ -13,15 +13,12 @@ namespace Avalonia.Animation
///
public class Animatable : AvaloniaObject
{
+ ///
+ /// Defines the property.
+ ///
public static readonly StyledProperty ClockProperty =
AvaloniaProperty.Register(nameof(Clock), inherits: true);
- public IClock Clock
- {
- get => GetValue(ClockProperty);
- set => SetValue(ClockProperty, value);
- }
-
///
/// Defines the property.
///
@@ -31,9 +28,18 @@ namespace Avalonia.Animation
o => o.Transitions,
(o, v) => o.Transitions = v);
- private Transitions _transitions;
+ private bool _transitionsEnabled = true;
+ private Transitions? _transitions;
+ private Dictionary? _transitionState;
- private Dictionary _previousTransitions;
+ ///
+ /// Gets or sets the clock which controls the animations on the control.
+ ///
+ public IClock Clock
+ {
+ get => GetValue(ClockProperty);
+ set => SetValue(ClockProperty, value);
+ }
///
/// Gets or sets the property transitions for the control.
@@ -43,48 +49,195 @@ namespace Avalonia.Animation
get
{
if (_transitions is null)
+ {
_transitions = new Transitions();
-
- if (_previousTransitions is null)
- _previousTransitions = new Dictionary();
+ _transitions.CollectionChanged += TransitionsCollectionChanged;
+ }
return _transitions;
}
set
{
+ // TODO: This is a hack, Setter should not replace transitions, but should add/remove.
if (value is null)
+ {
return;
+ }
- if (_previousTransitions is null)
- _previousTransitions = new Dictionary();
+ if (_transitions is object)
+ {
+ RemoveTransitions(_transitions);
+ _transitions.CollectionChanged -= TransitionsCollectionChanged;
+ }
SetAndRaise(TransitionsProperty, ref _transitions, value);
+ _transitions.CollectionChanged += TransitionsCollectionChanged;
+ AddTransitions(_transitions);
+ }
+ }
+
+ ///
+ /// Enables transitions for the control.
+ ///
+ ///
+ /// This method should not be called from user code, it will be called automatically by the framework
+ /// when a control is added to the visual tree.
+ ///
+ protected void EnableTransitions()
+ {
+ if (!_transitionsEnabled)
+ {
+ _transitionsEnabled = true;
+
+ if (_transitions is object)
+ {
+ AddTransitions(_transitions);
+ }
+ }
+ }
+
+ ///
+ /// Disables transitions for the control.
+ ///
+ ///
+ /// This method should not be called from user code, it will be called automatically by the framework
+ /// when a control is added to the visual tree.
+ ///
+ protected void DisableTransitions()
+ {
+ if (_transitionsEnabled)
+ {
+ _transitionsEnabled = false;
+
+ if (_transitions is object)
+ {
+ RemoveTransitions(_transitions);
+ }
+ }
+ }
+
+ protected sealed override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change)
+ {
+ if (_transitionsEnabled &&
+ _transitions is object &&
+ _transitionState is object &&
+ change.Priority > BindingPriority.Animation)
+ {
+ foreach (var transition in _transitions)
+ {
+ if (transition.Property == change.Property)
+ {
+ var state = _transitionState[transition];
+ var oldValue = state.BaseValue;
+ var newValue = GetAnimationBaseValue(transition.Property);
+
+ if (!Equals(oldValue, newValue))
+ {
+ state.BaseValue = newValue;
+
+ // We need to transition from the current animated value if present,
+ // instead of the old base value.
+ var animatedValue = GetValue(transition.Property);
+
+ if (!Equals(newValue, animatedValue))
+ {
+ oldValue = animatedValue;
+ }
+
+ state.Instance?.Dispose();
+ state.Instance = transition.Apply(
+ this,
+ Clock ?? AvaloniaLocator.Current.GetService(),
+ oldValue,
+ newValue);
+ return;
+ }
+ }
+ }
+ }
+
+ base.OnPropertyChangedCore(change);
+ }
+
+ private void TransitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (!_transitionsEnabled)
+ {
+ return;
+ }
+
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ AddTransitions(e.NewItems);
+ break;
+ case NotifyCollectionChangedAction.Remove:
+ RemoveTransitions(e.OldItems);
+ break;
+ case NotifyCollectionChangedAction.Replace:
+ RemoveTransitions(e.OldItems);
+ AddTransitions(e.NewItems);
+ break;
+ case NotifyCollectionChangedAction.Reset:
+ throw new NotSupportedException("Transitions collection cannot be reset.");
}
}
- protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ private void AddTransitions(IList items)
{
- if (_transitions is null || _previousTransitions is null || change.Priority == BindingPriority.Animation)
+ if (!_transitionsEnabled)
+ {
return;
+ }
- // PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations).
- foreach (var transition in _transitions)
+ _transitionState ??= new Dictionary();
+
+ for (var i = 0; i < items.Count; ++i)
{
- if (transition.Property == change.Property)
+ var t = (ITransition)items[i];
+
+ _transitionState.Add(t, new TransitionState
{
- if (_previousTransitions.TryGetValue(change.Property, out var dispose))
- dispose.Dispose();
+ BaseValue = GetAnimationBaseValue(t.Property),
+ });
+ }
+ }
- var instance = transition.Apply(
- this,
- Clock ?? Avalonia.Animation.Clock.GlobalClock,
- change.OldValue.GetValueOrDefault(),
- change.NewValue.GetValueOrDefault());
+ private void RemoveTransitions(IList items)
+ {
+ if (_transitionState is null)
+ {
+ return;
+ }
- _previousTransitions[change.Property] = instance;
- return;
+ for (var i = 0; i < items.Count; ++i)
+ {
+ var t = (ITransition)items[i];
+
+ if (_transitionState.TryGetValue(t, out var state))
+ {
+ state.Instance?.Dispose();
+ _transitionState.Remove(t);
}
}
}
+
+ private object GetAnimationBaseValue(AvaloniaProperty property)
+ {
+ var value = this.GetBaseValue(property, BindingPriority.LocalValue);
+
+ if (value == AvaloniaProperty.UnsetValue)
+ {
+ value = GetValue(property);
+ }
+
+ return value;
+ }
+
+ private class TransitionState
+ {
+ public IDisposable? Instance { get; set; }
+ public object? BaseValue { get; set; }
+ }
}
}
diff --git a/src/Avalonia.Animation/Transitions.cs b/src/Avalonia.Animation/Transitions.cs
index 2741039ebc..6687a2902d 100644
--- a/src/Avalonia.Animation/Transitions.cs
+++ b/src/Avalonia.Animation/Transitions.cs
@@ -1,4 +1,6 @@
+using System;
using Avalonia.Collections;
+using Avalonia.Threading;
namespace Avalonia.Animation
{
@@ -13,6 +15,17 @@ namespace Avalonia.Animation
public Transitions()
{
ResetBehavior = ResetBehavior.Remove;
+ Validate = ValidateTransition;
+ }
+
+ private void ValidateTransition(ITransition obj)
+ {
+ Dispatcher.UIThread.VerifyAccess();
+
+ if (obj.Property.IsDirect)
+ {
+ throw new InvalidOperationException("Cannot animate a direct property.");
+ }
}
}
}
diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs
index 36b72fa28e..bb9a4cf208 100644
--- a/src/Avalonia.Visuals/Visual.cs
+++ b/src/Avalonia.Visuals/Visual.cs
@@ -114,6 +114,9 @@ namespace Avalonia
///
public Visual()
{
+ // Disable transitions until we're added to the visual tree.
+ DisableTransitions();
+
var visualChildren = new AvaloniaList();
visualChildren.ResetBehavior = ResetBehavior.Remove;
visualChildren.Validate = visual => ValidateVisualChild(visual);
@@ -393,6 +396,7 @@ namespace Avalonia
RenderTransform.Changed += RenderTransformChanged;
}
+ EnableTransitions();
OnAttachedToVisualTree(e);
AttachedToVisualTree?.Invoke(this, e);
InvalidateVisual();
@@ -429,6 +433,7 @@ namespace Avalonia
RenderTransform.Changed -= RenderTransformChanged;
}
+ DisableTransitions();
OnDetachedFromVisualTree(e);
DetachedFromVisualTree?.Invoke(this, e);
e.Root?.Renderer?.AddDirty(this);
diff --git a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs
new file mode 100644
index 0000000000..e1169650a9
--- /dev/null
+++ b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs
@@ -0,0 +1,242 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Styling;
+using Avalonia.UnitTests;
+using Moq;
+using Xunit;
+
+namespace Avalonia.Animation.UnitTests
+{
+ public class AnimatableTests
+ {
+ [Fact]
+ public void Transition_Is_Not_Applied_When_Not_Attached_To_Visual_Tree()
+ {
+ var target = CreateTarget();
+ var control = new Control
+ {
+ Transitions = { target.Object },
+ };
+
+ control.Opacity = 0.5;
+
+ target.Verify(x => x.Apply(
+ control,
+ It.IsAny(),
+ 1.0,
+ 0.5),
+ Times.Never);
+ }
+
+ [Fact]
+ public void Transition_Is_Not_Applied_To_Initial_Style()
+ {
+ using (UnitTestApplication.Start(TestServices.RealStyler))
+ {
+ var target = CreateTarget();
+ var control = new Control
+ {
+ Transitions = { target.Object },
+ };
+
+ var root = new TestRoot
+ {
+ Styles =
+ {
+ new Style(x => x.OfType())
+ {
+ Setters =
+ {
+ new Setter(Visual.OpacityProperty, 0.8),
+ }
+ }
+ }
+ };
+
+ root.Child = control;
+
+ Assert.Equal(0.8, control.Opacity);
+
+ target.Verify(x => x.Apply(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny