From a2a135206524e0c53def447ac426c5f9328e6e96 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sun, 31 May 2020 19:36:02 +0800 Subject: [PATCH 1/5] Implement transition delay --- src/Avalonia.Animation/TransitionInstance.cs | 36 +++++++++++++++++--- src/Avalonia.Animation/Transition`1.cs | 9 +++-- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index ad2001d621..5184341324 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/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 { 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); } } diff --git a/src/Avalonia.Animation/Transition`1.cs b/src/Avalonia.Animation/Transition`1.cs index 138131acb9..4542a137e5 100644 --- a/src/Avalonia.Animation/Transition`1.cs +++ b/src/Avalonia.Animation/Transition`1.cs @@ -13,10 +13,15 @@ namespace Avalonia.Animation private AvaloniaProperty _prop; /// - /// Gets the duration of the animation. + /// Gets or sets the duration of the transition. /// public TimeSpan Duration { get; set; } + /// + /// Gets or sets delay before starting the transition. + /// + public TimeSpan Delay { get; set; } = TimeSpan.Zero; + /// /// Gets the easing class to be used. /// @@ -47,7 +52,7 @@ namespace Avalonia.Animation /// 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((AvaloniaProperty)Property, transition, Data.BindingPriority.Animation); } } From a7536aee6789bd97eb68fc61f392a53b71420b91 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sun, 31 May 2020 20:34:27 +0800 Subject: [PATCH 2/5] add unit test --- .../Avalonia.Animation.UnitTests/TestClock.cs | 10 ++- .../TransitionsTests.cs | 88 ++++++++++++------- 2 files changed, 65 insertions(+), 33 deletions(-) diff --git a/tests/Avalonia.Animation.UnitTests/TestClock.cs b/tests/Avalonia.Animation.UnitTests/TestClock.cs index a1c4ff9277..4812880c03 100644 --- a/tests/Avalonia.Animation.UnitTests/TestClock.cs +++ b/tests/Avalonia.Animation.UnitTests/TestClock.cs @@ -5,10 +5,12 @@ namespace Avalonia.Animation.UnitTests { internal class TestClock : IClock, IDisposable { + private TimeSpan _curTime; + private IObserver _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 observer) { _observer = observer; diff --git a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs index 70ffd781a1..640013dedd 100644 --- a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs +++ b/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)); } } From 7b1a9fbb84e14124a67d54748571344f6db74bc4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 4 Jun 2020 15:26:10 +0200 Subject: [PATCH 3/5] Fix caret not displaying when focused via Tab key. Also set caret position to end when selecting all, as textboxes when selected should scroll to display the end of the text. --- src/Avalonia.Controls/TextBox.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 42e16e6979..c2f496287c 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -347,7 +347,7 @@ namespace Avalonia.Controls if (IsFocused) { - DecideCaretVisibility(); + _presenter?.ShowCaret(); } } @@ -364,14 +364,7 @@ namespace Avalonia.Controls { SelectAll(); } - else - { - DecideCaretVisibility(); - } - } - private void DecideCaretVisibility() - { _presenter?.ShowCaret(); } @@ -975,6 +968,7 @@ namespace Avalonia.Controls { SelectionStart = 0; SelectionEnd = Text?.Length ?? 0; + CaretIndex = SelectionEnd; } private bool DeleteSelection() From c2ad34a4ce072ead90aa23323b76544a6fd91a9b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 4 Jun 2020 17:14:43 +0200 Subject: [PATCH 4/5] Fix moving caret left/right when selection present. When a selection is present, pressing the left key should move the caret to the start of the selection and pressing the right key should move the caret to the end of the selection. --- src/Avalonia.Controls/TextBox.cs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index c2f496287c..394699ce64 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -573,15 +573,15 @@ namespace Avalonia.Controls switch (e.Key) { case Key.Left: - MoveHorizontal(-1, hasWholeWordModifiers); - movement = true; selection = DetectSelection(); + MoveHorizontal(-1, hasWholeWordModifiers, selection); + movement = true; break; case Key.Right: - MoveHorizontal(1, hasWholeWordModifiers); - movement = true; selection = DetectSelection(); + MoveHorizontal(1, hasWholeWordModifiers, selection); + movement = true; break; case Key.Up: @@ -826,13 +826,21 @@ namespace Avalonia.Controls return result; } - private void MoveHorizontal(int direction, bool wholeWord) + private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting) { var text = Text ?? string.Empty; var caretIndex = CaretIndex; if (!wholeWord) { + if (SelectionStart != SelectionEnd && !isSelecting) + { + var start = Math.Min(SelectionStart, SelectionEnd); + var end = Math.Max(SelectionStart, SelectionEnd); + CaretIndex = direction < 0 ? start : end; + return; + } + var index = caretIndex + direction; if (index < 0 || index > text.Length) @@ -1049,14 +1057,14 @@ namespace Avalonia.Controls private void SetSelectionForControlBackspace() { SelectionStart = CaretIndex; - MoveHorizontal(-1, true); + MoveHorizontal(-1, true, false); SelectionEnd = CaretIndex; } private void SetSelectionForControlDelete() { SelectionStart = CaretIndex; - MoveHorizontal(1, true); + MoveHorizontal(1, true, false); SelectionEnd = CaretIndex; } From 5027de256cc2cd3e44347ff3ded1cc9d7812e8da Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Wed, 17 Jun 2020 21:33:50 +0200 Subject: [PATCH 5/5] Add an example for transition delay. --- samples/RenderDemo/Pages/TransitionsPage.xaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/samples/RenderDemo/Pages/TransitionsPage.xaml b/samples/RenderDemo/Pages/TransitionsPage.xaml index df7130a925..d6da293ff3 100644 --- a/samples/RenderDemo/Pages/TransitionsPage.xaml +++ b/samples/RenderDemo/Pages/TransitionsPage.xaml @@ -77,6 +77,19 @@ + + + + @@ -95,6 +108,7 @@ +