Browse Source

Merge pull request #3854 from AvaloniaUI/fixes/3109-transitions

Reworked transitions.
pull/3955/head
Jumar Macato 6 years ago
committed by GitHub
parent
commit
15caf2bcab
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 215
      src/Avalonia.Animation/Animatable.cs
  2. 13
      src/Avalonia.Animation/Transitions.cs
  3. 5
      src/Avalonia.Visuals/Visual.cs
  4. 242
      tests/Avalonia.Animation.UnitTests/AnimatableTests.cs

215
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
/// </summary>
public class Animatable : AvaloniaObject
{
/// <summary>
/// Defines the <see cref="Clock"/> property.
/// </summary>
public static readonly StyledProperty<IClock> ClockProperty =
AvaloniaProperty.Register<Animatable, IClock>(nameof(Clock), inherits: true);
public IClock Clock
{
get => GetValue(ClockProperty);
set => SetValue(ClockProperty, value);
}
/// <summary>
/// Defines the <see cref="Transitions"/> property.
/// </summary>
@ -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<ITransition, TransitionState>? _transitionState;
private Dictionary<AvaloniaProperty, IDisposable> _previousTransitions;
/// <summary>
/// Gets or sets the clock which controls the animations on the control.
/// </summary>
public IClock Clock
{
get => GetValue(ClockProperty);
set => SetValue(ClockProperty, value);
}
/// <summary>
/// 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<AvaloniaProperty, IDisposable>();
_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<AvaloniaProperty, IDisposable>();
if (_transitions is object)
{
RemoveTransitions(_transitions);
_transitions.CollectionChanged -= TransitionsCollectionChanged;
}
SetAndRaise(TransitionsProperty, ref _transitions, value);
_transitions.CollectionChanged += TransitionsCollectionChanged;
AddTransitions(_transitions);
}
}
/// <summary>
/// Enables transitions for the control.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
protected void EnableTransitions()
{
if (!_transitionsEnabled)
{
_transitionsEnabled = true;
if (_transitions is object)
{
AddTransitions(_transitions);
}
}
}
/// <summary>
/// Disables transitions for the control.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
protected void DisableTransitions()
{
if (_transitionsEnabled)
{
_transitionsEnabled = false;
if (_transitions is object)
{
RemoveTransitions(_transitions);
}
}
}
protected sealed override void OnPropertyChangedCore<T>(AvaloniaPropertyChangedEventArgs<T> 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<IGlobalClock>(),
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<T>(AvaloniaPropertyChangedEventArgs<T> 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<ITransition, TransitionState>();
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; }
}
}
}

13
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.");
}
}
}
}

5
src/Avalonia.Visuals/Visual.cs

@ -114,6 +114,9 @@ namespace Avalonia
/// </summary>
public Visual()
{
// Disable transitions until we're added to the visual tree.
DisableTransitions();
var visualChildren = new AvaloniaList<IVisual>();
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);

242
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<IClock>(),
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<Control>())
{
Setters =
{
new Setter(Visual.OpacityProperty, 0.8),
}
}
}
};
root.Child = control;
Assert.Equal(0.8, control.Opacity);
target.Verify(x => x.Apply(
It.IsAny<Control>(),
It.IsAny<IClock>(),
It.IsAny<object>(),
It.IsAny<object>()),
Times.Never);
}
}
[Fact]
public void Transition_Is_Applied_When_Local_Value_Changes()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
control.Opacity = 0.5;
target.Verify(x => x.Apply(
control,
It.IsAny<IClock>(),
1.0,
0.5));
}
[Fact]
public void Transition_Is_Not_Applied_When_Animated_Value_Changes()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
control.SetValue(Visual.OpacityProperty, 0.5, BindingPriority.Animation);
target.Verify(x => x.Apply(
control,
It.IsAny<IClock>(),
1.0,
0.5),
Times.Never);
}
[Fact]
public void Transition_Is_Not_Applied_When_StyleTrigger_Changes_With_LocalValue_Present()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
control.SetValue(Visual.OpacityProperty, 0.5);
target.Verify(x => x.Apply(
control,
It.IsAny<IClock>(),
1.0,
0.5));
target.ResetCalls();
control.SetValue(Visual.OpacityProperty, 0.8, BindingPriority.StyleTrigger);
target.Verify(x => x.Apply(
It.IsAny<Control>(),
It.IsAny<IClock>(),
It.IsAny<object>(),
It.IsAny<object>()),
Times.Never);
}
[Fact]
public void Transition_Is_Disposed_When_Local_Value_Changes()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
var sub = new Mock<IDisposable>();
target.Setup(x => x.Apply(control, It.IsAny<IClock>(), 1.0, 0.5)).Returns(sub.Object);
control.Opacity = 0.5;
sub.ResetCalls();
control.Opacity = 0.4;
sub.Verify(x => x.Dispose());
}
[Fact]
public void New_Transition_Is_Applied_When_Local_Value_Changes()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
target.Setup(x => x.Property).Returns(Visual.OpacityProperty);
target.Setup(x => x.Apply(control, It.IsAny<IClock>(), 1.0, 0.5))
.Callback(() =>
{
control.SetValue(Visual.OpacityProperty, 0.9, BindingPriority.Animation);
})
.Returns(Mock.Of<IDisposable>());
control.Opacity = 0.5;
Assert.Equal(0.9, control.Opacity);
target.ResetCalls();
control.Opacity = 0.4;
target.Verify(x => x.Apply(
control,
It.IsAny<IClock>(),
0.9,
0.4));
}
[Fact]
public void Transition_Is_Not_Applied_When_Removed_From_Visual_Tree()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
control.Opacity = 0.5;
target.Verify(x => x.Apply(
control,
It.IsAny<IClock>(),
1.0,
0.5));
target.ResetCalls();
var root = (TestRoot)control.Parent;
root.Child = null;
control.Opacity = 0.8;
target.Verify(x => x.Apply(
It.IsAny<Control>(),
It.IsAny<IClock>(),
It.IsAny<object>(),
It.IsAny<object>()),
Times.Never);
}
[Fact]
public void Animation_Is_Cancelled_When_Transition_Removed()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
var sub = new Mock<IDisposable>();
target.Setup(x => x.Apply(
It.IsAny<Animatable>(),
It.IsAny<IClock>(),
It.IsAny<object>(),
It.IsAny<object>())).Returns(sub.Object);
control.Opacity = 0.5;
control.Transitions.RemoveAt(0);
sub.Verify(x => x.Dispose());
}
private static Mock<ITransition> CreateTarget()
{
var target = new Mock<ITransition>();
var sub = new Mock<IDisposable>();
target.Setup(x => x.Property).Returns(Visual.OpacityProperty);
target.Setup(x => x.Apply(
It.IsAny<Animatable>(),
It.IsAny<IClock>(),
It.IsAny<object>(),
It.IsAny<object>())).Returns(sub.Object);
return target;
}
private static Control CreateControl(ITransition transition)
{
var control = new Control
{
Transitions = { transition },
};
var root = new TestRoot(control);
return control;
}
}
}
Loading…
Cancel
Save