Browse Source

Merge pull request #4052 from AvaloniaUI/add-transition-delay

Add transition delay
pull/4138/head
danwalmsley 6 years ago
committed by GitHub
parent
commit
c37f2ba4f0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      samples/RenderDemo/Pages/TransitionsPage.xaml
  2. 36
      src/Avalonia.Animation/TransitionInstance.cs
  3. 9
      src/Avalonia.Animation/Transition`1.cs
  4. 10
      tests/Avalonia.Animation.UnitTests/TestClock.cs
  5. 88
      tests/Avalonia.Animation.UnitTests/TransitionsTests.cs

14
samples/RenderDemo/Pages/TransitionsPage.xaml

@ -77,6 +77,19 @@
<Setter Property="RenderTransform" Value="skewX(-20deg)" />
</Style>
<Style Selector="Border.Rect6">
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.5" Delay="0:0:1"/>
</Transitions>
</Setter>
<Setter Property="RenderTransform" Value="scale(0.8)" />
</Style>
<Style Selector="Border.Rect6:pointerover">
<Setter Property="RenderTransform" Value="none" />
</Style>
</Styles>
</UserControl.Styles>
@ -95,6 +108,7 @@
<Border Classes="Test Rect3"/>
<Border Classes="Test Rect4" Background="Navy"/>
<Border Classes="Test Rect5" Background="SeaGreen"/>
<Border Classes="Test Rect6" Background="Orange"/>
</WrapPanel>
</StackPanel>
</Grid>

36
src/Avalonia.Animation/TransitionInstance.cs

@ -4,6 +4,7 @@ using System.Reactive.Linq;
using Avalonia.Animation.Easings;
using Avalonia.Animation.Utils;
using Avalonia.Reactive;
using Avalonia.Utilities;
namespace Avalonia.Animation
{
@ -13,31 +14,56 @@ namespace Avalonia.Animation
internal class TransitionInstance : SingleSubscriberObservableBase<double>
{
private IDisposable _timerSubscription;
private TimeSpan _delay;
private TimeSpan _duration;
private readonly IClock _baseClock;
private IClock _clock;
public TransitionInstance(IClock clock, TimeSpan Duration)
public TransitionInstance(IClock clock, TimeSpan delay, TimeSpan duration)
{
clock = clock ?? throw new ArgumentNullException(nameof(clock));
_duration = Duration;
_delay = delay;
_duration = duration;
_baseClock = clock;
}
private void TimerTick(TimeSpan t)
{
var interpVal = _duration.Ticks == 0 ? 1d : (double)t.Ticks / _duration.Ticks;
// [<------------- normalizedTotalDur ------------------>]
// [<---- Delay ---->][<---------- Duration ------------>]
// ^- normalizedDelayEnd
// [<---- normalizedInterpVal --->]
var normalizedInterpVal = 1d;
if (!MathUtilities.AreClose(_duration.TotalSeconds, 0d))
{
var normalizedTotalDur = _delay + _duration;
var normalizedDelayEnd = _delay.TotalSeconds / normalizedTotalDur.TotalSeconds;
var normalizedPresentationTime = t.TotalSeconds / normalizedTotalDur.TotalSeconds;
if (normalizedPresentationTime < normalizedDelayEnd
|| MathUtilities.AreClose(normalizedPresentationTime, normalizedDelayEnd))
{
normalizedInterpVal = 0d;
}
else
{
normalizedInterpVal = (t.TotalSeconds - _delay.TotalSeconds) / _duration.TotalSeconds;
}
}
// Clamp interpolation value.
if (interpVal >= 1d | interpVal < 0d)
if (normalizedInterpVal >= 1d || normalizedInterpVal < 0d)
{
PublishNext(1d);
PublishCompleted();
}
else
{
PublishNext(interpVal);
PublishNext(normalizedInterpVal);
}
}

9
src/Avalonia.Animation/Transition`1.cs

@ -13,10 +13,15 @@ namespace Avalonia.Animation
private AvaloniaProperty _prop;
/// <summary>
/// Gets the duration of the animation.
/// Gets or sets the duration of the transition.
/// </summary>
public TimeSpan Duration { get; set; }
/// <summary>
/// Gets or sets delay before starting the transition.
/// </summary>
public TimeSpan Delay { get; set; } = TimeSpan.Zero;
/// <summary>
/// Gets the easing class to be used.
/// </summary>
@ -47,7 +52,7 @@ namespace Avalonia.Animation
/// <inheritdocs/>
public virtual IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue)
{
var transition = DoTransition(new TransitionInstance(clock, Duration), (T)oldValue, (T)newValue);
var transition = DoTransition(new TransitionInstance(clock, Delay, Duration), (T)oldValue, (T)newValue);
return control.Bind<T>((AvaloniaProperty<T>)Property, transition, Data.BindingPriority.Animation);
}
}

10
tests/Avalonia.Animation.UnitTests/TestClock.cs

@ -5,10 +5,12 @@ namespace Avalonia.Animation.UnitTests
{
internal class TestClock : IClock, IDisposable
{
private TimeSpan _curTime;
private IObserver<TimeSpan> _observer;
public PlayState PlayState { get; set; } = PlayState.Run;
public void Dispose()
{
_observer?.OnCompleted();
@ -19,6 +21,12 @@ namespace Avalonia.Animation.UnitTests
_observer?.OnNext(time);
}
public void Pulse(TimeSpan time)
{
_curTime += time;
_observer?.OnNext(_curTime);
}
public IDisposable Subscribe(IObserver<TimeSpan> observer)
{
_observer = observer;

88
tests/Avalonia.Animation.UnitTests/TransitionsTests.cs

@ -10,13 +10,11 @@ namespace Avalonia.Animation.UnitTests
[Fact]
public void Check_Transitions_Interpolation_Negative_Bounds_Clamp()
{
var clock = new MockGlobalClock();
var clock = new TestClock();
using (UnitTestApplication.Start(new TestServices(globalClock: clock)))
var border = new Border
{
var border = new Border
{
Transitions = new Transitions
Transitions = new Transitions
{
new DoubleTransition
{
@ -24,27 +22,25 @@ namespace Avalonia.Animation.UnitTests
Property = Border.OpacityProperty,
}
}
};
};
border.Opacity = 0;
border.Opacity = 0;
clock.Pulse(TimeSpan.FromSeconds(0));
clock.Pulse(TimeSpan.FromSeconds(-0.5));
clock.Pulse(TimeSpan.FromSeconds(0));
clock.Pulse(TimeSpan.FromSeconds(-0.5));
Assert.Equal(0, border.Opacity);
Assert.Equal(0, border.Opacity);
}
}
[Fact]
public void Check_Transitions_Interpolation_Positive_Bounds_Clamp()
{
var clock = new MockGlobalClock();
var clock = new TestClock();
using (UnitTestApplication.Start(new TestServices(globalClock: clock)))
var border = new Border
{
var border = new Border
{
Transitions = new Transitions
Transitions = new Transitions
{
new DoubleTransition
{
@ -52,34 +48,62 @@ namespace Avalonia.Animation.UnitTests
Property = Border.OpacityProperty,
}
}
};
};
border.Opacity = 0;
border.Opacity = 0;
clock.Pulse(TimeSpan.FromSeconds(0));
clock.Pulse(TimeSpan.FromMilliseconds(1001));
clock.Pulse(TimeSpan.FromSeconds(0));
clock.Pulse(TimeSpan.FromMilliseconds(1001));
Assert.Equal(0, border.Opacity);
Assert.Equal(0, border.Opacity);
}
}
[Fact]
public void TransitionInstance_With_Zero_Duration_Is_Completed_On_First_Tick()
{
var clock = new MockGlobalClock();
var clock = new TestClock();
using (UnitTestApplication.Start(new TestServices(globalClock: clock)))
int i = 0;
var inst = new TransitionInstance(clock, TimeSpan.Zero, TimeSpan.Zero).Subscribe(nextValue =>
{
int i = 0;
var inst = new TransitionInstance(clock, TimeSpan.Zero).Subscribe(nextValue =>
switch (i++)
{
switch (i++)
{
case 0: Assert.Equal(0, nextValue); break;
case 1: Assert.Equal(1d, nextValue); break;
}
});
case 0: Assert.Equal(0, nextValue); break;
case 1: Assert.Equal(1d, nextValue); break;
}
});
clock.Pulse(TimeSpan.FromMilliseconds(10));
}
[Fact]
public void TransitionInstance_Properly_Calculates_Delay_And_Duration_Values()
{
var clock = new TestClock();
int i = -1;
var inst = new TransitionInstance(clock, TimeSpan.FromMilliseconds(30), TimeSpan.FromMilliseconds(70)).Subscribe(nextValue =>
{
switch (i++)
{
case 0: Assert.Equal(0, nextValue); break;
case 1: Assert.Equal(0, nextValue); break;
case 2: Assert.Equal(0, nextValue); break;
case 3: Assert.Equal(0, nextValue); break;
case 4: Assert.Equal(Math.Round(10d / 70d, 4), Math.Round(nextValue, 4)); break;
case 5: Assert.Equal(Math.Round(20d / 70d, 4), Math.Round(nextValue, 4)); break;
case 6: Assert.Equal(Math.Round(30d / 70d, 4), Math.Round(nextValue, 4)); break;
case 7: Assert.Equal(Math.Round(40d / 70d, 4), Math.Round(nextValue, 4)); break;
case 8: Assert.Equal(Math.Round(50d / 70d, 4), Math.Round(nextValue, 4)); break;
case 9: Assert.Equal(Math.Round(60d / 70d, 4), Math.Round(nextValue, 4)); break;
case 10: Assert.Equal(1d, nextValue); break;
}
});
for (int z = 0; z <= 10; z++)
{
clock.Pulse(TimeSpan.FromMilliseconds(10));
}
}

Loading…
Cancel
Save