From 0b4e6b847170d34184288bb031d294b62a471a1b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 28 Jun 2018 10:04:49 +0200 Subject: [PATCH 01/44] Make centralized RenderLoop. - Renamed `RenderLoop` to `RenderTimer` - Added new `RenderLoop` which `DeferredRenderer`s register themselves with for updates --- .../InternalPlatformThreadingInterface.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 2 +- .../Remote/PreviewerWindowingPlatform.cs | 3 +- src/Avalonia.Visuals/Avalonia.Visuals.csproj | 1 + ...ultRenderLoop.cs => DefaultRenderTimer.cs} | 9 +- .../Rendering/DeferredRenderer.cs | 44 +++---- src/Avalonia.Visuals/Rendering/IRenderLoop.cs | 17 +-- .../Rendering/IRenderLoopTask.cs | 12 ++ .../Rendering/IRenderTimer.cs | 20 ++++ src/Avalonia.Visuals/Rendering/RenderLoop.cs | 111 ++++++++++++++++++ src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs | 3 +- .../LinuxFramebufferPlatform.cs | 3 +- src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs | 5 +- .../{RenderLoop.cs => RenderTimer.cs} | 4 +- .../{RenderLoop.cs => RenderTimer.cs} | 4 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 3 +- tests/Avalonia.UnitTests/TestServices.cs | 4 +- .../Rendering/DeferredRendererTests.cs | 6 +- 18 files changed, 191 insertions(+), 62 deletions(-) rename src/Avalonia.Visuals/Rendering/{DefaultRenderLoop.cs => DefaultRenderTimer.cs} (92%) create mode 100644 src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs create mode 100644 src/Avalonia.Visuals/Rendering/IRenderTimer.cs create mode 100644 src/Avalonia.Visuals/Rendering/RenderLoop.cs rename src/OSX/Avalonia.MonoMac/{RenderLoop.cs => RenderTimer.cs} (91%) rename src/Windows/Avalonia.Win32/{RenderLoop.cs => RenderTimer.cs} (88%) diff --git a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs index 400bf5ccc3..47e600b9c8 100644 --- a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs +++ b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs @@ -9,7 +9,7 @@ using Avalonia.Threading; namespace Avalonia.Controls.Platform { - public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderLoop + public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderTimer { public InternalPlatformThreadingInterface() { diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 1161ded25f..fb5b932fd8 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -96,7 +96,7 @@ namespace Avalonia.Controls _applicationLifecycle = TryGetService(dependencyResolver); _renderInterface = TryGetService(dependencyResolver); - var renderLoop = TryGetService(dependencyResolver); + var renderLoop = TryGetService(dependencyResolver); Renderer = impl.CreateRenderer(this); impl.SetInputRoot(this); diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index 01998052d9..20acc30118 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -53,7 +53,8 @@ namespace Avalonia.DesignerSupport.Remote .Bind().ToConstant(Keyboard) .Bind().ToConstant(instance) .Bind().ToConstant(threading) - .Bind().ToConstant(threading) + .Bind().ToConstant(new RenderLoop()) + .Bind().ToConstant(threading) .Bind().ToSingleton() .Bind().ToConstant(instance) .Bind().ToSingleton(); diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index c34752a3ef..c88001cc0a 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -1,6 +1,7 @@  netstandard2.0 + Avalonia diff --git a/src/Avalonia.Visuals/Rendering/DefaultRenderLoop.cs b/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs similarity index 92% rename from src/Avalonia.Visuals/Rendering/DefaultRenderLoop.cs rename to src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs index 9cf849f59b..7ad8915bc4 100644 --- a/src/Avalonia.Visuals/Rendering/DefaultRenderLoop.cs +++ b/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs @@ -2,18 +2,19 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Threading.Tasks; using Avalonia.Platform; namespace Avalonia.Rendering { /// - /// Defines a default render loop that uses a standard timer. + /// Defines a default render timer that uses a standard timer. /// /// /// This class may be overridden by platform implementations to use a specialized timer /// implementation. /// - public class DefaultRenderLoop : IRenderLoop + public class DefaultRenderTimer : IRenderTimer { private IRuntimePlatform _runtime; private int _subscriberCount; @@ -21,12 +22,12 @@ namespace Avalonia.Rendering private IDisposable _subscription; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// The number of frames per second at which the loop should run. /// - public DefaultRenderLoop(int framesPerSecond) + public DefaultRenderTimer(int framesPerSecond) { FramesPerSecond = framesPerSecond; } diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index dc1d2933d0..f937964994 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -13,6 +13,7 @@ using Avalonia.Rendering.SceneGraph; using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.VisualTree; +using System.Threading.Tasks; namespace Avalonia.Rendering { @@ -20,7 +21,7 @@ namespace Avalonia.Rendering /// A renderer which renders the state of the visual tree to an intermediate scene graph /// representation which is then rendered on a rendering thread. /// - public class DeferredRenderer : RendererBase, IRenderer, IVisualBrushRenderer + public class DeferredRenderer : RendererBase, IRenderer, IRenderLoopTask, IVisualBrushRenderer { private readonly IDispatcher _dispatcher; private readonly IRenderLoop _renderLoop; @@ -149,7 +150,7 @@ namespace Avalonia.Rendering { if (!_running && _renderLoop != null) { - _renderLoop.Tick += OnRenderLoopTick; + _renderLoop.Add(this); _running = true; } } @@ -159,11 +160,23 @@ namespace Avalonia.Rendering { if (_running && _renderLoop != null) { - _renderLoop.Tick -= OnRenderLoopTick; + _renderLoop.Remove(this); _running = false; } } + bool IRenderLoopTask.NeedsUpdate => _dirty == null || _dirty.Count > 0; + + void IRenderLoopTask.Update() => UpdateScene(); + + void IRenderLoopTask.Render() + { + using (var scene = _scene?.Clone()) + { + Render(scene?.Item); + } + } + /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { @@ -420,31 +433,6 @@ namespace Avalonia.Rendering } } - private void OnRenderLoopTick(object sender, EventArgs e) - { - if (Monitor.TryEnter(_rendering)) - { - try - { - if (!_updateQueued && (_dirty == null || _dirty.Count > 0)) - { - _updateQueued = true; - _dispatcher.Post(UpdateScene, DispatcherPriority.Render); - } - - using (var scene = _scene?.Clone()) - { - Render(scene?.Item); - } - } - catch { } - finally - { - Monitor.Exit(_rendering); - } - } - } - private IRef GetOverlay( IDrawingContextImpl parentContext, Size size, diff --git a/src/Avalonia.Visuals/Rendering/IRenderLoop.cs b/src/Avalonia.Visuals/Rendering/IRenderLoop.cs index 36d915ddbd..bd1086d178 100644 --- a/src/Avalonia.Visuals/Rendering/IRenderLoop.cs +++ b/src/Avalonia.Visuals/Rendering/IRenderLoop.cs @@ -1,19 +1,8 @@ -using System; - -namespace Avalonia.Rendering +namespace Avalonia.Rendering { - /// - /// Defines the interface implemented by an application render loop. - /// public interface IRenderLoop { - /// - /// Raised when the render loop ticks to signal a new frame should be drawn. - /// - /// - /// This event can be raised on any thread; it is the responsibility of the subscriber to - /// switch execution to the right thread. - /// - event EventHandler Tick; + void Add(IRenderLoopTask i); + void Remove(IRenderLoopTask i); } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs b/src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs new file mode 100644 index 0000000000..2f251a5c17 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading.Tasks; + +namespace Avalonia.Rendering +{ + public interface IRenderLoopTask + { + bool NeedsUpdate { get; } + void Update(); + void Render(); + } +} diff --git a/src/Avalonia.Visuals/Rendering/IRenderTimer.cs b/src/Avalonia.Visuals/Rendering/IRenderTimer.cs new file mode 100644 index 0000000000..2665d6dd0b --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/IRenderTimer.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; + +namespace Avalonia.Rendering +{ + /// + /// Defines the interface implemented by an application render timer. + /// + public interface IRenderTimer + { + /// + /// Raised when the render timer ticks to signal a new frame should be drawn. + /// + /// + /// This event can be raised on any thread; it is the responsibility of the subscriber to + /// switch execution to the right thread. + /// + event EventHandler Tick; + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Rendering/RenderLoop.cs b/src/Avalonia.Visuals/Rendering/RenderLoop.cs new file mode 100644 index 0000000000..25febf8187 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/RenderLoop.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using Avalonia.Logging; +using Avalonia.Threading; + +namespace Avalonia.Rendering +{ + public class RenderLoop : IRenderLoop + { + private readonly IDispatcher _dispatcher; + private List _items = new List(); + private IRenderTimer _timer; + private volatile bool inTick; + + public RenderLoop() + { + _dispatcher = Dispatcher.UIThread; + } + + public RenderLoop(IRenderTimer timer, IDispatcher dispatcher) + { + _timer = timer; + _dispatcher = dispatcher; + } + + protected IRenderTimer Timer + { + get + { + if (_timer == null) + { + _timer = AvaloniaLocator.Current.GetService(); + } + + return _timer; + } + } + + public void Add(IRenderLoopTask i) + { + Contract.Requires(i != null); + Dispatcher.UIThread.VerifyAccess(); + + _items.Add(i); + + if (_items.Count == 1) + { + Timer.Tick += TimerTick; + } + } + + public void Remove(IRenderLoopTask i) + { + Contract.Requires(i != null); + Dispatcher.UIThread.VerifyAccess(); + + _items.Remove(i); + + if (_items.Count == 0) + { + Timer.Tick -= TimerTick; + } + } + + private async void TimerTick(object sender, EventArgs e) + { + if (!inTick) + { + inTick = true; + + try + { + var needsUpdate = false; + + foreach (var i in _items) + { + if (i.NeedsUpdate) + { + needsUpdate = true; + break; + } + } + + if (needsUpdate) + { + await _dispatcher.InvokeAsync(() => + { + foreach (var i in _items) + { + i.Update(); + } + }); + } + + foreach (var i in _items) + { + i.Render(); + } + } + catch (Exception ex) + { + Logger.Error(LogArea.Visual, this, "Exception in render loop: {Error}", ex); + } + finally + { + inTick = false; + } + } + } + } +} diff --git a/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs b/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs index ca8a1ad3a4..bbb6a01c05 100644 --- a/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs +++ b/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs @@ -52,7 +52,8 @@ namespace Avalonia.Gtk3 .Bind().ToConstant(Instance) .Bind().ToConstant(Instance) .Bind().ToSingleton() - .Bind().ToConstant(new DefaultRenderLoop(60)) + .Bind().ToConstant(new RenderLoop()) + .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new PlatformIconLoader()); } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 810be77b2b..9046c26cee 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -35,7 +35,8 @@ namespace Avalonia.LinuxFramebuffer .Bind().ToConstant(KeyboardDevice) .Bind().ToSingleton() .Bind().ToConstant(Threading) - .Bind().ToConstant(Threading); + .Bind().ToConstant(new RenderLoop()) + .Bind().ToConstant(Threading); } internal static TopLevel Initialize(T builder, string fbdev = null) where T : AppBuilderBase, new() diff --git a/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs b/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs index ba45ad8403..5dbf18b1de 100644 --- a/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs +++ b/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs @@ -21,6 +21,7 @@ namespace Avalonia.MonoMac private static bool s_monoMacInitialized; private static bool s_showInDock = true; private static IRenderLoop s_renderLoop; + private static IRenderTimer s_renderTimer; void DoInitialize() { @@ -35,6 +36,7 @@ namespace Avalonia.MonoMac .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToConstant(s_renderLoop) + .Bind().ToConstant(s_renderTimer) .Bind().ToConstant(PlatformThreadingInterface.Instance) /*.Bind().ToTransient()*/; } @@ -83,7 +85,8 @@ namespace Avalonia.MonoMac ThreadHelper.InitializeCocoaThreadingLocks(); App = NSApplication.SharedApplication; UpdateActivationPolicy(); - s_renderLoop = new RenderLoop(); //TODO: use CVDisplayLink + s_renderLoop = new RenderLoop(); + s_renderTimer = new RenderTimer(); //TODO: use CVDisplayLink s_monoMacInitialized = true; } diff --git a/src/OSX/Avalonia.MonoMac/RenderLoop.cs b/src/OSX/Avalonia.MonoMac/RenderTimer.cs similarity index 91% rename from src/OSX/Avalonia.MonoMac/RenderLoop.cs rename to src/OSX/Avalonia.MonoMac/RenderTimer.cs index 4d1f9b4201..d2fd50484a 100644 --- a/src/OSX/Avalonia.MonoMac/RenderLoop.cs +++ b/src/OSX/Avalonia.MonoMac/RenderTimer.cs @@ -6,12 +6,12 @@ using MonoMac.Foundation; namespace Avalonia.MonoMac { //TODO: Switch to using CVDisplayLink - public class RenderLoop : IRenderLoop + public class RenderTimer : IRenderTimer { private readonly object _lock = new object(); private readonly IDisposable _timer; - public RenderLoop() + public RenderTimer() { _timer = AvaloniaLocator.Current.GetService().StartSystemTimer(new TimeSpan(0, 0, 0, 0, 1000 / 60), () => diff --git a/src/Windows/Avalonia.Win32/RenderLoop.cs b/src/Windows/Avalonia.Win32/RenderTimer.cs similarity index 88% rename from src/Windows/Avalonia.Win32/RenderLoop.cs rename to src/Windows/Avalonia.Win32/RenderTimer.cs index 7d7befcc33..321a745fae 100644 --- a/src/Windows/Avalonia.Win32/RenderLoop.cs +++ b/src/Windows/Avalonia.Win32/RenderTimer.cs @@ -5,11 +5,11 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - internal class RenderLoop : DefaultRenderLoop + internal class RenderTimer : DefaultRenderTimer { private UnmanagedMethods.TimeCallback timerDelegate; - public RenderLoop(int framesPerSecond) + public RenderTimer(int framesPerSecond) : base(framesPerSecond) { } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 9afb1218af..d5eb97292a 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -82,7 +82,8 @@ namespace Avalonia.Win32 .Bind().ToConstant(WindowsKeyboardDevice.Instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance) - .Bind().ToConstant(new RenderLoop(60)) + .Bind().ToConstant(new RenderLoop()) + .Bind().ToConstant(new RenderTimer(60)) .Bind().ToSingleton() .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance); diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index d990defe3d..d68f1d167a 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -64,7 +64,7 @@ namespace Avalonia.UnitTests Func mouseDevice = null, IRuntimePlatform platform = null, IPlatformRenderInterface renderInterface = null, - IRenderLoop renderLoop = null, + IRenderTimer renderLoop = null, IScheduler scheduler = null, IStandardCursorFactory standardCursorFactory = null, IStyler styler = null, @@ -115,7 +115,7 @@ namespace Avalonia.UnitTests Func mouseDevice = null, IRuntimePlatform platform = null, IPlatformRenderInterface renderInterface = null, - IRenderLoop renderLoop = null, + IRenderTimer renderLoop = null, IScheduler scheduler = null, IStandardCursorFactory standardCursorFactory = null, IStyler styler = null, diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index 2350a31d5c..1af9a9499d 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -42,7 +42,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering [Fact] public void First_Frame_Calls_SceneBuilder_UpdateAll() { - var loop = new Mock(); + var loop = new Mock(); var root = new TestRoot(); var sceneBuilder = MockSceneBuilder(root); @@ -198,7 +198,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering [Fact] public void Should_Create_Layer_For_Root() { - var loop = new Mock(); + var loop = new Mock(); var root = new TestRoot(); var rootLayer = new Mock(); @@ -374,7 +374,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering private void RunFrame(Mock loop) { - loop.Raise(x => x.Tick += null, EventArgs.Empty); + //loop.Raise(x => x.Tick += null, EventArgs.Empty); } private IRenderTargetBitmapImpl CreateLayer() From 8ec2c8f661839f7826e2e262431c0b7debe4ad0e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 28 Jun 2018 10:36:35 +0200 Subject: [PATCH 02/44] Notify tick count in IRenderTimer.Tick. --- .../Platform/InternalPlatformThreadingInterface.cs | 4 ++-- src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs | 12 ++++++------ src/Avalonia.Visuals/Rendering/IRenderTimer.cs | 2 +- src/Avalonia.Visuals/Rendering/RenderLoop.cs | 2 +- src/OSX/Avalonia.MonoMac/RenderTimer.cs | 4 ++-- .../Avalonia.Win32/Interop/UnmanagedMethods.cs | 3 +++ src/Windows/Avalonia.Win32/RenderTimer.cs | 8 ++++++-- 7 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs index 47e600b9c8..501e15653a 100644 --- a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs +++ b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls.Platform public InternalPlatformThreadingInterface() { TlsCurrentThreadIsLoopThread = true; - StartTimer(DispatcherPriority.Render, new TimeSpan(0, 0, 0, 0, 66), () => Tick?.Invoke(this, new EventArgs())); + StartTimer(DispatcherPriority.Render, new TimeSpan(0, 0, 0, 0, 66), () => Tick?.Invoke(Environment.TickCount)); } private readonly AutoResetEvent _signaled = new AutoResetEvent(false); @@ -105,7 +105,7 @@ namespace Avalonia.Controls.Platform public bool CurrentThreadIsLoopThread => TlsCurrentThreadIsLoopThread; public event Action Signaled; - public event EventHandler Tick; + public event Action Tick; } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs b/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs index 7ad8915bc4..6b16ff8038 100644 --- a/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs +++ b/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs @@ -18,7 +18,7 @@ namespace Avalonia.Rendering { private IRuntimePlatform _runtime; private int _subscriberCount; - private EventHandler _tick; + private Action _tick; private IDisposable _subscription; /// @@ -38,7 +38,7 @@ namespace Avalonia.Rendering public int FramesPerSecond { get; } /// - public event EventHandler Tick + public event Action Tick { add { @@ -77,14 +77,14 @@ namespace Avalonia.Rendering /// This can be overridden by platform implementations to use a specialized timer /// implementation. /// - protected virtual IDisposable StartCore(Action tick) + protected virtual IDisposable StartCore(Action tick) { if (_runtime == null) { _runtime = AvaloniaLocator.Current.GetService(); } - return _runtime.StartSystemTimer(TimeSpan.FromSeconds(1.0 / FramesPerSecond), tick); + return _runtime.StartSystemTimer(TimeSpan.FromSeconds(1.0 / FramesPerSecond), () => tick(Environment.TickCount)); } /// @@ -96,9 +96,9 @@ namespace Avalonia.Rendering _subscription = null; } - private void InternalTick() + private void InternalTick(long tickCount) { - _tick(this, EventArgs.Empty); + _tick(tickCount); } } } diff --git a/src/Avalonia.Visuals/Rendering/IRenderTimer.cs b/src/Avalonia.Visuals/Rendering/IRenderTimer.cs index 2665d6dd0b..78f6183994 100644 --- a/src/Avalonia.Visuals/Rendering/IRenderTimer.cs +++ b/src/Avalonia.Visuals/Rendering/IRenderTimer.cs @@ -15,6 +15,6 @@ namespace Avalonia.Rendering /// This event can be raised on any thread; it is the responsibility of the subscriber to /// switch execution to the right thread. /// - event EventHandler Tick; + event Action Tick; } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Rendering/RenderLoop.cs b/src/Avalonia.Visuals/Rendering/RenderLoop.cs index 25febf8187..bdcfdcba8e 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLoop.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLoop.cs @@ -62,7 +62,7 @@ namespace Avalonia.Rendering } } - private async void TimerTick(object sender, EventArgs e) + private async void TimerTick(long tickCount) { if (!inTick) { diff --git a/src/OSX/Avalonia.MonoMac/RenderTimer.cs b/src/OSX/Avalonia.MonoMac/RenderTimer.cs index d2fd50484a..71f657b690 100644 --- a/src/OSX/Avalonia.MonoMac/RenderTimer.cs +++ b/src/OSX/Avalonia.MonoMac/RenderTimer.cs @@ -20,12 +20,12 @@ namespace Avalonia.MonoMac { using (new NSAutoreleasePool()) { - Tick?.Invoke(this, EventArgs.Empty); + Tick?.Invoke(Environment.TickCount); } } }); } - public event EventHandler Tick; + public event Action Tick; } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index f89086ccb7..1c70b3f03d 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -972,6 +972,9 @@ namespace Avalonia.Win32.Interop [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetMonitorInfo([In] IntPtr hMonitor, [Out] MONITORINFO lpmi); + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool QueryPerformanceCounter(out long lpPerformanceCount); + [return: MarshalAs(UnmanagedType.Bool)] [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "PostMessageW")] public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); diff --git a/src/Windows/Avalonia.Win32/RenderTimer.cs b/src/Windows/Avalonia.Win32/RenderTimer.cs index 321a745fae..cea1bf94a5 100644 --- a/src/Windows/Avalonia.Win32/RenderTimer.cs +++ b/src/Windows/Avalonia.Win32/RenderTimer.cs @@ -14,9 +14,13 @@ namespace Avalonia.Win32 { } - protected override IDisposable StartCore(Action tick) + protected override IDisposable StartCore(Action tick) { - timerDelegate = (id, uMsg, user, dw1, dw2) => tick(); + timerDelegate = (id, uMsg, user, dw1, dw2) => + { + UnmanagedMethods.QueryPerformanceCounter(out long tickCount); + tick(tickCount); + }; var handle = UnmanagedMethods.timeSetEvent( (uint)(1000 / FramesPerSecond), From 166f9f8cf01d27dd8a9afa040497c357f0e5eb6b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 28 Jun 2018 11:02:50 +0200 Subject: [PATCH 03/44] Pulse animation timer from render loop. --- src/Avalonia.Animation/Timing.cs | 35 +++++++++----------- src/Avalonia.Visuals/Rendering/RenderLoop.cs | 4 ++- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Animation/Timing.cs b/src/Avalonia.Animation/Timing.cs index 6367425911..d6a353fb34 100644 --- a/src/Avalonia.Animation/Timing.cs +++ b/src/Avalonia.Animation/Timing.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Threading; namespace Avalonia.Animation @@ -13,42 +14,38 @@ namespace Avalonia.Animation /// public static class Timing { - /// - /// The number of frames per second. - /// - public const int FramesPerSecond = 60; - - /// - /// The time span of each frame. - /// - internal static readonly TimeSpan FrameTick = TimeSpan.FromSeconds(1.0 / FramesPerSecond); + static TimerObservable _timer = new TimerObservable(); /// /// Initializes static members of the class. /// static Timing() { - var globalTimer = Observable.Interval(FrameTick, AvaloniaScheduler.Instance); - - AnimationsTimer = globalTimer - .Select(_ => GetTickCount()) + AnimationsTimer = _timer .Publish() .RefCount(); } + public static bool HasSubscriptions => _timer.HasSubscriptions; + internal static TimeSpan GetTickCount() => TimeSpan.FromMilliseconds(Environment.TickCount); /// /// Gets the animation timer. /// - /// - /// The animation timer triggers usually at 60 times per second or as - /// defined in . - /// The parameter passed to a subsciber is the current playstate of the animation. - /// internal static IObservable AnimationsTimer { get; } + + public static void Pulse(long tickCount) => _timer.Pulse(tickCount); + + private class TimerObservable : LightweightObservableBase + { + public bool HasSubscriptions { get; private set; } + public void Pulse(long tickCount) => PublishNext(TimeSpan.FromMilliseconds(tickCount)); + protected override void Initialize() => HasSubscriptions = true; + protected override void Deinitialize() => HasSubscriptions = false; + } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Rendering/RenderLoop.cs b/src/Avalonia.Visuals/Rendering/RenderLoop.cs index bdcfdcba8e..5cac6993ff 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLoop.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLoop.cs @@ -70,7 +70,7 @@ namespace Avalonia.Rendering try { - var needsUpdate = false; + var needsUpdate = Animation.Timing.HasSubscriptions; foreach (var i in _items) { @@ -85,6 +85,8 @@ namespace Avalonia.Rendering { await _dispatcher.InvokeAsync(() => { + Animation.Timing.Pulse(tickCount); + foreach (var i in _items) { i.Update(); From c7c9b0a2052488c8642fe155c80a7e4686c0dbe1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 29 Jun 2018 23:14:00 +0200 Subject: [PATCH 04/44] Use `Stopwatch.GetTimestamp()` for tickCount. --- src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs b/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs index 6b16ff8038..b05ecd5456 100644 --- a/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs +++ b/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Diagnostics; using System.Threading.Tasks; using Avalonia.Platform; @@ -84,7 +85,7 @@ namespace Avalonia.Rendering _runtime = AvaloniaLocator.Current.GetService(); } - return _runtime.StartSystemTimer(TimeSpan.FromSeconds(1.0 / FramesPerSecond), () => tick(Environment.TickCount)); + return _runtime.StartSystemTimer(TimeSpan.FromSeconds(1.0 / FramesPerSecond), () => tick(Stopwatch.GetTimestamp())); } /// From 33aec77f16313cfc6beb4f28701de4d3adc1411f Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sat, 30 Jun 2018 18:31:35 +0800 Subject: [PATCH 05/44] Move the playstate handling to the State machine instead of being pipelined by the timer itself. Removed unused AnimationStateTimer. --- src/Avalonia.Animation/Animatable.cs | 2 +- src/Avalonia.Animation/Timing.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 8a1a17a6fc..e51103aa55 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -74,4 +74,4 @@ namespace Avalonia.Animation } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/Timing.cs b/src/Avalonia.Animation/Timing.cs index d6a353fb34..8ee78018a9 100644 --- a/src/Avalonia.Animation/Timing.cs +++ b/src/Avalonia.Animation/Timing.cs @@ -12,7 +12,7 @@ namespace Avalonia.Animation /// /// Provides global timing functions for animations. /// - public static class Timing + public class Timing { static TimerObservable _timer = new TimerObservable(); From f2ba884e0ad3d210660bb60f8e387b652aa5d33b Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 27 Jul 2018 22:45:25 -0500 Subject: [PATCH 06/44] Cleanup logic in LightweightObservableBase. --- .../Reactive/LightweightObservableBase.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs index a2786d63da..41009e4cd3 100644 --- a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs +++ b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs @@ -82,18 +82,10 @@ namespace Avalonia.Reactive if (observers.Count == 0) { observers.TrimExcess(); + Deinitialize(); } - else - { - return; - } - } else - { - return; } } - - Deinitialize(); } } From 87e98cacf994eab709c368f283d8f58293926cfa Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 27 Jul 2018 23:57:44 -0500 Subject: [PATCH 07/44] Rewrite Win32 RenderTimer to use undeprecated APIs. --- .../Interop/UnmanagedMethods.cs | 28 +++++++++---- src/Windows/Avalonia.Win32/RenderTimer.cs | 41 +++++++++++++------ 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 1c70b3f03d..d6b95bc7b0 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -26,6 +26,9 @@ namespace Avalonia.Win32.Interop public delegate void TimeCallback(uint uTimerID, uint uMsg, UIntPtr dwUser, UIntPtr dw1, UIntPtr dw2); + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void WaitOrTimerCallback(IntPtr lpParameter, bool timerOrWaitFired); + public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); public enum Cursor @@ -848,11 +851,25 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, ShowWindowCommand nCmdShow); - [DllImport("Winmm.dll")] - public static extern uint timeKillEvent(uint uTimerID); + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr CreateTimerQueue(); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool DeleteTimerQueueEx(IntPtr TimerQueue, IntPtr CompletionEvent); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CreateTimerQueueTimer( + out IntPtr phNewTimer, + IntPtr TimerQueue, + WaitOrTimerCallback Callback, + IntPtr Parameter, + uint DueTime, + uint Period, + uint Flags); - [DllImport("Winmm.dll")] - public static extern uint timeSetEvent(uint uDelay, uint uResolution, TimeCallback lpTimeProc, UIntPtr dwUser, uint fuEvent); + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool DeleteTimerQueueTimer(IntPtr TimerQueue, IntPtr Timer, IntPtr CompletionEvent); [DllImport("user32.dll")] public static extern int ToUnicode( @@ -972,9 +989,6 @@ namespace Avalonia.Win32.Interop [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetMonitorInfo([In] IntPtr hMonitor, [Out] MONITORINFO lpmi); - [DllImport("kernel32.dll", SetLastError = true)] - public static extern bool QueryPerformanceCounter(out long lpPerformanceCount); - [return: MarshalAs(UnmanagedType.Bool)] [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "PostMessageW")] public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); diff --git a/src/Windows/Avalonia.Win32/RenderTimer.cs b/src/Windows/Avalonia.Win32/RenderTimer.cs index cea1bf94a5..0cb107d3a9 100644 --- a/src/Windows/Avalonia.Win32/RenderTimer.cs +++ b/src/Windows/Avalonia.Win32/RenderTimer.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Disposables; +using System.Threading; using Avalonia.Rendering; using Avalonia.Win32.Interop; @@ -7,7 +8,21 @@ namespace Avalonia.Win32 { internal class RenderTimer : DefaultRenderTimer { - private UnmanagedMethods.TimeCallback timerDelegate; + private UnmanagedMethods.WaitOrTimerCallback timerDelegate; + + private static IntPtr _timerQueue; + + private static void EnsureTimerQueueCreated() + { + if (Volatile.Read(ref _timerQueue) == null) + { + var queue = UnmanagedMethods.CreateTimerQueue(); + if (Interlocked.CompareExchange(ref _timerQueue, queue, IntPtr.Zero) != IntPtr.Zero) + { + UnmanagedMethods.DeleteTimerQueueEx(queue, IntPtr.Zero); + } + } + } public RenderTimer(int framesPerSecond) : base(framesPerSecond) @@ -16,23 +31,25 @@ namespace Avalonia.Win32 protected override IDisposable StartCore(Action tick) { - timerDelegate = (id, uMsg, user, dw1, dw2) => - { - UnmanagedMethods.QueryPerformanceCounter(out long tickCount); - tick(tickCount); - }; + EnsureTimerQueueCreated(); + var msPerFrame = 1000 / FramesPerSecond; + + timerDelegate = (_, __) => tick(TimeStampToFrames()); - var handle = UnmanagedMethods.timeSetEvent( - (uint)(1000 / FramesPerSecond), - 0, + UnmanagedMethods.CreateTimerQueueTimer( + out var timer, + _timerQueue, timerDelegate, - UIntPtr.Zero, - 1); + IntPtr.Zero, + (uint)msPerFrame, + (uint)msPerFrame, + 0 + ); return Disposable.Create(() => { timerDelegate = null; - UnmanagedMethods.timeKillEvent(handle); + UnmanagedMethods.DeleteTimerQueueTimer(_timerQueue, timer, IntPtr.Zero); }); } } From e24a125ec0b4add42060e25935521e5557a644f7 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 28 Jul 2018 00:49:13 -0500 Subject: [PATCH 08/44] Make MonoMac platform use DefaultRenderTimer infrastructure in its RenderTimer. --- src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs | 4 ++-- src/OSX/Avalonia.MonoMac/RenderTimer.cs | 21 +++++++++------------ 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs b/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs index 5dbf18b1de..5757413b7a 100644 --- a/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs +++ b/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs @@ -86,7 +86,7 @@ namespace Avalonia.MonoMac App = NSApplication.SharedApplication; UpdateActivationPolicy(); s_renderLoop = new RenderLoop(); - s_renderTimer = new RenderTimer(); //TODO: use CVDisplayLink + s_renderTimer = new RenderTimer(60); //TODO: use CVDisplayLink s_monoMacInitialized = true; } @@ -136,4 +136,4 @@ namespace Avalonia return builder.UseWindowingSubsystem(MonoMac.MonoMacPlatform.Initialize, "MonoMac"); } } -} \ No newline at end of file +} diff --git a/src/OSX/Avalonia.MonoMac/RenderTimer.cs b/src/OSX/Avalonia.MonoMac/RenderTimer.cs index 71f657b690..22ad2e81a2 100644 --- a/src/OSX/Avalonia.MonoMac/RenderTimer.cs +++ b/src/OSX/Avalonia.MonoMac/RenderTimer.cs @@ -6,26 +6,23 @@ using MonoMac.Foundation; namespace Avalonia.MonoMac { //TODO: Switch to using CVDisplayLink - public class RenderTimer : IRenderTimer + public class RenderTimer : DefaultRenderTimer { - private readonly object _lock = new object(); - private readonly IDisposable _timer; + public RenderTimer(int framesPerSecond) : base(framesPerSecond) + { + } - public RenderTimer() + protected override IDisposable StartCore(Action tick) { - _timer = AvaloniaLocator.Current.GetService().StartSystemTimer(new TimeSpan(0, 0, 0, 0, 1000 / 60), + return AvaloniaLocator.Current.GetService().StartSystemTimer( + TimeSpan.FromSeconds(1.0 / FramesPerSecond), () => { - lock (_lock) + using (new NSAutoreleasePool()) { - using (new NSAutoreleasePool()) - { - Tick?.Invoke(Environment.TickCount); - } + tick?.Invoke(Environment.TickCount); } }); } - - public event Action Tick; } } From 179ea4eca9666e53dfc678abb4d1f670128a3243 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Aug 2018 21:38:54 +0200 Subject: [PATCH 09/44] inTick doesn't need to be volatile. --- src/Avalonia.Visuals/Rendering/RenderLoop.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/RenderLoop.cs b/src/Avalonia.Visuals/Rendering/RenderLoop.cs index 5cac6993ff..076e8bbd33 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLoop.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLoop.cs @@ -10,7 +10,7 @@ namespace Avalonia.Rendering private readonly IDispatcher _dispatcher; private List _items = new List(); private IRenderTimer _timer; - private volatile bool inTick; + private bool inTick; public RenderLoop() { From 25b72971880093256d8773c19f950a8c470f1a9f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Aug 2018 21:46:11 +0200 Subject: [PATCH 10/44] Added RenderLoop docs. --- src/Avalonia.Visuals/Rendering/IRenderLoop.cs | 22 ++++++++++++++++++- src/Avalonia.Visuals/Rendering/RenderLoop.cs | 20 +++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/IRenderLoop.cs b/src/Avalonia.Visuals/Rendering/IRenderLoop.cs index bd1086d178..dd7442e7f8 100644 --- a/src/Avalonia.Visuals/Rendering/IRenderLoop.cs +++ b/src/Avalonia.Visuals/Rendering/IRenderLoop.cs @@ -1,8 +1,28 @@ namespace Avalonia.Rendering { + /// + /// The application render loop. + /// + /// + /// The render loop is responsible for advancing the animation timer and updating the scene + /// graph for visible windows. + /// public interface IRenderLoop { + /// + /// Adds an update task. + /// + /// The update task. + /// + /// Registered update tasks will be polled on each tick of the render loop after the + /// animation timer has been pulsed. + /// void Add(IRenderLoopTask i); + + /// + /// Removes an update task. + /// + /// The update task. void Remove(IRenderLoopTask i); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Rendering/RenderLoop.cs b/src/Avalonia.Visuals/Rendering/RenderLoop.cs index 076e8bbd33..98fc12b4b1 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLoop.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLoop.cs @@ -5,6 +5,13 @@ using Avalonia.Threading; namespace Avalonia.Rendering { + /// + /// The application render loop. + /// + /// + /// The render loop is responsible for advancing the animation timer and updating the scene + /// graph for visible windows. + /// public class RenderLoop : IRenderLoop { private readonly IDispatcher _dispatcher; @@ -12,17 +19,28 @@ namespace Avalonia.Rendering private IRenderTimer _timer; private bool inTick; + /// + /// Initializes a new instance of the class. + /// public RenderLoop() { _dispatcher = Dispatcher.UIThread; } + /// + /// Initializes a new instance of the class. + /// + /// The render timer. + /// The UI thread dispatcher. public RenderLoop(IRenderTimer timer, IDispatcher dispatcher) { _timer = timer; _dispatcher = dispatcher; } + /// + /// Gets the render timer. + /// protected IRenderTimer Timer { get @@ -36,6 +54,7 @@ namespace Avalonia.Rendering } } + /// public void Add(IRenderLoopTask i) { Contract.Requires(i != null); @@ -49,6 +68,7 @@ namespace Avalonia.Rendering } } + /// public void Remove(IRenderLoopTask i) { Contract.Requires(i != null); From 5e1e25e6fa8a17e7ead7c55b1b0b17b73b0fbb05 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 6 Sep 2018 17:31:05 -0500 Subject: [PATCH 11/44] Clean up render timers to use Environment.TickCount. --- src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs | 2 +- src/Windows/Avalonia.Win32/RenderTimer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs b/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs index b05ecd5456..a83334ff5e 100644 --- a/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs +++ b/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs @@ -85,7 +85,7 @@ namespace Avalonia.Rendering _runtime = AvaloniaLocator.Current.GetService(); } - return _runtime.StartSystemTimer(TimeSpan.FromSeconds(1.0 / FramesPerSecond), () => tick(Stopwatch.GetTimestamp())); + return _runtime.StartSystemTimer(TimeSpan.FromSeconds(1.0 / FramesPerSecond), () => tick(Environment.TickCount)); } /// diff --git a/src/Windows/Avalonia.Win32/RenderTimer.cs b/src/Windows/Avalonia.Win32/RenderTimer.cs index 0cb107d3a9..c911bc3adf 100644 --- a/src/Windows/Avalonia.Win32/RenderTimer.cs +++ b/src/Windows/Avalonia.Win32/RenderTimer.cs @@ -34,7 +34,7 @@ namespace Avalonia.Win32 EnsureTimerQueueCreated(); var msPerFrame = 1000 / FramesPerSecond; - timerDelegate = (_, __) => tick(TimeStampToFrames()); + timerDelegate = (_, __) => tick(Environment.TickCount); UnmanagedMethods.CreateTimerQueueTimer( out var timer, From c5898b98af57b33b2db31a5de19680eedc5b1349 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 6 Sep 2018 17:31:37 -0500 Subject: [PATCH 12/44] Remove unused field. --- .../Rendering/DeferredRenderer.cs | 49 ++++++++----------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index f937964994..fcc90edd43 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -32,7 +32,6 @@ namespace Avalonia.Rendering private volatile IRef _scene; private DirtyVisuals _dirty; private IRef _overlay; - private bool _updateQueued; private object _rendering = new object(); private int _lastSceneId = -1; private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects(); @@ -394,42 +393,34 @@ namespace Avalonia.Rendering private void UpdateScene() { Dispatcher.UIThread.VerifyAccess(); - - try + if (_root.IsVisible) { - if (_root.IsVisible) - { - var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root)); - var scene = sceneRef.Item; + var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root)); + var scene = sceneRef.Item; - if (_dirty == null) - { - _dirty = new DirtyVisuals(); - _sceneBuilder.UpdateAll(scene); - } - else if (_dirty.Count > 0) + if (_dirty == null) + { + _dirty = new DirtyVisuals(); + _sceneBuilder.UpdateAll(scene); + } + else if (_dirty.Count > 0) + { + foreach (var visual in _dirty) { - foreach (var visual in _dirty) - { - _sceneBuilder.Update(scene, visual); - } + _sceneBuilder.Update(scene, visual); } + } - var oldScene = Interlocked.Exchange(ref _scene, sceneRef); - oldScene?.Dispose(); + var oldScene = Interlocked.Exchange(ref _scene, sceneRef); + oldScene?.Dispose(); - _dirty.Clear(); - (_root as IRenderRoot)?.Invalidate(new Rect(scene.Size)); - } - else - { - var oldScene = Interlocked.Exchange(ref _scene, null); - oldScene?.Dispose(); - } + _dirty.Clear(); + (_root as IRenderRoot)?.Invalidate(new Rect(scene.Size)); } - finally + else { - _updateQueued = false; + var oldScene = Interlocked.Exchange(ref _scene, null); + oldScene?.Dispose(); } } From 6fe8dba8d957648109978237d9aa665d9524f967 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 6 Sep 2018 22:23:13 -0500 Subject: [PATCH 13/44] Remove timing class and create Clock classes that can hook into the render loop. --- src/Avalonia.Animation/AnimationInstance`1.cs | 5 +- src/Avalonia.Animation/Clock.cs | 47 +++++++++++++++++ src/Avalonia.Animation/Timing.cs | 51 ------------------- src/Avalonia.Animation/TransitionInstance.cs | 7 ++- .../Animation/RenderLoopClock.cs | 21 ++++++++ .../Rendering/DeferredRenderer.cs | 2 +- .../Rendering/IRenderLoopTask.cs | 15 +++++- src/Avalonia.Visuals/Rendering/RenderLoop.cs | 17 +++---- src/Windows/Avalonia.Win32/Win32Platform.cs | 6 +++ 9 files changed, 101 insertions(+), 70 deletions(-) create mode 100644 src/Avalonia.Animation/Clock.cs delete mode 100644 src/Avalonia.Animation/Timing.cs create mode 100644 src/Avalonia.Visuals/Animation/RenderLoopClock.cs diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index 5a72904ed2..a8d8dc73ef 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -82,8 +82,7 @@ namespace Avalonia.Animation protected override void Subscribed() { - _timerSubscription = Timing.AnimationsTimer - .Subscribe(p => this.Step(p)); + _timerSubscription = Clock.GlobalClock.Subscribe(Step); } public void Step(TimeSpan frameTick) @@ -225,4 +224,4 @@ namespace Avalonia.Animation } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/Clock.cs b/src/Avalonia.Animation/Clock.cs new file mode 100644 index 0000000000..4c37d663b1 --- /dev/null +++ b/src/Avalonia.Animation/Clock.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Text; +using Avalonia.Reactive; + +namespace Avalonia.Animation +{ + public class Clock : IObservable + { + public static Clock GlobalClock => AvaloniaLocator.Current.GetService(); + + private ClockObservable _observable; + + private IObservable _connectedObservable; + + public Clock() + { + _observable = new ClockObservable(); + _connectedObservable = _observable.Publish().RefCount(); + } + + public bool HasSubscriptions => _observable.HasSubscriptions; + + public TimeSpan CurrentTime { get; private set; } + + public void Pulse(long tickCount) + { + var time = TimeSpan.FromMilliseconds(tickCount); + _observable.Pulse(time); + CurrentTime = time; + } + + public IDisposable Subscribe(IObserver observer) + { + return _connectedObservable.Subscribe(observer); + } + + private class ClockObservable : LightweightObservableBase + { + public bool HasSubscriptions { get; private set; } + public void Pulse(TimeSpan tickCount) => PublishNext(tickCount); + protected override void Initialize() => HasSubscriptions = true; + protected override void Deinitialize() => HasSubscriptions = false; + } + } +} diff --git a/src/Avalonia.Animation/Timing.cs b/src/Avalonia.Animation/Timing.cs deleted file mode 100644 index 8ee78018a9..0000000000 --- a/src/Avalonia.Animation/Timing.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Linq; -using System.Reactive.Linq; -using Avalonia.Reactive; -using Avalonia.Threading; - -namespace Avalonia.Animation -{ - /// - /// Provides global timing functions for animations. - /// - public class Timing - { - static TimerObservable _timer = new TimerObservable(); - - /// - /// Initializes static members of the class. - /// - static Timing() - { - AnimationsTimer = _timer - .Publish() - .RefCount(); - } - - public static bool HasSubscriptions => _timer.HasSubscriptions; - - internal static TimeSpan GetTickCount() => TimeSpan.FromMilliseconds(Environment.TickCount); - - /// - /// Gets the animation timer. - /// - internal static IObservable AnimationsTimer - { - get; - } - - public static void Pulse(long tickCount) => _timer.Pulse(tickCount); - - private class TimerObservable : LightweightObservableBase - { - public bool HasSubscriptions { get; private set; } - public void Pulse(long tickCount) => PublishNext(TimeSpan.FromMilliseconds(tickCount)); - protected override void Initialize() => HasSubscriptions = true; - protected override void Deinitialize() => HasSubscriptions = false; - } - } -} diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index e2719cb472..4c61adea28 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -45,10 +45,9 @@ namespace Avalonia.Animation protected override void Subscribed() { - startTime = Timing.GetTickCount(); - timerSubscription = Timing.AnimationsTimer - .Subscribe(t => TimerTick(t)); + startTime = Clock.GlobalClock.CurrentTime; + timerSubscription = Clock.GlobalClock.Subscribe(TimerTick); PublishNext(0.0d); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Animation/RenderLoopClock.cs b/src/Avalonia.Visuals/Animation/RenderLoopClock.cs new file mode 100644 index 0000000000..3d166035ec --- /dev/null +++ b/src/Avalonia.Visuals/Animation/RenderLoopClock.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Rendering; + +namespace Avalonia.Animation +{ + public class RenderLoopClock : Clock, IRenderLoopTask + { + bool IRenderLoopTask.NeedsUpdate => HasSubscriptions; + + void IRenderLoopTask.Render() + { + } + + void IRenderLoopTask.Update(long tickCount) + { + Pulse(tickCount); + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index fcc90edd43..3221dd85c6 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -166,7 +166,7 @@ namespace Avalonia.Rendering bool IRenderLoopTask.NeedsUpdate => _dirty == null || _dirty.Count > 0; - void IRenderLoopTask.Update() => UpdateScene(); + void IRenderLoopTask.Update(long tickCount) => UpdateScene(); void IRenderLoopTask.Render() { diff --git a/src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs b/src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs index 2f251a5c17..b031bf00df 100644 --- a/src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs +++ b/src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs @@ -6,7 +6,20 @@ namespace Avalonia.Rendering public interface IRenderLoopTask { bool NeedsUpdate { get; } - void Update(); + void Update(long tickCount); void Render(); } + + public class MockRenderLoopTask : IRenderLoopTask + { + public bool NeedsUpdate => true; + + public void Render() + { + } + + public void Update(long tickCount) + { + } + } } diff --git a/src/Avalonia.Visuals/Rendering/RenderLoop.cs b/src/Avalonia.Visuals/Rendering/RenderLoop.cs index 98fc12b4b1..a850b99c5e 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLoop.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLoop.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using Avalonia.Logging; using Avalonia.Threading; @@ -17,7 +18,7 @@ namespace Avalonia.Rendering private readonly IDispatcher _dispatcher; private List _items = new List(); private IRenderTimer _timer; - private bool inTick; + private int inTick; /// /// Initializes a new instance of the class. @@ -84,13 +85,11 @@ namespace Avalonia.Rendering private async void TimerTick(long tickCount) { - if (!inTick) + if (Interlocked.CompareExchange(ref inTick, 1, 0) == 0) { - inTick = true; - try { - var needsUpdate = Animation.Timing.HasSubscriptions; + var needsUpdate = false; foreach (var i in _items) { @@ -105,13 +104,11 @@ namespace Avalonia.Rendering { await _dispatcher.InvokeAsync(() => { - Animation.Timing.Pulse(tickCount); - foreach (var i in _items) { - i.Update(); + i.Update(tickCount); } - }); + }).ConfigureAwait(false); } foreach (var i in _items) @@ -125,7 +122,7 @@ namespace Avalonia.Rendering } finally { - inTick = false; + Interlocked.Exchange(ref inTick, 0); } } } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index d5eb97292a..812a557765 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -9,6 +9,7 @@ using System.IO; using System.Reactive.Disposables; using System.Runtime.InteropServices; using System.Threading; +using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; @@ -76,6 +77,8 @@ namespace Avalonia.Win32 public static void Initialize(bool deferredRendering = true) { + var clock = new RenderLoopClock(); + AvaloniaLocator.CurrentMutable .Bind().ToSingleton() .Bind().ToConstant(CursorFactory.Instance) @@ -84,6 +87,7 @@ namespace Avalonia.Win32 .Bind().ToConstant(s_instance) .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new RenderTimer(60)) + .Bind().ToConstant(clock) .Bind().ToSingleton() .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance); @@ -93,6 +97,8 @@ namespace Avalonia.Win32 if (OleContext.Current != null) AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); + + AvaloniaLocator.Current.GetService().Add(clock); } public bool HasMessages() From 5c8ced89a30e3339f070d75b3f361f8342ae99fa Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 6 Sep 2018 22:52:41 -0500 Subject: [PATCH 14/44] Move GlobalPlayState tracking to Clock. --- src/Avalonia.Animation/Animation.cs | 7 ++++++- src/Avalonia.Animation/Clock.cs | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 2c359ecac3..7b3aa06ea0 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -17,10 +17,15 @@ namespace Avalonia.Animation /// public class Animation : AvaloniaList, IAnimation { + /// /// Gets or sets the animation play state for all animations /// - public static PlayState GlobalPlayState { get; set; } = PlayState.Run; + public static PlayState GlobalPlayState + { + get => AvaloniaLocator.Current.GetService().PlayState; + set => AvaloniaLocator.Current.GetService().PlayState = value; + } /// /// Gets or sets the active time of this animation. diff --git a/src/Avalonia.Animation/Clock.cs b/src/Avalonia.Animation/Clock.cs index 4c37d663b1..1ba6425551 100644 --- a/src/Avalonia.Animation/Clock.cs +++ b/src/Avalonia.Animation/Clock.cs @@ -24,6 +24,8 @@ namespace Avalonia.Animation public TimeSpan CurrentTime { get; private set; } + public PlayState PlayState { get; set; } + public void Pulse(long tickCount) { var time = TimeSpan.FromMilliseconds(tickCount); From a987c4648b77407c9b17519a52747d4868ae524a Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 6 Sep 2018 22:57:06 -0500 Subject: [PATCH 15/44] Move Pause PlayState time tracking into Clock. --- src/Avalonia.Animation/AnimationInstance`1.cs | 26 +++--------------- src/Avalonia.Animation/Clock.cs | 27 ++++++++++++++++--- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index a8d8dc73ef..a2d25e4524 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -8,7 +8,7 @@ using Avalonia.Reactive; namespace Avalonia.Animation { /// - /// Handles interpolatoin and time-related functions + /// Handles interpolation and time-related functions /// for keyframe animations. /// internal class AnimationInstance : SingleSubscriberObservableBase @@ -30,8 +30,6 @@ namespace Avalonia.Animation private TimeSpan _delay; private TimeSpan _duration; private TimeSpan _firstFrameCount; - private TimeSpan _internalClock; - private TimeSpan? _previousClock; private Easings.Easing _easeFunc; private Action _onCompleteAction; private Func _interpolator; @@ -120,23 +118,6 @@ namespace Avalonia.Animation if (Animation.GlobalPlayState == PlayState.Stop || _targetControl.PlayState == PlayState.Stop) DoComplete(); - if (!_previousClock.HasValue) - { - _previousClock = systemTime; - _internalClock = TimeSpan.Zero; - } - else - { - if (Animation.GlobalPlayState == PlayState.Pause || _targetControl.PlayState == PlayState.Pause) - { - _previousClock = systemTime; - return; - } - var delta = systemTime - _previousClock; - _internalClock += delta.Value; - _previousClock = systemTime; - } - if (!_gotFirstKFValue) { _firstKFValue = (T)_parent.First().Value; @@ -145,7 +126,7 @@ namespace Avalonia.Animation if (!_gotFirstFrameCount) { - _firstFrameCount = _internalClock; + _firstFrameCount = systemTime; _gotFirstFrameCount = true; } } @@ -154,7 +135,7 @@ namespace Avalonia.Animation { DoPlayStatesAndTime(systemTime); - var time = _internalClock - _firstFrameCount; + var time = systemTime - _firstFrameCount; var delayEndpoint = _delay; var iterationEndpoint = delayEndpoint + _duration; @@ -179,7 +160,6 @@ namespace Avalonia.Animation } else { - _previousClock = systemTime; return; } diff --git a/src/Avalonia.Animation/Clock.cs b/src/Avalonia.Animation/Clock.cs index 1ba6425551..ced63163c9 100644 --- a/src/Avalonia.Animation/Clock.cs +++ b/src/Avalonia.Animation/Clock.cs @@ -14,6 +14,9 @@ namespace Avalonia.Animation private IObservable _connectedObservable; + private TimeSpan? _previousTime; + private TimeSpan _internalTime; + public Clock() { _observable = new ClockObservable(); @@ -28,9 +31,27 @@ namespace Avalonia.Animation public void Pulse(long tickCount) { - var time = TimeSpan.FromMilliseconds(tickCount); - _observable.Pulse(time); - CurrentTime = time; + var systemTime = TimeSpan.FromMilliseconds(tickCount); + + if (!_previousTime.HasValue) + { + _previousTime = systemTime; + _internalTime = TimeSpan.Zero; + } + else + { + if (PlayState == PlayState.Pause) + { + _previousTime = systemTime; + return; + } + var delta = systemTime - _previousTime; + _internalTime += delta.Value; + _previousTime = systemTime; + } + + _observable.Pulse(_internalTime); + CurrentTime = _internalTime; } public IDisposable Subscribe(IObserver observer) From 9b9676d3fe346610fdc05eededd2c90253430bd7 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 6 Sep 2018 23:03:53 -0500 Subject: [PATCH 16/44] Enable chaining clocks. --- src/Avalonia.Animation/Clock.cs | 16 +++++++++++----- .../Animation/RenderLoopClock.cs | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Animation/Clock.cs b/src/Avalonia.Animation/Clock.cs index ced63163c9..2de781ea57 100644 --- a/src/Avalonia.Animation/Clock.cs +++ b/src/Avalonia.Animation/Clock.cs @@ -14,25 +14,31 @@ namespace Avalonia.Animation private IObservable _connectedObservable; + private IDisposable _parentSubscription; + private TimeSpan? _previousTime; private TimeSpan _internalTime; - public Clock() + protected Clock() { _observable = new ClockObservable(); _connectedObservable = _observable.Publish().RefCount(); } + public Clock(Clock parent) + :this() + { + _parentSubscription = parent.Subscribe(Pulse); + } + public bool HasSubscriptions => _observable.HasSubscriptions; public TimeSpan CurrentTime { get; private set; } public PlayState PlayState { get; set; } - public void Pulse(long tickCount) + protected void Pulse(TimeSpan systemTime) { - var systemTime = TimeSpan.FromMilliseconds(tickCount); - if (!_previousTime.HasValue) { _previousTime = systemTime; @@ -62,7 +68,7 @@ namespace Avalonia.Animation private class ClockObservable : LightweightObservableBase { public bool HasSubscriptions { get; private set; } - public void Pulse(TimeSpan tickCount) => PublishNext(tickCount); + public void Pulse(TimeSpan time) => PublishNext(time); protected override void Initialize() => HasSubscriptions = true; protected override void Deinitialize() => HasSubscriptions = false; } diff --git a/src/Avalonia.Visuals/Animation/RenderLoopClock.cs b/src/Avalonia.Visuals/Animation/RenderLoopClock.cs index 3d166035ec..d60d366ad4 100644 --- a/src/Avalonia.Visuals/Animation/RenderLoopClock.cs +++ b/src/Avalonia.Visuals/Animation/RenderLoopClock.cs @@ -15,7 +15,7 @@ namespace Avalonia.Animation void IRenderLoopTask.Update(long tickCount) { - Pulse(tickCount); + Pulse(TimeSpan.FromMilliseconds(tickCount)); } } } From bf1b78c4ab0da4ccb60a1d5456da50a2bf5092f9 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 6 Sep 2018 23:39:25 -0500 Subject: [PATCH 17/44] Enable Animations to run on a non-global clock. --- src/Avalonia.Animation/Animation.cs | 15 ++++++++++----- src/Avalonia.Animation/AnimationInstance`1.cs | 8 +++++--- src/Avalonia.Animation/Animator`1.cs | 8 ++++---- src/Avalonia.Animation/Clock.cs | 10 ++++++++++ src/Avalonia.Animation/IAnimation.cs | 4 ++-- src/Avalonia.Animation/IAnimator.cs | 2 +- src/Avalonia.Styling/Styling/Style.cs | 2 +- src/Avalonia.Visuals/Animation/RenderLoopClock.cs | 5 +++++ .../Animation/TransformAnimator.cs | 6 +++--- 9 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 7b3aa06ea0..e787143b59 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -154,12 +154,12 @@ namespace Avalonia.Animation } /// - public IDisposable Apply(Animatable control, IObservable match, Action onComplete) + public IDisposable Apply(Animatable control, Clock clock, IObservable match, Action onComplete) { var (animators, subscriptions) = InterpretKeyframes(control); if (animators.Count == 1) { - subscriptions.Add(animators[0].Apply(this, control, match, onComplete)); + subscriptions.Add(animators[0].Apply(this, control, clock, match, onComplete)); } else { @@ -173,7 +173,7 @@ namespace Avalonia.Animation animatorOnComplete = () => tcs.SetResult(null); completionTasks.Add(tcs.Task); } - subscriptions.Add(animator.Apply(this, control, match, animatorOnComplete)); + subscriptions.Add(animator.Apply(this, control, clock, match, animatorOnComplete)); } if (onComplete != null) @@ -185,15 +185,20 @@ namespace Avalonia.Animation } /// - public Task RunAsync(Animatable control) + public Task RunAsync(Animatable control, Clock clock = null) { + if (clock == null) + { + clock = Clock.GlobalClock; + } + var run = new TaskCompletionSource(); if (this.RepeatCount == RepeatCount.Loop) run.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); IDisposable subscriptions = null; - subscriptions = this.Apply(control, Observable.Return(true), () => + subscriptions = this.Apply(control, clock, Observable.Return(true), () => { run.SetResult(null); subscriptions?.Dispose(); diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index a2d25e4524..fe84dd879a 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -34,8 +34,9 @@ namespace Avalonia.Animation private Action _onCompleteAction; private Func _interpolator; private IDisposable _timerSubscription; + private readonly Clock _clock; - public AnimationInstance(Animation animation, Animatable control, Animator animator, Action OnComplete, Func Interpolator) + public AnimationInstance(Animation animation, Animatable control, Animator animator, Clock clock, Action OnComplete, Func Interpolator) { if (animation.SpeedRatio <= 0) throw new InvalidOperationException("Speed ratio cannot be negative or zero."); @@ -71,6 +72,7 @@ namespace Avalonia.Animation _fillMode = animation.FillMode; _onCompleteAction = OnComplete; _interpolator = Interpolator; + _clock = clock; } protected override void Unsubscribed() @@ -80,7 +82,7 @@ namespace Avalonia.Animation protected override void Subscribed() { - _timerSubscription = Clock.GlobalClock.Subscribe(Step); + _timerSubscription = _clock.Subscribe(Step); } public void Step(TimeSpan frameTick) @@ -115,7 +117,7 @@ namespace Avalonia.Animation private void DoPlayStatesAndTime(TimeSpan systemTime) { - if (Animation.GlobalPlayState == PlayState.Stop || _targetControl.PlayState == PlayState.Stop) + if (_clock.PlayState == PlayState.Stop || _targetControl.PlayState == PlayState.Stop) DoComplete(); if (!_gotFirstKFValue) diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index f0ef55aa9e..c699ff635a 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -32,7 +32,7 @@ namespace Avalonia.Animation } /// - public virtual IDisposable Apply(Animation animation, Animatable control, IObservable match, Action onComplete) + public virtual IDisposable Apply(Animation animation, Animatable control, Clock clock, IObservable match, Action onComplete) { if (!_isVerifiedAndConverted) VerifyConvertKeyFrames(); @@ -41,7 +41,7 @@ namespace Avalonia.Animation .Where(p => p) .Subscribe(_ => { - var timerObs = RunKeyFrames(animation, control, onComplete); + var timerObs = RunKeyFrames(animation, control, clock, onComplete); }); } @@ -101,9 +101,9 @@ namespace Avalonia.Animation /// /// Runs the KeyFrames Animation. /// - private IDisposable RunKeyFrames(Animation animation, Animatable control, Action onComplete) + private IDisposable RunKeyFrames(Animation animation, Animatable control, Clock clock, Action onComplete) { - var instance = new AnimationInstance(animation, control, this, onComplete, DoInterpolation); + var instance = new AnimationInstance(animation, control, this, clock, onComplete, DoInterpolation); return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); } diff --git a/src/Avalonia.Animation/Clock.cs b/src/Avalonia.Animation/Clock.cs index 2de781ea57..e8616c9694 100644 --- a/src/Avalonia.Animation/Clock.cs +++ b/src/Avalonia.Animation/Clock.cs @@ -58,6 +58,16 @@ namespace Avalonia.Animation _observable.Pulse(_internalTime); CurrentTime = _internalTime; + + if (PlayState == PlayState.Stop) + { + Stop(); + } + } + + protected virtual void Stop() + { + _parentSubscription?.Dispose(); } public IDisposable Subscribe(IObserver observer) diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index 1d545a322a..f726cf43dc 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -11,11 +11,11 @@ namespace Avalonia.Animation /// /// Apply the animation to the specified control /// - IDisposable Apply(Animatable control, IObservable match, Action onComplete = null); + IDisposable Apply(Animatable control, Clock clock, IObservable match, Action onComplete = null); /// /// Run the animation to the specified control /// - Task RunAsync(Animatable control); + Task RunAsync(Animatable control, Clock clock); } } diff --git a/src/Avalonia.Animation/IAnimator.cs b/src/Avalonia.Animation/IAnimator.cs index 9a4da35a02..134b30a555 100644 --- a/src/Avalonia.Animation/IAnimator.cs +++ b/src/Avalonia.Animation/IAnimator.cs @@ -16,6 +16,6 @@ namespace Avalonia.Animation /// /// Applies the current KeyFrame group to the specified control. /// - IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch, Action onComplete); + IDisposable Apply(Animation animation, Animatable control, Clock clock, IObservable obsMatch, Action onComplete); } } diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 399be5470d..a033184588 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -120,7 +120,7 @@ namespace Avalonia.Styling obsMatch = Observable.Return(true); } - var sub = animation.Apply((Animatable)control, obsMatch); + var sub = animation.Apply((Animatable)control, Clock.GlobalClock, obsMatch); subs.Add(sub); } diff --git a/src/Avalonia.Visuals/Animation/RenderLoopClock.cs b/src/Avalonia.Visuals/Animation/RenderLoopClock.cs index d60d366ad4..d9ee269739 100644 --- a/src/Avalonia.Visuals/Animation/RenderLoopClock.cs +++ b/src/Avalonia.Visuals/Animation/RenderLoopClock.cs @@ -7,6 +7,11 @@ namespace Avalonia.Animation { public class RenderLoopClock : Clock, IRenderLoopTask { + protected override void Stop() + { + AvaloniaLocator.Current.GetService().Remove(this); + } + bool IRenderLoopTask.NeedsUpdate => HasSubscriptions; void IRenderLoopTask.Render() diff --git a/src/Avalonia.Visuals/Animation/TransformAnimator.cs b/src/Avalonia.Visuals/Animation/TransformAnimator.cs index 2be1226abe..4476058bfe 100644 --- a/src/Avalonia.Visuals/Animation/TransformAnimator.cs +++ b/src/Avalonia.Visuals/Animation/TransformAnimator.cs @@ -12,7 +12,7 @@ namespace Avalonia.Animation DoubleAnimator childKeyFrames; /// - public override IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch, Action onComplete) + public override IDisposable Apply(Animation animation, Animatable control, Clock clock, IObservable obsMatch, Action onComplete) { var ctrl = (Visual)control; @@ -44,7 +44,7 @@ namespace Avalonia.Animation // It's a transform object so let's target that. if (renderTransformType == Property.OwnerType) { - return childKeyFrames.Apply(animation, ctrl.RenderTransform, obsMatch, onComplete); + return childKeyFrames.Apply(animation, ctrl.RenderTransform, clock, obsMatch, onComplete); } // It's a TransformGroup and try finding the target there. else if (renderTransformType == typeof(TransformGroup)) @@ -53,7 +53,7 @@ namespace Avalonia.Animation { if (transform.GetType() == Property.OwnerType) { - return childKeyFrames.Apply(animation, transform, obsMatch, onComplete); + return childKeyFrames.Apply(animation, transform, clock, obsMatch, onComplete); } } } From b0368c80b29e7ea7fcc99f32c53252c2218fbc4b Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 6 Sep 2018 23:43:06 -0500 Subject: [PATCH 18/44] Enable transitions to run on custom clocks. --- src/Avalonia.Animation/Animatable.cs | 2 +- src/Avalonia.Animation/ITransition.cs | 2 +- src/Avalonia.Animation/TransitionInstance.cs | 8 +++++--- src/Avalonia.Animation/Transition`1.cs | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index e51103aa55..5208356570 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -69,7 +69,7 @@ namespace Avalonia.Animation if (match != null) { - match.Apply(this, e.OldValue, e.NewValue); + match.Apply(this, Clock.GlobalClock, e.OldValue, e.NewValue); } } } diff --git a/src/Avalonia.Animation/ITransition.cs b/src/Avalonia.Animation/ITransition.cs index e2ffe7fc6e..7afaa2325a 100644 --- a/src/Avalonia.Animation/ITransition.cs +++ b/src/Avalonia.Animation/ITransition.cs @@ -13,7 +13,7 @@ namespace Avalonia.Animation /// /// Applies the transition to the specified . /// - IDisposable Apply(Animatable control, object oldValue, object newValue); + IDisposable Apply(Animatable control, Clock clock, object oldValue, object newValue); /// /// Gets the property to be animated. diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index 4c61adea28..b0c927f3cd 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -18,10 +18,12 @@ namespace Avalonia.Animation private IDisposable timerSubscription; private TimeSpan startTime; private TimeSpan duration; + private readonly Clock _clock; - public TransitionInstance(TimeSpan Duration) + public TransitionInstance(Clock clock, TimeSpan Duration) { duration = Duration; + _clock = clock; } private void TimerTick(TimeSpan t) @@ -45,8 +47,8 @@ namespace Avalonia.Animation protected override void Subscribed() { - startTime = Clock.GlobalClock.CurrentTime; - timerSubscription = Clock.GlobalClock.Subscribe(TimerTick); + startTime = _clock.CurrentTime; + timerSubscription = _clock.Subscribe(TimerTick); PublishNext(0.0d); } } diff --git a/src/Avalonia.Animation/Transition`1.cs b/src/Avalonia.Animation/Transition`1.cs index 4b01c54f5c..23df7f9807 100644 --- a/src/Avalonia.Animation/Transition`1.cs +++ b/src/Avalonia.Animation/Transition`1.cs @@ -49,9 +49,9 @@ namespace Avalonia.Animation public abstract IObservable DoTransition(IObservable progress, T oldValue, T newValue); /// - public virtual IDisposable Apply(Animatable control, object oldValue, object newValue) + public virtual IDisposable Apply(Animatable control, Clock clock, object oldValue, object newValue) { - var transition = DoTransition(new TransitionInstance(Duration), (T)oldValue, (T)newValue); + var transition = DoTransition(new TransitionInstance(clock, Duration), (T)oldValue, (T)newValue); return control.Bind((AvaloniaProperty)Property, transition, Data.BindingPriority.Animation); } From 6a380d6591e0f07e15edcbb6ace96baabec9b287 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 6 Sep 2018 23:49:52 -0500 Subject: [PATCH 19/44] Reorganize RenderLoopClock registration. --- src/Avalonia.Controls/AppBuilderBase.cs | 2 +- src/Avalonia.Controls/Application.cs | 7 +++++++ src/Windows/Avalonia.Win32/Win32Platform.cs | 5 ----- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index c92d5d7694..9561282274 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -272,10 +272,10 @@ namespace Avalonia.Controls s_setupWasAlreadyCalled = true; - Instance.RegisterServices(); RuntimePlatformServicesInitializer(); WindowingSubsystemInitializer(); RenderingSubsystemInitializer(); + Instance.RegisterServices(); Instance.Initialize(); AfterSetupCallback(Self); } diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 4c549ac7d4..8f6544ccd8 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -4,12 +4,14 @@ using System; using System.Reactive.Concurrency; using System.Threading; +using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Input.Raw; using Avalonia.Platform; +using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Threading; @@ -335,6 +337,11 @@ namespace Avalonia .Bind().ToConstant(AvaloniaScheduler.Instance) .Bind().ToConstant(DragDropDevice.Instance) .Bind().ToTransient(); + + var clock = new RenderLoopClock(); + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(clock) + .GetService().Add(clock); } } } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 812a557765..ef2dfd3c1a 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -77,8 +77,6 @@ namespace Avalonia.Win32 public static void Initialize(bool deferredRendering = true) { - var clock = new RenderLoopClock(); - AvaloniaLocator.CurrentMutable .Bind().ToSingleton() .Bind().ToConstant(CursorFactory.Instance) @@ -87,7 +85,6 @@ namespace Avalonia.Win32 .Bind().ToConstant(s_instance) .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new RenderTimer(60)) - .Bind().ToConstant(clock) .Bind().ToSingleton() .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance); @@ -97,8 +94,6 @@ namespace Avalonia.Win32 if (OleContext.Current != null) AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); - - AvaloniaLocator.Current.GetService().Add(clock); } public bool HasMessages() From 0c611b981d86e979b0989b0c5b3c15ce0a5ef81a Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 6 Sep 2018 23:57:38 -0500 Subject: [PATCH 20/44] Refactor clock types. --- src/Avalonia.Animation/Clock.cs | 71 ++---------------- src/Avalonia.Animation/ClockBase.cs | 75 +++++++++++++++++++ src/Avalonia.Animation/IClock.cs | 13 ++++ src/Avalonia.Controls/Application.cs | 2 +- .../Animation/RenderLoopClock.cs | 2 +- 5 files changed, 95 insertions(+), 68 deletions(-) create mode 100644 src/Avalonia.Animation/ClockBase.cs create mode 100644 src/Avalonia.Animation/IClock.cs diff --git a/src/Avalonia.Animation/Clock.cs b/src/Avalonia.Animation/Clock.cs index e8616c9694..f61a4c7db1 100644 --- a/src/Avalonia.Animation/Clock.cs +++ b/src/Avalonia.Animation/Clock.cs @@ -6,81 +6,20 @@ using Avalonia.Reactive; namespace Avalonia.Animation { - public class Clock : IObservable + public class Clock : ClockBase { - public static Clock GlobalClock => AvaloniaLocator.Current.GetService(); - - private ClockObservable _observable; - - private IObservable _connectedObservable; + public static IClock GlobalClock => AvaloniaLocator.Current.GetService(); private IDisposable _parentSubscription; - - private TimeSpan? _previousTime; - private TimeSpan _internalTime; - - protected Clock() - { - _observable = new ClockObservable(); - _connectedObservable = _observable.Publish().RefCount(); - } - - public Clock(Clock parent) - :this() + + public Clock(IClock parent) { _parentSubscription = parent.Subscribe(Pulse); } - public bool HasSubscriptions => _observable.HasSubscriptions; - - public TimeSpan CurrentTime { get; private set; } - - public PlayState PlayState { get; set; } - - protected void Pulse(TimeSpan systemTime) - { - if (!_previousTime.HasValue) - { - _previousTime = systemTime; - _internalTime = TimeSpan.Zero; - } - else - { - if (PlayState == PlayState.Pause) - { - _previousTime = systemTime; - return; - } - var delta = systemTime - _previousTime; - _internalTime += delta.Value; - _previousTime = systemTime; - } - - _observable.Pulse(_internalTime); - CurrentTime = _internalTime; - - if (PlayState == PlayState.Stop) - { - Stop(); - } - } - - protected virtual void Stop() + protected override void Stop() { _parentSubscription?.Dispose(); } - - public IDisposable Subscribe(IObserver observer) - { - return _connectedObservable.Subscribe(observer); - } - - private class ClockObservable : LightweightObservableBase - { - public bool HasSubscriptions { get; private set; } - public void Pulse(TimeSpan time) => PublishNext(time); - protected override void Initialize() => HasSubscriptions = true; - protected override void Deinitialize() => HasSubscriptions = false; - } } } diff --git a/src/Avalonia.Animation/ClockBase.cs b/src/Avalonia.Animation/ClockBase.cs new file mode 100644 index 0000000000..ea784269d9 --- /dev/null +++ b/src/Avalonia.Animation/ClockBase.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Text; +using Avalonia.Reactive; + +namespace Avalonia.Animation +{ + public class ClockBase : IClock + { + private ClockObservable _observable; + + private IObservable _connectedObservable; + + private TimeSpan? _previousTime; + private TimeSpan _internalTime; + + protected ClockBase() + { + _observable = new ClockObservable(); + _connectedObservable = _observable.Publish().RefCount(); + } + + public bool HasSubscriptions => _observable.HasSubscriptions; + + public TimeSpan CurrentTime { get; private set; } + + public PlayState PlayState { get; set; } + + protected void Pulse(TimeSpan systemTime) + { + if (!_previousTime.HasValue) + { + _previousTime = systemTime; + _internalTime = TimeSpan.Zero; + } + else + { + if (PlayState == PlayState.Pause) + { + _previousTime = systemTime; + return; + } + var delta = systemTime - _previousTime; + _internalTime += delta.Value; + _previousTime = systemTime; + } + + _observable.Pulse(_internalTime); + CurrentTime = _internalTime; + + if (PlayState == PlayState.Stop) + { + Stop(); + } + } + + protected virtual void Stop() + { + } + + public IDisposable Subscribe(IObserver observer) + { + return _connectedObservable.Subscribe(observer); + } + + private class ClockObservable : LightweightObservableBase + { + public bool HasSubscriptions { get; private set; } + public void Pulse(TimeSpan time) => PublishNext(time); + protected override void Initialize() => HasSubscriptions = true; + protected override void Deinitialize() => HasSubscriptions = false; + } + } +} diff --git a/src/Avalonia.Animation/IClock.cs b/src/Avalonia.Animation/IClock.cs new file mode 100644 index 0000000000..58c997841d --- /dev/null +++ b/src/Avalonia.Animation/IClock.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Animation +{ + public interface IClock : IObservable + { + bool HasSubscriptions { get; } + TimeSpan CurrentTime { get; } + PlayState PlayState { get; set; } + } +} diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 8f6544ccd8..8c03bac61a 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -340,7 +340,7 @@ namespace Avalonia var clock = new RenderLoopClock(); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(clock) + .Bind().ToConstant(clock) .GetService().Add(clock); } } diff --git a/src/Avalonia.Visuals/Animation/RenderLoopClock.cs b/src/Avalonia.Visuals/Animation/RenderLoopClock.cs index d9ee269739..e59b3aac0d 100644 --- a/src/Avalonia.Visuals/Animation/RenderLoopClock.cs +++ b/src/Avalonia.Visuals/Animation/RenderLoopClock.cs @@ -5,7 +5,7 @@ using Avalonia.Rendering; namespace Avalonia.Animation { - public class RenderLoopClock : Clock, IRenderLoopTask + public class RenderLoopClock : ClockBase, IRenderLoopTask { protected override void Stop() { From 59cad1cf86651e4ab3f3c015d1e0636c50c2ea08 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 7 Sep 2018 00:02:41 -0500 Subject: [PATCH 21/44] Fix unfinished clock type refactor. --- src/Avalonia.Animation/Animatable.cs | 5 ++++- src/Avalonia.Animation/Animation.cs | 4 ++-- src/Avalonia.Animation/Animator`1.cs | 4 ++-- src/Avalonia.Animation/Clock.cs | 5 +++++ src/Avalonia.Animation/IAnimation.cs | 4 ++-- src/Avalonia.Animation/IAnimator.cs | 2 +- src/Avalonia.Animation/ITransition.cs | 2 +- src/Avalonia.Animation/Transition`1.cs | 2 +- 8 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 5208356570..03a7a32e02 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -14,7 +14,10 @@ namespace Avalonia.Animation /// Base class for all animatable objects. /// public class Animatable : AvaloniaObject - { + { + public static readonly StyledProperty ClockProperty = + AvaloniaProperty.Register(nameof(Clock), inherits: true); + /// /// Defines the property. /// diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index e787143b59..1d2a7b4559 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -154,7 +154,7 @@ namespace Avalonia.Animation } /// - public IDisposable Apply(Animatable control, Clock clock, IObservable match, Action onComplete) + public IDisposable Apply(Animatable control, IClock clock, IObservable match, Action onComplete) { var (animators, subscriptions) = InterpretKeyframes(control); if (animators.Count == 1) @@ -185,7 +185,7 @@ namespace Avalonia.Animation } /// - public Task RunAsync(Animatable control, Clock clock = null) + public Task RunAsync(Animatable control, IClock clock = null) { if (clock == null) { diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index c699ff635a..44a10db545 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -32,7 +32,7 @@ namespace Avalonia.Animation } /// - public virtual IDisposable Apply(Animation animation, Animatable control, Clock clock, IObservable match, Action onComplete) + public virtual IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete) { if (!_isVerifiedAndConverted) VerifyConvertKeyFrames(); @@ -101,7 +101,7 @@ namespace Avalonia.Animation /// /// Runs the KeyFrames Animation. /// - private IDisposable RunKeyFrames(Animation animation, Animatable control, Clock clock, Action onComplete) + private IDisposable RunKeyFrames(Animation animation, Animatable control, IClock clock, Action onComplete) { var instance = new AnimationInstance(animation, control, this, clock, onComplete, DoInterpolation); return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); diff --git a/src/Avalonia.Animation/Clock.cs b/src/Avalonia.Animation/Clock.cs index f61a4c7db1..e009c2aad5 100644 --- a/src/Avalonia.Animation/Clock.cs +++ b/src/Avalonia.Animation/Clock.cs @@ -11,6 +11,11 @@ namespace Avalonia.Animation public static IClock GlobalClock => AvaloniaLocator.Current.GetService(); private IDisposable _parentSubscription; + + public Clock() + :this(GlobalClock) + { + } public Clock(IClock parent) { diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index f726cf43dc..34b0a5d769 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -11,11 +11,11 @@ namespace Avalonia.Animation /// /// Apply the animation to the specified control /// - IDisposable Apply(Animatable control, Clock clock, IObservable match, Action onComplete = null); + IDisposable Apply(Animatable control, IClock clock, IObservable match, Action onComplete = null); /// /// Run the animation to the specified control /// - Task RunAsync(Animatable control, Clock clock); + Task RunAsync(Animatable control, IClock clock); } } diff --git a/src/Avalonia.Animation/IAnimator.cs b/src/Avalonia.Animation/IAnimator.cs index 134b30a555..04bad8e112 100644 --- a/src/Avalonia.Animation/IAnimator.cs +++ b/src/Avalonia.Animation/IAnimator.cs @@ -16,6 +16,6 @@ namespace Avalonia.Animation /// /// Applies the current KeyFrame group to the specified control. /// - IDisposable Apply(Animation animation, Animatable control, Clock clock, IObservable obsMatch, Action onComplete); + IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable obsMatch, Action onComplete); } } diff --git a/src/Avalonia.Animation/ITransition.cs b/src/Avalonia.Animation/ITransition.cs index 7afaa2325a..e5d8466f04 100644 --- a/src/Avalonia.Animation/ITransition.cs +++ b/src/Avalonia.Animation/ITransition.cs @@ -13,7 +13,7 @@ namespace Avalonia.Animation /// /// Applies the transition to the specified . /// - IDisposable Apply(Animatable control, Clock clock, object oldValue, object newValue); + IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue); /// /// Gets the property to be animated. diff --git a/src/Avalonia.Animation/Transition`1.cs b/src/Avalonia.Animation/Transition`1.cs index 23df7f9807..b54ec8f51c 100644 --- a/src/Avalonia.Animation/Transition`1.cs +++ b/src/Avalonia.Animation/Transition`1.cs @@ -49,7 +49,7 @@ namespace Avalonia.Animation public abstract IObservable DoTransition(IObservable progress, T oldValue, T newValue); /// - public virtual IDisposable Apply(Animatable control, Clock clock, object oldValue, object newValue) + public virtual IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue) { var transition = DoTransition(new TransitionInstance(clock, Duration), (T)oldValue, (T)newValue); return control.Bind((AvaloniaProperty)Property, transition, Data.BindingPriority.Animation); From ec9c61bbbe4afcb18852b4992428817d3fa111a9 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 7 Sep 2018 00:11:49 -0500 Subject: [PATCH 22/44] Allow clocks to be bindable and inherited down the logical tree. --- src/Avalonia.Animation/Animatable.cs | 23 ++++--------------- src/Avalonia.Animation/AnimationInstance`1.cs | 4 ++-- src/Avalonia.Animation/TransitionInstance.cs | 2 +- src/Avalonia.Styling/Styling/Style.cs | 19 ++++++++------- .../Animation/TransformAnimator.cs | 2 +- 5 files changed, 19 insertions(+), 31 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 03a7a32e02..516f383b92 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -18,25 +18,10 @@ namespace Avalonia.Animation public static readonly StyledProperty ClockProperty = AvaloniaProperty.Register(nameof(Clock), inherits: true); - /// - /// Defines the property. - /// - public static readonly DirectProperty PlayStateProperty = - AvaloniaProperty.RegisterDirect( - nameof(PlayState), - o => o.PlayState, - (o, v) => o.PlayState = v); - - private PlayState _playState = PlayState.Run; - - /// - /// Gets or sets the state of the animation for this - /// control. - /// - public PlayState PlayState + public IClock Clock { - get { return _playState; } - set { SetAndRaise(PlayStateProperty, ref _playState, value); } + get => GetValue(ClockProperty); + set => SetValue(ClockProperty, value); } /// @@ -72,7 +57,7 @@ namespace Avalonia.Animation if (match != null) { - match.Apply(this, Clock.GlobalClock, e.OldValue, e.NewValue); + match.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue); } } } diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index fe84dd879a..99ebbe752a 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -34,7 +34,7 @@ namespace Avalonia.Animation private Action _onCompleteAction; private Func _interpolator; private IDisposable _timerSubscription; - private readonly Clock _clock; + private readonly IClock _clock; public AnimationInstance(Animation animation, Animatable control, Animator animator, Clock clock, Action OnComplete, Func Interpolator) { @@ -117,7 +117,7 @@ namespace Avalonia.Animation private void DoPlayStatesAndTime(TimeSpan systemTime) { - if (_clock.PlayState == PlayState.Stop || _targetControl.PlayState == PlayState.Stop) + if (_clock.PlayState == PlayState.Stop) DoComplete(); if (!_gotFirstKFValue) diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index b0c927f3cd..ad87ad7010 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -18,7 +18,7 @@ namespace Avalonia.Animation private IDisposable timerSubscription; private TimeSpan startTime; private TimeSpan duration; - private readonly Clock _clock; + private readonly IClock _clock; public TransitionInstance(Clock clock, TimeSpan Duration) { diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index a033184588..62b3ca72ae 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -111,17 +111,20 @@ namespace Avalonia.Styling { var subs = GetSubscriptions(control); - foreach (var animation in Animations) + if (control is Animatable animatable) { - IObservable obsMatch = match.ObservableResult; - - if (match.ImmediateResult == true) + foreach (var animation in Animations) { - obsMatch = Observable.Return(true); - } + IObservable obsMatch = match.ObservableResult; - var sub = animation.Apply((Animatable)control, Clock.GlobalClock, obsMatch); - subs.Add(sub); + if (match.ImmediateResult == true) + { + obsMatch = Observable.Return(true); + } + + var sub = animation.Apply(animatable, animatable.Clock ?? Clock.GlobalClock, obsMatch); + subs.Add(sub); + } } foreach (var setter in Setters) diff --git a/src/Avalonia.Visuals/Animation/TransformAnimator.cs b/src/Avalonia.Visuals/Animation/TransformAnimator.cs index 4476058bfe..6336c49dc5 100644 --- a/src/Avalonia.Visuals/Animation/TransformAnimator.cs +++ b/src/Avalonia.Visuals/Animation/TransformAnimator.cs @@ -12,7 +12,7 @@ namespace Avalonia.Animation DoubleAnimator childKeyFrames; /// - public override IDisposable Apply(Animation animation, Animatable control, Clock clock, IObservable obsMatch, Action onComplete) + public override IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable obsMatch, Action onComplete) { var ctrl = (Visual)control; From 51faa94534c2ca0a0de03305f96d10a8cdb6ce8e Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 7 Sep 2018 13:49:44 -0500 Subject: [PATCH 23/44] Allow users to supply custom clocks in XAML or code before animations are applied. Change AnimationsPage to show an example with a custom clock and the animations on that page. --- samples/RenderDemo/Pages/AnimationsPage.xaml | 7 +++-- .../RenderDemo/Pages/AnimationsPage.xaml.cs | 16 ++++++++++++ .../ViewModels/AnimationsPageViewModel.cs | 26 +++++-------------- src/Avalonia.Animation/Animation.cs | 10 ------- src/Avalonia.Animation/Animator`1.cs | 8 +++++- src/Avalonia.Styling/Styling/Style.cs | 2 +- .../Animation/TransformAnimator.cs | 4 +-- 7 files changed, 37 insertions(+), 36 deletions(-) diff --git a/samples/RenderDemo/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml index 5287e4e373..473807ac50 100644 --- a/samples/RenderDemo/Pages/AnimationsPage.xaml +++ b/samples/RenderDemo/Pages/AnimationsPage.xaml @@ -107,9 +107,12 @@ + + + Hover to activate Transform Keyframe Animations. - public class Animation : AvaloniaList, IAnimation { - - /// - /// Gets or sets the animation play state for all animations - /// - public static PlayState GlobalPlayState - { - get => AvaloniaLocator.Current.GetService().PlayState; - set => AvaloniaLocator.Current.GetService().PlayState = value; - } - /// /// Gets or sets the active time of this animation. /// diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 44a10db545..b68f2fc79a 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -103,7 +103,13 @@ namespace Avalonia.Animation /// private IDisposable RunKeyFrames(Animation animation, Animatable control, IClock clock, Action onComplete) { - var instance = new AnimationInstance(animation, control, this, clock, onComplete, DoInterpolation); + var instance = new AnimationInstance( + animation, + control, + this, + clock ?? control.Clock ?? Clock.GlobalClock, + onComplete, + DoInterpolation); return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); } diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 62b3ca72ae..067bb59fe9 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -122,7 +122,7 @@ namespace Avalonia.Styling obsMatch = Observable.Return(true); } - var sub = animation.Apply(animatable, animatable.Clock ?? Clock.GlobalClock, obsMatch); + var sub = animation.Apply(animatable, null, obsMatch); subs.Add(sub); } } diff --git a/src/Avalonia.Visuals/Animation/TransformAnimator.cs b/src/Avalonia.Visuals/Animation/TransformAnimator.cs index 6336c49dc5..721a895900 100644 --- a/src/Avalonia.Visuals/Animation/TransformAnimator.cs +++ b/src/Avalonia.Visuals/Animation/TransformAnimator.cs @@ -44,7 +44,7 @@ namespace Avalonia.Animation // It's a transform object so let's target that. if (renderTransformType == Property.OwnerType) { - return childKeyFrames.Apply(animation, ctrl.RenderTransform, clock, obsMatch, onComplete); + return childKeyFrames.Apply(animation, ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete); } // It's a TransformGroup and try finding the target there. else if (renderTransformType == typeof(TransformGroup)) @@ -53,7 +53,7 @@ namespace Avalonia.Animation { if (transform.GetType() == Property.OwnerType) { - return childKeyFrames.Apply(animation, transform, clock, obsMatch, onComplete); + return childKeyFrames.Apply(animation, transform, clock ?? control.Clock, obsMatch, onComplete); } } } From 6b0ef13027e60cd63131077a6695af14d40cb717 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 7 Sep 2018 15:29:11 -0500 Subject: [PATCH 24/44] Clean up naming in TransformAnimator. --- .../Animation/TransformAnimator.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Visuals/Animation/TransformAnimator.cs b/src/Avalonia.Visuals/Animation/TransformAnimator.cs index 721a895900..64b3ebd626 100644 --- a/src/Avalonia.Visuals/Animation/TransformAnimator.cs +++ b/src/Avalonia.Visuals/Animation/TransformAnimator.cs @@ -9,7 +9,7 @@ namespace Avalonia.Animation /// public class TransformAnimator : Animator { - DoubleAnimator childKeyFrames; + DoubleAnimator childAnimator; /// public override IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable obsMatch, Action onComplete) @@ -36,15 +36,15 @@ namespace Avalonia.Animation var renderTransformType = ctrl.RenderTransform.GetType(); - if (childKeyFrames == null) + if (childAnimator == null) { - InitializeChildKeyFrames(); + InitializeChildAnimator(); } // It's a transform object so let's target that. if (renderTransformType == Property.OwnerType) { - return childKeyFrames.Apply(animation, ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete); + return childAnimator.Apply(animation, ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete); } // It's a TransformGroup and try finding the target there. else if (renderTransformType == typeof(TransformGroup)) @@ -53,7 +53,7 @@ namespace Avalonia.Animation { if (transform.GetType() == Property.OwnerType) { - return childKeyFrames.Apply(animation, transform, clock ?? control.Clock, obsMatch, onComplete); + return childAnimator.Apply(animation, transform, clock ?? control.Clock, obsMatch, onComplete); } } } @@ -73,16 +73,16 @@ namespace Avalonia.Animation return null; } - void InitializeChildKeyFrames() + void InitializeChildAnimator() { - childKeyFrames = new DoubleAnimator(); + childAnimator = new DoubleAnimator(); foreach (AnimatorKeyFrame keyframe in this) { - childKeyFrames.Add(keyframe); + childAnimator.Add(keyframe); } - childKeyFrames.Property = Property; + childAnimator.Property = Property; } /// From 58a85c53c757962040abe68b7f1e00f02b3b50c3 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 7 Sep 2018 16:15:06 -0500 Subject: [PATCH 25/44] Have each AnimationInstance and TransitionInstance use their own internal clock instead of relying on tracking the start time of the global clock. Use a binary search to find the correct keyframe instead of linear search. --- .../ViewModels/AnimationsPageViewModel.cs | 2 +- src/Avalonia.Animation/Animation.cs | 5 -- src/Avalonia.Animation/AnimationInstance`1.cs | 32 +++---- src/Avalonia.Animation/Animator`1.cs | 84 +++++++++++-------- src/Avalonia.Animation/ClockBase.cs | 5 +- src/Avalonia.Animation/IClock.cs | 2 - src/Avalonia.Animation/TransitionInstance.cs | 26 +++--- src/Avalonia.Animation/Transition`1.cs | 3 - 8 files changed, 76 insertions(+), 83 deletions(-) diff --git a/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs index e4276fa1b5..7b89b7944c 100644 --- a/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs +++ b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs @@ -8,7 +8,7 @@ namespace RenderDemo.ViewModels { private bool _isPlaying = true; - private string _playStateText = "Pause all animations"; + private string _playStateText = "Pause animations on this page"; public void TogglePlayState() { diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 65463f3d52..d7efc69e10 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -177,11 +177,6 @@ namespace Avalonia.Animation /// public Task RunAsync(Animatable control, IClock clock = null) { - if (clock == null) - { - clock = Clock.GlobalClock; - } - var run = new TaskCompletionSource(); if (this.RepeatCount == RepeatCount.Loop) diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index 99ebbe752a..468c9c93a4 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -19,7 +19,6 @@ namespace Avalonia.Animation private double _currentIteration; private bool _isLooping; private bool _gotFirstKFValue; - private bool _gotFirstFrameCount; private bool _iterationDelay; private FillMode _fillMode; private PlaybackDirection _animationDirection; @@ -29,14 +28,14 @@ namespace Avalonia.Animation private double _speedRatio; private TimeSpan _delay; private TimeSpan _duration; - private TimeSpan _firstFrameCount; private Easings.Easing _easeFunc; private Action _onCompleteAction; private Func _interpolator; private IDisposable _timerSubscription; - private readonly IClock _clock; + private readonly IClock _baseClock; + private IClock _clock; - public AnimationInstance(Animation animation, Animatable control, Animator animator, Clock clock, Action OnComplete, Func Interpolator) + public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action OnComplete, Func Interpolator) { if (animation.SpeedRatio <= 0) throw new InvalidOperationException("Speed ratio cannot be negative or zero."); @@ -72,16 +71,18 @@ namespace Avalonia.Animation _fillMode = animation.FillMode; _onCompleteAction = OnComplete; _interpolator = Interpolator; - _clock = clock; + _baseClock = baseClock; } protected override void Unsubscribed() { _timerSubscription?.Dispose(); + _clock.PlayState = PlayState.Stop; } protected override void Subscribed() { + _clock = new Clock(_baseClock); _timerSubscription = _clock.Subscribe(Step); } @@ -115,9 +116,9 @@ namespace Avalonia.Animation PublishNext(_lastInterpValue); } - private void DoPlayStatesAndTime(TimeSpan systemTime) + private void DoPlayStates() { - if (_clock.PlayState == PlayState.Stop) + if (_clock.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop) DoComplete(); if (!_gotFirstKFValue) @@ -125,19 +126,12 @@ namespace Avalonia.Animation _firstKFValue = (T)_parent.First().Value; _gotFirstKFValue = true; } - - if (!_gotFirstFrameCount) - { - _firstFrameCount = systemTime; - _gotFirstFrameCount = true; - } } - private void InternalStep(TimeSpan systemTime) + private void InternalStep(TimeSpan time) { - DoPlayStatesAndTime(systemTime); - - var time = systemTime - _firstFrameCount; + DoPlayStates(); + var delayEndpoint = _delay; var iterationEndpoint = delayEndpoint + _duration; @@ -158,14 +152,14 @@ namespace Avalonia.Animation } //Calculate the current iteration number - _currentIteration = (int)Math.Floor((double)time.Ticks / iterationEndpoint.Ticks) + 2; + _currentIteration = (int)Math.Floor((double)((double)time.Ticks / iterationEndpoint.Ticks)) + 2; } else { return; } - time = TimeSpan.FromTicks(time.Ticks % iterationEndpoint.Ticks); + time = TimeSpan.FromTicks((long)(time.Ticks % iterationEndpoint.Ticks)); if (!_isLooping) { diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index b68f2fc79a..b79e2d9342 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -52,52 +52,72 @@ namespace Avalonia.Animation /// (i.e., the normalized time between the selected keyframes, relative to the /// time parameter). /// - /// The time parameter, relative to the total animation time - protected (double IntraKFTime, KeyFramePair KFPair) GetKFPairAndIntraKFTime(double t) + /// The time parameter, relative to the total animation time + protected (double IntraKFTime, KeyFramePair KFPair) GetKFPairAndIntraKFTime(double animationTime) { - AnimatorKeyFrame firstCue, lastCue ; + AnimatorKeyFrame firstKeyframe, lastKeyframe ; int kvCount = _convertedKeyframes.Count; if (kvCount > 2) { - if (t <= 0.0) + if (animationTime <= 0.0) { - firstCue = _convertedKeyframes[0]; - lastCue = _convertedKeyframes[1]; + firstKeyframe = _convertedKeyframes[0]; + lastKeyframe = _convertedKeyframes[1]; } - else if (t >= 1.0) + else if (animationTime >= 1.0) { - firstCue = _convertedKeyframes[_convertedKeyframes.Count - 2]; - lastCue = _convertedKeyframes[_convertedKeyframes.Count - 1]; + firstKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 2]; + lastKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 1]; } else { - (double time, int index) maxval = (0.0d, 0); - for (int i = 0; i < _convertedKeyframes.Count; i++) - { - var comp = _convertedKeyframes[i].Cue.CueValue; - if (t >= comp) - { - maxval = (comp, i); - } - } - firstCue = _convertedKeyframes[maxval.index]; - lastCue = _convertedKeyframes[maxval.index + 1]; + int index = FindClosestBeforeKeyFrame(animationTime); + firstKeyframe = _convertedKeyframes[index]; + lastKeyframe = _convertedKeyframes[index + 1]; } } else { - firstCue = _convertedKeyframes[0]; - lastCue = _convertedKeyframes[1]; + firstKeyframe = _convertedKeyframes[0]; + lastKeyframe = _convertedKeyframes[1]; } - double t0 = firstCue.Cue.CueValue; - double t1 = lastCue.Cue.CueValue; - var intraframeTime = (t - t0) / (t1 - t0); - var firstFrameData = (firstCue.GetTypedValue(), firstCue.isNeutral); - var lastFrameData = (lastCue.GetTypedValue(), lastCue.isNeutral); + double t0 = firstKeyframe.Cue.CueValue; + double t1 = lastKeyframe.Cue.CueValue; + var intraframeTime = (animationTime - t0) / (t1 - t0); + var firstFrameData = (firstKeyframe.GetTypedValue(), firstKeyframe.isNeutral); + var lastFrameData = (lastKeyframe.GetTypedValue(), lastKeyframe.isNeutral); return (intraframeTime, new KeyFramePair(firstFrameData, lastFrameData)); } + private int FindClosestBeforeKeyFrame(double time) + { + int FindClosestBeforeKeyFrame(int startIndex, int length) + { + if (length == 0 || length == 1) + { + return startIndex; + } + + int middle = startIndex + (length / 2); + + if (_convertedKeyframes[middle].Cue.CueValue < time) + { + return FindClosestBeforeKeyFrame(middle, length - middle); + } + else if (_convertedKeyframes[middle].Cue.CueValue > time) + { + return FindClosestBeforeKeyFrame(startIndex, middle - startIndex); + } + else + { + return middle; + } + } + + return FindClosestBeforeKeyFrame(0, _convertedKeyframes.Count); + } + /// /// Runs the KeyFrames Animation. /// @@ -130,14 +150,6 @@ namespace Avalonia.Animation AddNeutralKeyFramesIfNeeded(); - var copy = _convertedKeyframes.ToList().OrderBy(p => p.Cue.CueValue); - _convertedKeyframes.Clear(); - - foreach (AnimatorKeyFrame keyframe in copy) - { - _convertedKeyframes.Add(keyframe); - } - _isVerifiedAndConverted = true; } @@ -167,7 +179,7 @@ namespace Avalonia.Animation { if (!hasStartKey) { - _convertedKeyframes.Add(new AnimatorKeyFrame(null, new Cue(0.0d)) { Value = default(T), isNeutral = true }); + _convertedKeyframes.Insert(0, new AnimatorKeyFrame(null, new Cue(0.0d)) { Value = default(T), isNeutral = true }); } if (!hasEndKey) diff --git a/src/Avalonia.Animation/ClockBase.cs b/src/Avalonia.Animation/ClockBase.cs index ea784269d9..a2b29e728e 100644 --- a/src/Avalonia.Animation/ClockBase.cs +++ b/src/Avalonia.Animation/ClockBase.cs @@ -21,9 +21,7 @@ namespace Avalonia.Animation _connectedObservable = _observable.Publish().RefCount(); } - public bool HasSubscriptions => _observable.HasSubscriptions; - - public TimeSpan CurrentTime { get; private set; } + protected bool HasSubscriptions => _observable.HasSubscriptions; public PlayState PlayState { get; set; } @@ -47,7 +45,6 @@ namespace Avalonia.Animation } _observable.Pulse(_internalTime); - CurrentTime = _internalTime; if (PlayState == PlayState.Stop) { diff --git a/src/Avalonia.Animation/IClock.cs b/src/Avalonia.Animation/IClock.cs index 58c997841d..ae44102077 100644 --- a/src/Avalonia.Animation/IClock.cs +++ b/src/Avalonia.Animation/IClock.cs @@ -6,8 +6,6 @@ namespace Avalonia.Animation { public interface IClock : IObservable { - bool HasSubscriptions { get; } - TimeSpan CurrentTime { get; } PlayState PlayState { get; set; } } } diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index ad87ad7010..eff2c4e9f3 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -15,23 +15,22 @@ namespace Avalonia.Animation /// internal class TransitionInstance : SingleSubscriberObservableBase { - private IDisposable timerSubscription; - private TimeSpan startTime; - private TimeSpan duration; - private readonly IClock _clock; + private IDisposable _timerSubscription; + private TimeSpan _duration; + private readonly IClock _baseClock; + private IClock _clock; - public TransitionInstance(Clock clock, TimeSpan Duration) + public TransitionInstance(IClock clock, TimeSpan Duration) { - duration = Duration; - _clock = clock; + _duration = Duration; + _baseClock = clock; } private void TimerTick(TimeSpan t) { - var interpVal = (double)(t.Ticks - startTime.Ticks) / duration.Ticks; + var interpVal = (double)t.Ticks / _duration.Ticks; - if (interpVal > 1d - || interpVal < 0d) + if (interpVal > 1d || interpVal < 0d) { PublishCompleted(); return; @@ -42,13 +41,14 @@ namespace Avalonia.Animation protected override void Unsubscribed() { - timerSubscription?.Dispose(); + _timerSubscription?.Dispose(); + _clock.PlayState = PlayState.Stop; } protected override void Subscribed() { - startTime = _clock.CurrentTime; - timerSubscription = _clock.Subscribe(TimerTick); + _clock = new Clock(_baseClock); + _timerSubscription = _clock.Subscribe(TimerTick); PublishNext(0.0d); } } diff --git a/src/Avalonia.Animation/Transition`1.cs b/src/Avalonia.Animation/Transition`1.cs index b54ec8f51c..cd0d5d9ce9 100644 --- a/src/Avalonia.Animation/Transition`1.cs +++ b/src/Avalonia.Animation/Transition`1.cs @@ -14,7 +14,6 @@ namespace Avalonia.Animation public abstract class Transition : AvaloniaObject, ITransition { private AvaloniaProperty _prop; - private Easing _easing; /// /// Gets the duration of the animation. @@ -54,7 +53,5 @@ namespace Avalonia.Animation var transition = DoTransition(new TransitionInstance(clock, Duration), (T)oldValue, (T)newValue); return control.Bind((AvaloniaProperty)Property, transition, Data.BindingPriority.Animation); } - - } } From 3f5ec49b4a32762da011346f0e989cc8970c9524 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 7 Sep 2018 16:36:41 -0500 Subject: [PATCH 26/44] Update iOS and Android projects to use RenderTimers. --- src/Android/Avalonia.Android/AndroidPlatform.cs | 3 ++- ...isplayLinkRenderLoop.cs => DisplayLinkRenderTimer.cs} | 9 +++++---- src/iOS/Avalonia.iOS/iOSPlatform.cs | 3 ++- 3 files changed, 9 insertions(+), 6 deletions(-) rename src/iOS/Avalonia.iOS/{DisplayLinkRenderLoop.cs => DisplayLinkRenderTimer.cs} (72%) diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 2b46bfa492..5f0edadf63 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -52,7 +52,8 @@ namespace Avalonia.Android .Bind().ToTransient() .Bind().ToConstant(Instance) .Bind().ToSingleton() - .Bind().ToConstant(new DefaultRenderLoop(60)) + .Bind().ToConstant(new DefaultRenderTimer(60)) + .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new AssetLoader(app.GetType().Assembly)); SkiaPlatform.Initialize(); diff --git a/src/iOS/Avalonia.iOS/DisplayLinkRenderLoop.cs b/src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs similarity index 72% rename from src/iOS/Avalonia.iOS/DisplayLinkRenderLoop.cs rename to src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs index 4f275dd8ea..1357a4f642 100644 --- a/src/iOS/Avalonia.iOS/DisplayLinkRenderLoop.cs +++ b/src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs @@ -5,11 +5,12 @@ using Foundation; namespace Avalonia.iOS { - class DisplayLinkRenderLoop : IRenderLoop + class DisplayLinkRenderTimer : IRenderTimer { - public event EventHandler Tick; + public event Action Tick; private CADisplayLink _link; - public DisplayLinkRenderLoop() + + public DisplayLinkRenderTimer() { _link = CADisplayLink.Create(OnFrame); @@ -20,7 +21,7 @@ namespace Avalonia.iOS { try { - Tick?.Invoke(this, new EventArgs()); + Tick?.Invoke(Environment.TickCount); } catch (Exception) { diff --git a/src/iOS/Avalonia.iOS/iOSPlatform.cs b/src/iOS/Avalonia.iOS/iOSPlatform.cs index abaebca489..7b50040bf2 100644 --- a/src/iOS/Avalonia.iOS/iOSPlatform.cs +++ b/src/iOS/Avalonia.iOS/iOSPlatform.cs @@ -41,7 +41,8 @@ namespace Avalonia.iOS .Bind().ToConstant(PlatformThreadingInterface.Instance) .Bind().ToSingleton() .Bind().ToSingleton() - .Bind().ToSingleton(); + .Bind().ToSingleton() + .Bind().ToSingleton(); } } } From 8357bc86b02980a13a597d52cf72c7e5798b6755 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 7 Sep 2018 18:10:03 -0500 Subject: [PATCH 27/44] Add tests for the new RenderLoop logic. --- src/Avalonia.Visuals/Rendering/RenderLoop.cs | 16 +-- .../Rendering/DeferredRendererTests.cs | 64 ++++------ .../Rendering/RenderLoopTests.cs | 119 ++++++++++++++++++ 3 files changed, 146 insertions(+), 53 deletions(-) create mode 100644 tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs diff --git a/src/Avalonia.Visuals/Rendering/RenderLoop.cs b/src/Avalonia.Visuals/Rendering/RenderLoop.cs index a850b99c5e..d920be2706 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLoop.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLoop.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using Avalonia.Logging; using Avalonia.Threading; @@ -89,18 +90,7 @@ namespace Avalonia.Rendering { try { - var needsUpdate = false; - - foreach (var i in _items) - { - if (i.NeedsUpdate) - { - needsUpdate = true; - break; - } - } - - if (needsUpdate) + if (_items.Any(item => item.NeedsUpdate)) { await _dispatcher.InvokeAsync(() => { @@ -108,7 +98,7 @@ namespace Avalonia.Rendering { i.Update(tickCount); } - }).ConfigureAwait(false); + }, DispatcherPriority.Render).ConfigureAwait(false); } foreach (var i in _items) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index 1af9a9499d..e2a5c0c54c 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Subjects; - +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Media; @@ -22,27 +22,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering { public class DeferredRendererTests { - [Fact] - public void First_Frame_Calls_UpdateScene_On_Dispatcher() - { - var root = new TestRoot(); - - var dispatcher = new Mock(); - dispatcher.Setup(x => x.Post(It.IsAny(), DispatcherPriority.Render)) - .Callback((a, p) => a()); - - CreateTargetAndRunFrame(root, dispatcher: dispatcher.Object); - - dispatcher.Verify(x => - x.Post( - It.Is(a => a.Method.Name == "UpdateScene"), - DispatcherPriority.Render)); - } - [Fact] public void First_Frame_Calls_SceneBuilder_UpdateAll() { - var loop = new Mock(); var root = new TestRoot(); var sceneBuilder = MockSceneBuilder(root); @@ -54,6 +36,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering [Fact] public void Frame_Does_Not_Call_SceneBuilder_If_No_Dirty_Controls() { + var dispatcher = new ImmediateDispatcher(); var loop = new Mock(); var root = new TestRoot(); var sceneBuilder = MockSceneBuilder(root); @@ -63,8 +46,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering sceneBuilder: sceneBuilder.Object); target.Start(); - IgnoreFirstFrame(loop, sceneBuilder); - RunFrame(loop); + IgnoreFirstFrame(target, sceneBuilder); + RunFrame(target); sceneBuilder.Verify(x => x.UpdateAll(It.IsAny()), Times.Never); sceneBuilder.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.Never); @@ -73,8 +56,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering [Fact] public void Should_Update_Dirty_Controls_In_Order() { - var loop = new Mock(); var dispatcher = new ImmediateDispatcher(); + var loop = new Mock(); Border border; Decorator decorator; @@ -98,7 +81,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering dispatcher: dispatcher); target.Start(); - IgnoreFirstFrame(loop, sceneBuilder); + IgnoreFirstFrame(target, sceneBuilder); target.AddDirty(border); target.AddDirty(canvas); target.AddDirty(root); @@ -108,7 +91,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering sceneBuilder.Setup(x => x.Update(It.IsAny(), It.IsAny())) .Callback((_, v) => result.Add(v)); - RunFrame(loop); + RunFrame(target); Assert.Equal(new List { root, decorator, border, canvas }, result); } @@ -198,7 +181,6 @@ namespace Avalonia.Visuals.UnitTests.Rendering [Fact] public void Should_Create_Layer_For_Root() { - var loop = new Mock(); var root = new TestRoot(); var rootLayer = new Mock(); @@ -239,19 +221,19 @@ namespace Avalonia.Visuals.UnitTests.Rendering root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); - var loop = new Mock(); - var target = CreateTargetAndRunFrame(root, loop: loop); + var timer = new Mock(); + var target = CreateTargetAndRunFrame(root, timer); Assert.Equal(new[] { root }, target.Layers.Select(x => x.LayerRoot)); var animation = new BehaviorSubject(0.5); border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); - RunFrame(loop); + RunFrame(target); Assert.Equal(new IVisual[] { root, border }, target.Layers.Select(x => x.LayerRoot)); animation.OnCompleted(); - RunFrame(loop); + RunFrame(target); Assert.Equal(new[] { root }, target.Layers.Select(x => x.LayerRoot)); } @@ -280,8 +262,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); - var loop = new Mock(); - var target = CreateTargetAndRunFrame(root, loop: loop); + var timer = new Mock(); + var target = CreateTargetAndRunFrame(root, timer); Assert.Single(target.Layers); } @@ -345,19 +327,20 @@ namespace Avalonia.Visuals.UnitTests.Rendering private DeferredRenderer CreateTargetAndRunFrame( TestRoot root, - Mock loop = null, + Mock timer = null, ISceneBuilder sceneBuilder = null, IDispatcher dispatcher = null) { - loop = loop ?? new Mock(); + timer = timer ?? new Mock(); + dispatcher = dispatcher ?? new ImmediateDispatcher(); var target = new DeferredRenderer( root, - loop.Object, + new RenderLoop(timer.Object, dispatcher), sceneBuilder: sceneBuilder, - dispatcher: dispatcher ?? new ImmediateDispatcher()); + dispatcher: dispatcher); root.Renderer = target; target.Start(); - RunFrame(loop); + RunFrame(target); return target; } @@ -366,15 +349,16 @@ namespace Avalonia.Visuals.UnitTests.Rendering return Mock.Get(renderer.Layers[layerRoot].Bitmap.Item.CreateDrawingContext(null)); } - private void IgnoreFirstFrame(Mock loop, Mock sceneBuilder) + private void IgnoreFirstFrame(IRenderLoopTask task, Mock sceneBuilder) { - RunFrame(loop); + RunFrame(task); sceneBuilder.ResetCalls(); } - private void RunFrame(Mock loop) + private void RunFrame(IRenderLoopTask task) { - //loop.Raise(x => x.Tick += null, EventArgs.Empty); + task.Update(0); + task.Render(); } private IRenderTargetBitmapImpl CreateLayer() diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs new file mode 100644 index 0000000000..30ef35a2bb --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Rendering; +using Avalonia.Threading; +using Moq; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Rendering +{ + public class RenderLoopTests + { + [Fact] + public void RenderLoop_Update_Runs_On_Dispatcher() + { + var dispatcher = new Mock(); + + bool inDispatcher = false; + + dispatcher.Setup( + d => d.InvokeAsync(It.IsAny(), DispatcherPriority.Render)) + .Callback((Action a, DispatcherPriority _) => + { + inDispatcher = true; + a(); + inDispatcher = false; + }) + .Returns(Task.CompletedTask); + + var timer = new Mock(); + + var loop = new RenderLoop(timer.Object, dispatcher.Object); + + var renderTask = new Mock(); + + renderTask.Setup(t => t.NeedsUpdate).Returns(true); + renderTask.Setup(t => t.Update(It.IsAny())) + .Callback((long _) => Assert.True(inDispatcher)); + + loop.Add(renderTask.Object); + + timer.Raise(t => t.Tick += null, 0L); + + renderTask.Verify(t => t.Update(It.IsAny()), Times.Once()); + } + + [Fact] + public void RenderLoop_Does_Not_Update_When_No_Tasks_Need_Update() + { + var dispatcher = new Mock(); + dispatcher.Setup( + d => d.InvokeAsync(It.IsAny(), DispatcherPriority.Render)) + .Callback((Action a, DispatcherPriority _) => a()) + .Returns(Task.CompletedTask); + + var timer = new Mock(); + var loop = new RenderLoop(timer.Object, dispatcher.Object); + var renderTask = new Mock(); + renderTask.Setup(t => t.NeedsUpdate).Returns(false); + + loop.Add(renderTask.Object); + timer.Raise(t => t.Tick += null, 0L); + + renderTask.Verify(t => t.Update(It.IsAny()), Times.Never()); + } + + [Fact] + public void RenderLoop_Render_Runs_Off_Dispatcher() + { + var dispatcher = new Mock(); + bool inDispatcher = false; + dispatcher.Setup( + d => d.InvokeAsync(It.IsAny(), DispatcherPriority.Render)) + .Callback((Action a, DispatcherPriority _) => + { + inDispatcher = true; + a(); + inDispatcher = false; + }) + .Returns(Task.CompletedTask); + + var timer = new Mock(); + var loop = new RenderLoop(timer.Object, dispatcher.Object); + + var renderTask = new Mock(); + + renderTask.Setup(t => t.NeedsUpdate).Returns(true); + renderTask.Setup(t => t.Render()) + .Callback(() => Assert.False(inDispatcher)); + + loop.Add(renderTask.Object); + timer.Raise(t => t.Tick += null, 0L); + + renderTask.Verify(t => t.Update(It.IsAny()), Times.Once()); + } + + [Fact] + public void RenderLoop_Passes_Tick_Count_To_Update() + { + var dispatcher = new Mock(); + dispatcher.Setup( + d => d.InvokeAsync(It.IsAny(), DispatcherPriority.Render)) + .Callback((Action a, DispatcherPriority _) => a()) + .Returns(Task.CompletedTask); + + var timer = new Mock(); + var loop = new RenderLoop(timer.Object, dispatcher.Object); + var renderTask = new Mock(); + renderTask.Setup(t => t.NeedsUpdate).Returns(true); + + loop.Add(renderTask.Object); + var tickCount = 12345L; + timer.Raise(t => t.Tick += null, tickCount); + + renderTask.Verify(t => t.Update(tickCount), Times.Once()); + } + } +} From ce95625d66509c15bb9f20607596eff900eef3a7 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 7 Sep 2018 18:45:06 -0500 Subject: [PATCH 28/44] Only add the clock to the render loop if there is a render loop. --- src/Avalonia.Controls/Application.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 8c03bac61a..586a73b75c 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -341,7 +341,7 @@ namespace Avalonia var clock = new RenderLoopClock(); AvaloniaLocator.CurrentMutable .Bind().ToConstant(clock) - .GetService().Add(clock); + .GetService()?.Add(clock); } } } From ec695433dec3a6b66967f692b66538e95366a842 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 10 Sep 2018 01:02:35 +0800 Subject: [PATCH 29/44] Cancel an animation instance when the selector turns false. --- src/Avalonia.Animation/Animator`1.cs | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index f0ef55aa9e..74f09d5488 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -37,12 +37,29 @@ namespace Avalonia.Animation if (!_isVerifiedAndConverted) VerifyConvertKeyFrames(); - return match + var matchStream = match + .DistinctUntilChanged() + .Publish() + .RefCount(); + + var activeInstance = matchStream .Where(p => p) - .Subscribe(_ => - { - var timerObs = RunKeyFrames(animation, control, onComplete); - }); + .Select(p => RunKeyFrames(animation, control, onComplete)); + + var negationStream = matchStream + .Where(p => !p); + + return Observable + .WithLatestFrom( + negationStream, + activeInstance, + (isMatch, instance) => + { + if (!isMatch && animation.RepeatCount.IsLoop) + instance?.Dispose(); + return true; + }) + .Subscribe(); } /// From 64a4a6d82af01a3ba2c69182b2fcd16c33ddc153 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Tue, 11 Sep 2018 15:04:12 +0800 Subject: [PATCH 30/44] Simplify Fix; Invalidate when IsIndeterminate property changes. --- src/Avalonia.Animation/Animator`1.cs | 39 ++++++++++------------------ src/Avalonia.Controls/ProgressBar.cs | 8 +++++- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 74f09d5488..ab82bfb35d 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -17,7 +17,7 @@ namespace Avalonia.Animation /// List of type-converted keyframes. /// private readonly List _convertedKeyframes = new List(); - + private bool _isVerifiedAndConverted; /// @@ -28,38 +28,25 @@ namespace Avalonia.Animation public Animator() { // Invalidate keyframes when changed. - this.CollectionChanged += delegate { _isVerifiedAndConverted = false; }; + this.CollectionChanged += delegate { _isVerifiedAndConverted = false; }; } /// public virtual IDisposable Apply(Animation animation, Animatable control, IObservable match, Action onComplete) { - if (!_isVerifiedAndConverted) + if (!_isVerifiedAndConverted) VerifyConvertKeyFrames(); - var matchStream = match - .DistinctUntilChanged() - .Publish() - .RefCount(); - - var activeInstance = matchStream - .Where(p => p) - .Select(p => RunKeyFrames(animation, control, onComplete)); - - var negationStream = matchStream - .Where(p => !p); - - return Observable - .WithLatestFrom( - negationStream, - activeInstance, - (isMatch, instance) => + return match + .DistinctUntilChanged() + .Select(x => x ? RunKeyFrames(animation, control, onComplete) : null) + .Buffer(2, 1) + .Where(x => x.Count > 1) + .Subscribe(x => { - if (!isMatch && animation.RepeatCount.IsLoop) - instance?.Dispose(); - return true; - }) - .Subscribe(); + if (animation.RepeatCount.IsLoop) + x[0]?.Dispose(); + }); } /// @@ -72,7 +59,7 @@ namespace Avalonia.Animation /// The time parameter, relative to the total animation time protected (double IntraKFTime, KeyFramePair KFPair) GetKFPairAndIntraKFTime(double t) { - AnimatorKeyFrame firstCue, lastCue ; + AnimatorKeyFrame firstCue, lastCue; int kvCount = _convertedKeyframes.Count; if (kvCount > 2) { diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 7f4b549849..fe7b8e64c7 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -38,6 +38,7 @@ namespace Avalonia.Controls PseudoClass(IsIndeterminateProperty, ":indeterminate"); ValueProperty.Changed.AddClassHandler(x => x.ValueChanged); + IsIndeterminateProperty.Changed.AddClassHandler(x => x.IsIndeterminateChanged); } public bool IsIndeterminate @@ -118,5 +119,10 @@ namespace Avalonia.Controls { UpdateIndicator(Bounds.Size); } + + private void IsIndeterminateChanged(AvaloniaPropertyChangedEventArgs e) + { + UpdateIndicator(Bounds.Size); + } } -} +} \ No newline at end of file From 3cbcd0ac0fab750a413b0e9aaeba88d092faa238 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Wed, 12 Sep 2018 10:42:44 +0800 Subject: [PATCH 31/44] Match CSS's behavior on selectors & animations. --- src/Avalonia.Animation/Animator`1.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index ab82bfb35d..e4af0f356d 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -37,16 +37,11 @@ namespace Avalonia.Animation if (!_isVerifiedAndConverted) VerifyConvertKeyFrames(); - return match - .DistinctUntilChanged() - .Select(x => x ? RunKeyFrames(animation, control, onComplete) : null) - .Buffer(2, 1) - .Where(x => x.Count > 1) - .Subscribe(x => - { - if (animation.RepeatCount.IsLoop) - x[0]?.Dispose(); - }); + return match.DistinctUntilChanged() + .Select(x => x ? RunKeyFrames(animation, control, onComplete) : null) + .Buffer(2, 1) + .Where(x => x.Count > 1) + .Subscribe(x => x[0]?.Dispose()); } /// From 5fce271ff8bfff57987199d0fde5839c4788d650 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 11 Sep 2018 21:43:05 -0500 Subject: [PATCH 32/44] PR Feedback --- src/Avalonia.Animation/IAnimation.cs | 4 ++-- src/Avalonia.Animation/IAnimator.cs | 2 +- src/Avalonia.Animation/IGlobalClock.cs | 10 ++++++++++ src/Avalonia.Controls/Application.cs | 2 +- .../InternalPlatformThreadingInterface.cs | 9 ++++++--- src/Avalonia.Controls/TopLevel.cs | 1 - src/Avalonia.Visuals/Animation/RenderLoopClock.cs | 6 +++--- .../Rendering/DefaultRenderTimer.cs | 12 +++++++----- .../Rendering/DeferredRenderer.cs | 2 +- src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs | 15 +-------------- src/Avalonia.Visuals/Rendering/IRenderTimer.cs | 4 ++-- src/Avalonia.Visuals/Rendering/RenderLoop.cs | 4 ++-- .../Rendering/RenderLoopTests.cs | 14 +++++++------- 13 files changed, 43 insertions(+), 42 deletions(-) create mode 100644 src/Avalonia.Animation/IGlobalClock.cs diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index 34b0a5d769..ff85535d8a 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -9,12 +9,12 @@ namespace Avalonia.Animation public interface IAnimation { /// - /// Apply the animation to the specified control + /// Apply the animation to the specified control and run it when produces true. /// IDisposable Apply(Animatable control, IClock clock, IObservable match, Action onComplete = null); /// - /// Run the animation to the specified control + /// Run the animation on the specified control. /// Task RunAsync(Animatable control, IClock clock); } diff --git a/src/Avalonia.Animation/IAnimator.cs b/src/Avalonia.Animation/IAnimator.cs index 04bad8e112..d0fb173c54 100644 --- a/src/Avalonia.Animation/IAnimator.cs +++ b/src/Avalonia.Animation/IAnimator.cs @@ -16,6 +16,6 @@ namespace Avalonia.Animation /// /// Applies the current KeyFrame group to the specified control. /// - IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable obsMatch, Action onComplete); + IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete); } } diff --git a/src/Avalonia.Animation/IGlobalClock.cs b/src/Avalonia.Animation/IGlobalClock.cs new file mode 100644 index 0000000000..b0455e2c80 --- /dev/null +++ b/src/Avalonia.Animation/IGlobalClock.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Animation +{ + public interface IGlobalClock : IClock + { + } +} diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 586a73b75c..37796ff9ba 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -340,7 +340,7 @@ namespace Avalonia var clock = new RenderLoopClock(); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(clock) + .Bind().ToConstant(clock) .GetService()?.Add(clock); } } diff --git a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs index 501e15653a..bb357453ff 100644 --- a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs +++ b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs @@ -14,7 +14,10 @@ namespace Avalonia.Controls.Platform public InternalPlatformThreadingInterface() { TlsCurrentThreadIsLoopThread = true; - StartTimer(DispatcherPriority.Render, new TimeSpan(0, 0, 0, 0, 66), () => Tick?.Invoke(Environment.TickCount)); + StartTimer( + DispatcherPriority.Render, + new TimeSpan(0, 0, 0, 0, 66), + () => Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount))); } private readonly AutoResetEvent _signaled = new AutoResetEvent(false); @@ -105,7 +108,7 @@ namespace Avalonia.Controls.Platform public bool CurrentThreadIsLoopThread => TlsCurrentThreadIsLoopThread; public event Action Signaled; - public event Action Tick; + public event Action Tick; } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index fb5b932fd8..630753396f 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -96,7 +96,6 @@ namespace Avalonia.Controls _applicationLifecycle = TryGetService(dependencyResolver); _renderInterface = TryGetService(dependencyResolver); - var renderLoop = TryGetService(dependencyResolver); Renderer = impl.CreateRenderer(this); impl.SetInputRoot(this); diff --git a/src/Avalonia.Visuals/Animation/RenderLoopClock.cs b/src/Avalonia.Visuals/Animation/RenderLoopClock.cs index e59b3aac0d..504caef461 100644 --- a/src/Avalonia.Visuals/Animation/RenderLoopClock.cs +++ b/src/Avalonia.Visuals/Animation/RenderLoopClock.cs @@ -5,7 +5,7 @@ using Avalonia.Rendering; namespace Avalonia.Animation { - public class RenderLoopClock : ClockBase, IRenderLoopTask + public class RenderLoopClock : ClockBase, IRenderLoopTask, IGlobalClock { protected override void Stop() { @@ -18,9 +18,9 @@ namespace Avalonia.Animation { } - void IRenderLoopTask.Update(long tickCount) + void IRenderLoopTask.Update(TimeSpan time) { - Pulse(TimeSpan.FromMilliseconds(tickCount)); + Pulse(time); } } } diff --git a/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs b/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs index a83334ff5e..d0eb181c65 100644 --- a/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs +++ b/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering { private IRuntimePlatform _runtime; private int _subscriberCount; - private Action _tick; + private Action _tick; private IDisposable _subscription; /// @@ -39,7 +39,7 @@ namespace Avalonia.Rendering public int FramesPerSecond { get; } /// - public event Action Tick + public event Action Tick { add { @@ -78,14 +78,16 @@ namespace Avalonia.Rendering /// This can be overridden by platform implementations to use a specialized timer /// implementation. /// - protected virtual IDisposable StartCore(Action tick) + protected virtual IDisposable StartCore(Action tick) { if (_runtime == null) { _runtime = AvaloniaLocator.Current.GetService(); } - return _runtime.StartSystemTimer(TimeSpan.FromSeconds(1.0 / FramesPerSecond), () => tick(Environment.TickCount)); + return _runtime.StartSystemTimer( + TimeSpan.FromSeconds(1.0 / FramesPerSecond), + () => tick(TimeSpan.FromMilliseconds(Environment.TickCount))); } /// @@ -97,7 +99,7 @@ namespace Avalonia.Rendering _subscription = null; } - private void InternalTick(long tickCount) + private void InternalTick(TimeSpan tickCount) { _tick(tickCount); } diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 3221dd85c6..fc67b5c461 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -166,7 +166,7 @@ namespace Avalonia.Rendering bool IRenderLoopTask.NeedsUpdate => _dirty == null || _dirty.Count > 0; - void IRenderLoopTask.Update(long tickCount) => UpdateScene(); + void IRenderLoopTask.Update(TimeSpan time) => UpdateScene(); void IRenderLoopTask.Render() { diff --git a/src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs b/src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs index b031bf00df..15f0afc797 100644 --- a/src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs +++ b/src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs @@ -6,20 +6,7 @@ namespace Avalonia.Rendering public interface IRenderLoopTask { bool NeedsUpdate { get; } - void Update(long tickCount); + void Update(TimeSpan time); void Render(); } - - public class MockRenderLoopTask : IRenderLoopTask - { - public bool NeedsUpdate => true; - - public void Render() - { - } - - public void Update(long tickCount) - { - } - } } diff --git a/src/Avalonia.Visuals/Rendering/IRenderTimer.cs b/src/Avalonia.Visuals/Rendering/IRenderTimer.cs index 78f6183994..d333e928a0 100644 --- a/src/Avalonia.Visuals/Rendering/IRenderTimer.cs +++ b/src/Avalonia.Visuals/Rendering/IRenderTimer.cs @@ -15,6 +15,6 @@ namespace Avalonia.Rendering /// This event can be raised on any thread; it is the responsibility of the subscriber to /// switch execution to the right thread. /// - event Action Tick; + event Action Tick; } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Rendering/RenderLoop.cs b/src/Avalonia.Visuals/Rendering/RenderLoop.cs index d920be2706..d0d5b2250d 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLoop.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLoop.cs @@ -84,7 +84,7 @@ namespace Avalonia.Rendering } } - private async void TimerTick(long tickCount) + private async void TimerTick(TimeSpan time) { if (Interlocked.CompareExchange(ref inTick, 1, 0) == 0) { @@ -96,7 +96,7 @@ namespace Avalonia.Rendering { foreach (var i in _items) { - i.Update(tickCount); + i.Update(time); } }, DispatcherPriority.Render).ConfigureAwait(false); } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs index 30ef35a2bb..bf992f4027 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs @@ -35,14 +35,14 @@ namespace Avalonia.Visuals.UnitTests.Rendering var renderTask = new Mock(); renderTask.Setup(t => t.NeedsUpdate).Returns(true); - renderTask.Setup(t => t.Update(It.IsAny())) + renderTask.Setup(t => t.Update(It.IsAny())) .Callback((long _) => Assert.True(inDispatcher)); loop.Add(renderTask.Object); timer.Raise(t => t.Tick += null, 0L); - renderTask.Verify(t => t.Update(It.IsAny()), Times.Once()); + renderTask.Verify(t => t.Update(It.IsAny()), Times.Once()); } [Fact] @@ -62,7 +62,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering loop.Add(renderTask.Object); timer.Raise(t => t.Tick += null, 0L); - renderTask.Verify(t => t.Update(It.IsAny()), Times.Never()); + renderTask.Verify(t => t.Update(It.IsAny()), Times.Never()); } [Fact] @@ -92,7 +92,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering loop.Add(renderTask.Object); timer.Raise(t => t.Tick += null, 0L); - renderTask.Verify(t => t.Update(It.IsAny()), Times.Once()); + renderTask.Verify(t => t.Update(It.IsAny()), Times.Once()); } [Fact] @@ -110,10 +110,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering renderTask.Setup(t => t.NeedsUpdate).Returns(true); loop.Add(renderTask.Object); - var tickCount = 12345L; - timer.Raise(t => t.Tick += null, tickCount); + var time = new TimeSpan(123456789L); + timer.Raise(t => t.Tick += null, time); - renderTask.Verify(t => t.Update(tickCount), Times.Once()); + renderTask.Verify(t => t.Update(time), Times.Once()); } } } From 0aa5866ea862ae9a4836dc1899361fd90f7b0739 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 11 Sep 2018 22:40:36 -0500 Subject: [PATCH 33/44] Fix missed changes in IRenderTimer. --- src/OSX/Avalonia.MonoMac/RenderTimer.cs | 4 ++-- src/Windows/Avalonia.Win32/RenderTimer.cs | 4 ++-- src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs | 4 ++-- .../Rendering/DeferredRendererTests.cs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/OSX/Avalonia.MonoMac/RenderTimer.cs b/src/OSX/Avalonia.MonoMac/RenderTimer.cs index 22ad2e81a2..f3c49828d6 100644 --- a/src/OSX/Avalonia.MonoMac/RenderTimer.cs +++ b/src/OSX/Avalonia.MonoMac/RenderTimer.cs @@ -12,7 +12,7 @@ namespace Avalonia.MonoMac { } - protected override IDisposable StartCore(Action tick) + protected override IDisposable StartCore(Action tick) { return AvaloniaLocator.Current.GetService().StartSystemTimer( TimeSpan.FromSeconds(1.0 / FramesPerSecond), @@ -20,7 +20,7 @@ namespace Avalonia.MonoMac { using (new NSAutoreleasePool()) { - tick?.Invoke(Environment.TickCount); + tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount)); } }); } diff --git a/src/Windows/Avalonia.Win32/RenderTimer.cs b/src/Windows/Avalonia.Win32/RenderTimer.cs index c911bc3adf..7dbb745a23 100644 --- a/src/Windows/Avalonia.Win32/RenderTimer.cs +++ b/src/Windows/Avalonia.Win32/RenderTimer.cs @@ -29,12 +29,12 @@ namespace Avalonia.Win32 { } - protected override IDisposable StartCore(Action tick) + protected override IDisposable StartCore(Action tick) { EnsureTimerQueueCreated(); var msPerFrame = 1000 / FramesPerSecond; - timerDelegate = (_, __) => tick(Environment.TickCount); + timerDelegate = (_, __) => tick(TimeSpan.FromMilliseconds(Environment.TickCount)); UnmanagedMethods.CreateTimerQueueTimer( out var timer, diff --git a/src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs b/src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs index 1357a4f642..0cefba7f19 100644 --- a/src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs +++ b/src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs @@ -7,7 +7,7 @@ namespace Avalonia.iOS { class DisplayLinkRenderTimer : IRenderTimer { - public event Action Tick; + public event Action Tick; private CADisplayLink _link; public DisplayLinkRenderTimer() @@ -21,7 +21,7 @@ namespace Avalonia.iOS { try { - Tick?.Invoke(Environment.TickCount); + Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount)); } catch (Exception) { diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index e2a5c0c54c..8c103360d4 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -357,7 +357,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering private void RunFrame(IRenderLoopTask task) { - task.Update(0); + task.Update(TimeSpan.Zero); task.Render(); } From dcb0d5d090b50fa882e0f9d24300f4af2c5b57d3 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 12 Sep 2018 14:18:15 -0500 Subject: [PATCH 34/44] Fix Raise calls in RenderLoopTests --- .../Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs index bf992f4027..c8fc57285d 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs @@ -40,7 +40,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering loop.Add(renderTask.Object); - timer.Raise(t => t.Tick += null, 0L); + timer.Raise(t => t.Tick += null, TimeSpan.Zero); renderTask.Verify(t => t.Update(It.IsAny()), Times.Once()); } @@ -60,7 +60,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering renderTask.Setup(t => t.NeedsUpdate).Returns(false); loop.Add(renderTask.Object); - timer.Raise(t => t.Tick += null, 0L); + timer.Raise(t => t.Tick += null, TimeSpan.Zero); renderTask.Verify(t => t.Update(It.IsAny()), Times.Never()); } @@ -90,7 +90,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering .Callback(() => Assert.False(inDispatcher)); loop.Add(renderTask.Object); - timer.Raise(t => t.Tick += null, 0L); + timer.Raise(t => t.Tick += null, TimeSpan.Zero); renderTask.Verify(t => t.Update(It.IsAny()), Times.Once()); } From f20ebb3ca7dfb594ec8ccfca5cd531c3a0cb5fa9 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 12 Sep 2018 14:55:41 -0500 Subject: [PATCH 35/44] Fix another API mismatch from the API change of IRenderLoopTask that I missed beforehand. --- tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs index c8fc57285d..16c2d3ee18 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs @@ -36,7 +36,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering renderTask.Setup(t => t.NeedsUpdate).Returns(true); renderTask.Setup(t => t.Update(It.IsAny())) - .Callback((long _) => Assert.True(inDispatcher)); + .Callback((TimeSpan _) => Assert.True(inDispatcher)); loop.Add(renderTask.Object); From a8d4c8d799ee1abf5338028361b0855745ebae47 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 13 Sep 2018 14:24:13 +0800 Subject: [PATCH 36/44] Add a new Disposable Extention. --- src/Avalonia.Animation/Animator`1.cs | 11 +++-- .../Reactive/DisposeOnNextObservable.cs | 40 +++++++++++++++++++ src/Avalonia.Base/Reactive/ObservableEx.cs | 17 +++++++- 3 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 src/Avalonia.Base/Reactive/DisposeOnNextObservable.cs diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index e4af0f356d..888450e7f0 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Animation.Utils; using Avalonia.Collections; using Avalonia.Data; +using Avalonia.Reactive; + namespace Avalonia.Animation { @@ -38,10 +41,10 @@ namespace Avalonia.Animation VerifyConvertKeyFrames(); return match.DistinctUntilChanged() - .Select(x => x ? RunKeyFrames(animation, control, onComplete) : null) - .Buffer(2, 1) - .Where(x => x.Count > 1) - .Subscribe(x => x[0]?.Dispose()); + .ObserveOn(Avalonia.Threading.AvaloniaScheduler.Instance) + .Select(x => x ? RunKeyFrames(animation, control, onComplete) : Disposable.Empty) + .DisposeCurrentOnNext() + .Subscribe(); } /// diff --git a/src/Avalonia.Base/Reactive/DisposeOnNextObservable.cs b/src/Avalonia.Base/Reactive/DisposeOnNextObservable.cs new file mode 100644 index 0000000000..8650fe5400 --- /dev/null +++ b/src/Avalonia.Base/Reactive/DisposeOnNextObservable.cs @@ -0,0 +1,40 @@ +using System; +using Avalonia.Threading; + +namespace Avalonia.Reactive +{ + public class DisposeOnNextObservable : LightweightObservableBase, IObserver where T : IDisposable + { + private IDisposable lastValue; + + private void ValueNext(T value) + { + this.PublishNext(value); + lastValue?.Dispose(); + lastValue = value; + } + + public void OnCompleted() + { + this.PublishCompleted(); + } + + public void OnError(Exception error) + { + this.PublishError(error); + } + + void IObserver.OnNext(T value) + { + ValueNext(value); + } + + protected override void Initialize() + { + } + + protected override void Deinitialize() + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Reactive/ObservableEx.cs b/src/Avalonia.Base/Reactive/ObservableEx.cs index 5b2a39d5ff..dc3be36015 100644 --- a/src/Avalonia.Base/Reactive/ObservableEx.cs +++ b/src/Avalonia.Base/Reactive/ObservableEx.cs @@ -22,6 +22,20 @@ namespace Avalonia.Reactive return new SingleValueImpl(value); } + /// + /// Disposes the current and saves the next. + /// + /// The type of the value. + /// The source . + /// The observable. + public static IObservable DisposeCurrentOnNext(this IObservable observable) + where T : IDisposable + { + var subject = new DisposeOnNextObservable(); + observable.Subscribe(subject); + return subject; + } + private class SingleValueImpl : IObservable { private T _value; @@ -30,7 +44,6 @@ namespace Avalonia.Reactive { _value = value; } - public IDisposable Subscribe(IObserver observer) { observer.OnNext(_value); @@ -38,4 +51,4 @@ namespace Avalonia.Reactive } } } -} +} \ No newline at end of file From 74c8cedde9070956d283137e0407d1888572c72b Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 13 Sep 2018 15:17:32 +0800 Subject: [PATCH 37/44] Try fixing the sporadic Bindings Exceptions. --- src/Avalonia.Animation/Animator`1.cs | 7 ++----- src/Avalonia.Base/Reactive/DisposeOnNextObservable.cs | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 888450e7f0..decca8e858 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -1,14 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Animation.Utils; using Avalonia.Collections; using Avalonia.Data; using Avalonia.Reactive; - namespace Avalonia.Animation { /// @@ -41,8 +39,7 @@ namespace Avalonia.Animation VerifyConvertKeyFrames(); return match.DistinctUntilChanged() - .ObserveOn(Avalonia.Threading.AvaloniaScheduler.Instance) - .Select(x => x ? RunKeyFrames(animation, control, onComplete) : Disposable.Empty) + .Select(x => x ? RunKeyFrames(animation, control, onComplete) : null) .DisposeCurrentOnNext() .Subscribe(); } @@ -172,4 +169,4 @@ namespace Avalonia.Animation } } } -} +} \ No newline at end of file diff --git a/src/Avalonia.Base/Reactive/DisposeOnNextObservable.cs b/src/Avalonia.Base/Reactive/DisposeOnNextObservable.cs index 8650fe5400..18af9e8752 100644 --- a/src/Avalonia.Base/Reactive/DisposeOnNextObservable.cs +++ b/src/Avalonia.Base/Reactive/DisposeOnNextObservable.cs @@ -9,9 +9,9 @@ namespace Avalonia.Reactive private void ValueNext(T value) { - this.PublishNext(value); lastValue?.Dispose(); lastValue = value; + this.PublishNext(value); } public void OnCompleted() From f2f96e2f46a520be9f84febfeb79276b5da91586 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 13 Sep 2018 20:42:18 +0200 Subject: [PATCH 38/44] Don't dispose completed binding. --- src/Avalonia.Base/PriorityBindingEntry.cs | 7 +++++++ src/Avalonia.Base/PriorityLevel.cs | 14 +++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/PriorityBindingEntry.cs b/src/Avalonia.Base/PriorityBindingEntry.cs index 570bfe03dc..d4a47306a7 100644 --- a/src/Avalonia.Base/PriorityBindingEntry.cs +++ b/src/Avalonia.Base/PriorityBindingEntry.cs @@ -50,6 +50,11 @@ namespace Avalonia get; } + /// + /// Gets a value indicating whether the binding has completed. + /// + public bool HasCompleted { get; private set; } + /// /// The current value of the binding. /// @@ -129,6 +134,8 @@ namespace Avalonia private void Completed() { + HasCompleted = true; + if (Dispatcher.UIThread.CheckAccess()) { _owner.Completed(this); diff --git a/src/Avalonia.Base/PriorityLevel.cs b/src/Avalonia.Base/PriorityLevel.cs index 96661bd7ea..909558b0ce 100644 --- a/src/Avalonia.Base/PriorityLevel.cs +++ b/src/Avalonia.Base/PriorityLevel.cs @@ -112,12 +112,16 @@ namespace Avalonia return Disposable.Create(() => { - Bindings.Remove(node); - entry.Dispose(); - - if (entry.Index >= ActiveBindingIndex) + if (!entry.HasCompleted) { - ActivateFirstBinding(); + Bindings.Remove(node); + + entry.Dispose(); + + if (entry.Index >= ActiveBindingIndex) + { + ActivateFirstBinding(); + } } }); } From ee1a8ee30fb0f10e94335ce666af4fe19e59734d Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Fri, 14 Sep 2018 10:49:49 +0800 Subject: [PATCH 39/44] Make a specialized observable for instance lifetime handling. Delete DisposeOnNextObservable. --- src/Avalonia.Animation/AnimationInstance`1.cs | 7 +-- src/Avalonia.Animation/Animator`1.cs | 18 +++--- .../DisposeAnimationInstanceObservable.cs | 62 +++++++++++++++++++ .../Reactive/DisposeOnNextObservable.cs | 40 ------------ src/Avalonia.Base/Reactive/ObservableEx.cs | 16 +---- 5 files changed, 73 insertions(+), 70 deletions(-) create mode 100644 src/Avalonia.Animation/DisposeAnimationInstanceObservable.cs delete mode 100644 src/Avalonia.Base/Reactive/DisposeOnNextObservable.cs diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index 5a72904ed2..c264663b56 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -154,7 +154,7 @@ namespace Avalonia.Animation private void InternalStep(TimeSpan systemTime) { DoPlayStatesAndTime(systemTime); - + var time = _internalClock - _firstFrameCount; var delayEndpoint = _delay; var iterationEndpoint = delayEndpoint + _duration; @@ -188,10 +188,7 @@ namespace Avalonia.Animation if (!_isLooping) { - if (_currentIteration > _repeatCount) - DoComplete(); - - if (time > iterationEndpoint) + if ((_currentIteration > _repeatCount) | (time > iterationEndpoint)) DoComplete(); } diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index decca8e858..0de3991a88 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; @@ -38,10 +41,8 @@ namespace Avalonia.Animation if (!_isVerifiedAndConverted) VerifyConvertKeyFrames(); - return match.DistinctUntilChanged() - .Select(x => x ? RunKeyFrames(animation, control, onComplete) : null) - .DisposeCurrentOnNext() - .Subscribe(); + var subject = new DisposeAnimationInstanceObservable(this, animation, control, onComplete); + return match.Subscribe(subject); } /// @@ -96,11 +97,8 @@ namespace Avalonia.Animation var lastFrameData = (lastCue.GetTypedValue(), lastCue.isNeutral); return (intraframeTime, new KeyFramePair(firstFrameData, lastFrameData)); } - - /// - /// Runs the KeyFrames Animation. - /// - private IDisposable RunKeyFrames(Animation animation, Animatable control, Action onComplete) + + internal IDisposable Run(Animation animation, Animatable control, Action onComplete) { var instance = new AnimationInstance(animation, control, this, onComplete, DoInterpolation); return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); diff --git a/src/Avalonia.Animation/DisposeAnimationInstanceObservable.cs b/src/Avalonia.Animation/DisposeAnimationInstanceObservable.cs new file mode 100644 index 0000000000..902a09030b --- /dev/null +++ b/src/Avalonia.Animation/DisposeAnimationInstanceObservable.cs @@ -0,0 +1,62 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using Avalonia.Animation.Utils; +using Avalonia.Collections; +using Avalonia.Data; +using Avalonia.Reactive; + +namespace Avalonia.Animation +{ + /// + /// Manages the lifetime of animation instances as determined by its selector state. + /// + internal class DisposeAnimationInstanceObservable : IObserver, IDisposable + { + private IDisposable _lastInstance; + private bool _lastMatch; + private Animator _animator; + private Animation _animation; + private Animatable _control; + private Action _onComplete; + + public DisposeAnimationInstanceObservable(Animator animator, Animation animation, Animatable control, Action onComplete) + { + this._animator = animator; + this._animation = animation; + this._control = control; + this._onComplete = onComplete; + } + + public void Dispose() + { + _lastInstance?.Dispose(); + } + + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + _lastInstance?.Dispose(); + } + + void IObserver.OnNext(bool matchVal) + { + if (matchVal != _lastMatch) + { + _lastInstance?.Dispose(); + if (matchVal) + { + _lastInstance = _animator.RunAnimation(_animation, _control, _onComplete); + } + _lastMatch = matchVal; + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Reactive/DisposeOnNextObservable.cs b/src/Avalonia.Base/Reactive/DisposeOnNextObservable.cs deleted file mode 100644 index 18af9e8752..0000000000 --- a/src/Avalonia.Base/Reactive/DisposeOnNextObservable.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using Avalonia.Threading; - -namespace Avalonia.Reactive -{ - public class DisposeOnNextObservable : LightweightObservableBase, IObserver where T : IDisposable - { - private IDisposable lastValue; - - private void ValueNext(T value) - { - lastValue?.Dispose(); - lastValue = value; - this.PublishNext(value); - } - - public void OnCompleted() - { - this.PublishCompleted(); - } - - public void OnError(Exception error) - { - this.PublishError(error); - } - - void IObserver.OnNext(T value) - { - ValueNext(value); - } - - protected override void Initialize() - { - } - - protected override void Deinitialize() - { - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Reactive/ObservableEx.cs b/src/Avalonia.Base/Reactive/ObservableEx.cs index dc3be36015..a1ec8f9a8a 100644 --- a/src/Avalonia.Base/Reactive/ObservableEx.cs +++ b/src/Avalonia.Base/Reactive/ObservableEx.cs @@ -21,21 +21,7 @@ namespace Avalonia.Reactive { return new SingleValueImpl(value); } - - /// - /// Disposes the current and saves the next. - /// - /// The type of the value. - /// The source . - /// The observable. - public static IObservable DisposeCurrentOnNext(this IObservable observable) - where T : IDisposable - { - var subject = new DisposeOnNextObservable(); - observable.Subscribe(subject); - return subject; - } - + private class SingleValueImpl : IObservable { private T _value; From a462d0563c75e8c4c461e57847d7b4ad55e94157 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Fri, 14 Sep 2018 10:50:18 +0800 Subject: [PATCH 40/44] Add missing license headers on Avalonia.Animations. --- src/Avalonia.Animation/DoubleAnimator.cs | 5 ++++- src/Avalonia.Animation/FillMode.cs | 5 ++++- src/Avalonia.Animation/IAnimation.cs | 3 +++ src/Avalonia.Animation/IAnimationSetter.cs | 3 +++ src/Avalonia.Animation/IAnimator.cs | 5 ++++- src/Avalonia.Animation/KeyFrame.cs | 5 ++++- src/Avalonia.Animation/KeyFramePair`1.cs | 3 +++ src/Avalonia.Animation/PlayState.cs | 5 ++++- src/Avalonia.Animation/PlaybackDirection.cs | 5 ++++- 9 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Animation/DoubleAnimator.cs b/src/Avalonia.Animation/DoubleAnimator.cs index aeeb29a7dd..2e0ce64185 100644 --- a/src/Avalonia.Animation/DoubleAnimator.cs +++ b/src/Avalonia.Animation/DoubleAnimator.cs @@ -1,4 +1,7 @@ -namespace Avalonia.Animation +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace Avalonia.Animation { /// /// Animator that handles properties. diff --git a/src/Avalonia.Animation/FillMode.cs b/src/Avalonia.Animation/FillMode.cs index 001e1cdeb4..39beecf455 100644 --- a/src/Avalonia.Animation/FillMode.cs +++ b/src/Avalonia.Animation/FillMode.cs @@ -1,4 +1,7 @@ -namespace Avalonia.Animation +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace Avalonia.Animation { public enum FillMode { diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index 1d545a322a..831391ce46 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Threading.Tasks; diff --git a/src/Avalonia.Animation/IAnimationSetter.cs b/src/Avalonia.Animation/IAnimationSetter.cs index 2d22377286..9c8365ea37 100644 --- a/src/Avalonia.Animation/IAnimationSetter.cs +++ b/src/Avalonia.Animation/IAnimationSetter.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + namespace Avalonia.Animation { public interface IAnimationSetter diff --git a/src/Avalonia.Animation/IAnimator.cs b/src/Avalonia.Animation/IAnimator.cs index 9a4da35a02..0f26b7dc2f 100644 --- a/src/Avalonia.Animation/IAnimator.cs +++ b/src/Avalonia.Animation/IAnimator.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; using System.Collections.Generic; namespace Avalonia.Animation diff --git a/src/Avalonia.Animation/KeyFrame.cs b/src/Avalonia.Animation/KeyFrame.cs index 5eb0d2e901..44e39e042e 100644 --- a/src/Avalonia.Animation/KeyFrame.cs +++ b/src/Avalonia.Animation/KeyFrame.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; using System.Collections.Generic; using Avalonia.Collections; diff --git a/src/Avalonia.Animation/KeyFramePair`1.cs b/src/Avalonia.Animation/KeyFramePair`1.cs index b0622a1580..60a16d094f 100644 --- a/src/Avalonia.Animation/KeyFramePair`1.cs +++ b/src/Avalonia.Animation/KeyFramePair`1.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + namespace Avalonia.Animation { /// diff --git a/src/Avalonia.Animation/PlayState.cs b/src/Avalonia.Animation/PlayState.cs index 313d33d586..8d28f06eb1 100644 --- a/src/Avalonia.Animation/PlayState.cs +++ b/src/Avalonia.Animation/PlayState.cs @@ -1,4 +1,7 @@ -namespace Avalonia.Animation +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace Avalonia.Animation { /// /// Determines the playback state of an animation. diff --git a/src/Avalonia.Animation/PlaybackDirection.cs b/src/Avalonia.Animation/PlaybackDirection.cs index bbce6106e1..a44dd388ae 100644 --- a/src/Avalonia.Animation/PlaybackDirection.cs +++ b/src/Avalonia.Animation/PlaybackDirection.cs @@ -1,4 +1,7 @@ -namespace Avalonia.Animation +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace Avalonia.Animation { /// /// Determines the playback direction of an animation. From d9f1da005251757fcee83de2600f65ecd15f4ba9 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Fri, 14 Sep 2018 10:51:59 +0800 Subject: [PATCH 41/44] Fix missed method rename. --- src/Avalonia.Animation/DisposeAnimationInstanceObservable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/DisposeAnimationInstanceObservable.cs b/src/Avalonia.Animation/DisposeAnimationInstanceObservable.cs index 902a09030b..f58e816e54 100644 --- a/src/Avalonia.Animation/DisposeAnimationInstanceObservable.cs +++ b/src/Avalonia.Animation/DisposeAnimationInstanceObservable.cs @@ -53,7 +53,7 @@ namespace Avalonia.Animation _lastInstance?.Dispose(); if (matchVal) { - _lastInstance = _animator.RunAnimation(_animation, _control, _onComplete); + _lastInstance = _animator.Run(_animation, _control, _onComplete); } _lastMatch = matchVal; } From 58319bf299ac0643bd38222dadc103eea7269763 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 14 Sep 2018 15:32:35 -0500 Subject: [PATCH 42/44] Fix typing of Clock.GlobalClock. --- src/Avalonia.Animation/Clock.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/Clock.cs b/src/Avalonia.Animation/Clock.cs index e009c2aad5..bea6c75982 100644 --- a/src/Avalonia.Animation/Clock.cs +++ b/src/Avalonia.Animation/Clock.cs @@ -8,7 +8,7 @@ namespace Avalonia.Animation { public class Clock : ClockBase { - public static IClock GlobalClock => AvaloniaLocator.Current.GetService(); + public static IClock GlobalClock => AvaloniaLocator.Current.GetService(); private IDisposable _parentSubscription; From e4fff20551f80f06e3bdd82861d272814667c445 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sat, 15 Sep 2018 11:56:57 +0800 Subject: [PATCH 43/44] Address PR Reviews --- src/Avalonia.Animation/AnimationInstance`1.cs | 2 +- src/Avalonia.Animation/Animator`1.cs | 2 +- ...servable.cs => DisposeAnimationInstanceSubject.cs} | 4 ++-- src/Avalonia.Controls/ProgressBar.cs | 11 +++-------- 4 files changed, 7 insertions(+), 12 deletions(-) rename src/Avalonia.Animation/{DisposeAnimationInstanceObservable.cs => DisposeAnimationInstanceSubject.cs} (87%) diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index c264663b56..1480fbe741 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -188,7 +188,7 @@ namespace Avalonia.Animation if (!_isLooping) { - if ((_currentIteration > _repeatCount) | (time > iterationEndpoint)) + if ((_currentIteration > _repeatCount) || (time > iterationEndpoint)) DoComplete(); } diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 0de3991a88..bdf655b5cb 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -41,7 +41,7 @@ namespace Avalonia.Animation if (!_isVerifiedAndConverted) VerifyConvertKeyFrames(); - var subject = new DisposeAnimationInstanceObservable(this, animation, control, onComplete); + var subject = new DisposeAnimationInstanceSubject(this, animation, control, onComplete); return match.Subscribe(subject); } diff --git a/src/Avalonia.Animation/DisposeAnimationInstanceObservable.cs b/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs similarity index 87% rename from src/Avalonia.Animation/DisposeAnimationInstanceObservable.cs rename to src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs index f58e816e54..1ac2ac8b98 100644 --- a/src/Avalonia.Animation/DisposeAnimationInstanceObservable.cs +++ b/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs @@ -15,7 +15,7 @@ namespace Avalonia.Animation /// /// Manages the lifetime of animation instances as determined by its selector state. /// - internal class DisposeAnimationInstanceObservable : IObserver, IDisposable + internal class DisposeAnimationInstanceSubject : IObserver, IDisposable { private IDisposable _lastInstance; private bool _lastMatch; @@ -24,7 +24,7 @@ namespace Avalonia.Animation private Animatable _control; private Action _onComplete; - public DisposeAnimationInstanceObservable(Animator animator, Animation animation, Animatable control, Action onComplete) + public DisposeAnimationInstanceSubject(Animator animator, Animation animation, Animatable control, Action onComplete) { this._animator = animator; this._animation = animation; diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index fe7b8e64c7..a0f51099cd 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -37,8 +37,8 @@ namespace Avalonia.Controls PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal"); PseudoClass(IsIndeterminateProperty, ":indeterminate"); - ValueProperty.Changed.AddClassHandler(x => x.ValueChanged); - IsIndeterminateProperty.Changed.AddClassHandler(x => x.IsIndeterminateChanged); + ValueProperty.Changed.AddClassHandler(x => x.UpdateIndicatorWhenPropChanged); + IsIndeterminateProperty.Changed.AddClassHandler(x => x.UpdateIndicatorWhenPropChanged); } public bool IsIndeterminate @@ -115,12 +115,7 @@ namespace Avalonia.Controls } } - private void ValueChanged(AvaloniaPropertyChangedEventArgs e) - { - UpdateIndicator(Bounds.Size); - } - - private void IsIndeterminateChanged(AvaloniaPropertyChangedEventArgs e) + private void UpdateIndicatorWhenPropChanged(AvaloniaPropertyChangedEventArgs e) { UpdateIndicator(Bounds.Size); } From 12747f3175b590f6b3796714a2a729e6d6cd99d8 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sat, 15 Sep 2018 12:09:43 +0800 Subject: [PATCH 44/44] Fix missed merge conflict. --- src/Avalonia.Animation/Animator`1.cs | 6 +++--- src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 3a30c60e79..d1a8960a10 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -41,7 +41,7 @@ namespace Avalonia.Animation if (!_isVerifiedAndConverted) VerifyConvertKeyFrames(); - var subject = new DisposeAnimationInstanceSubject(this, animation, control, onComplete); + var subject = new DisposeAnimationInstanceSubject(this, animation, control, clock, onComplete); return match.Subscribe(subject); } @@ -55,7 +55,7 @@ namespace Avalonia.Animation /// The time parameter, relative to the total animation time protected (double IntraKFTime, KeyFramePair KFPair) GetKFPairAndIntraKFTime(double animationTime) { - AnimatorKeyFrame firstKeyframe, lastKeyframe ; + AnimatorKeyFrame firstKeyframe, lastKeyframe; int kvCount = _convertedKeyframes.Count; if (kvCount > 2) { @@ -89,7 +89,7 @@ namespace Avalonia.Animation var lastFrameData = (lastKeyframe.GetTypedValue(), lastKeyframe.isNeutral); return (intraframeTime, new KeyFramePair(firstFrameData, lastFrameData)); } - + private int FindClosestBeforeKeyFrame(double time) { int FindClosestBeforeKeyFrame(int startIndex, int length) diff --git a/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs b/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs index 1ac2ac8b98..a535b30b58 100644 --- a/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs +++ b/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs @@ -23,15 +23,18 @@ namespace Avalonia.Animation private Animation _animation; private Animatable _control; private Action _onComplete; + private IClock _clock; - public DisposeAnimationInstanceSubject(Animator animator, Animation animation, Animatable control, Action onComplete) + public DisposeAnimationInstanceSubject(Animator animator, Animation animation, Animatable control, IClock clock, Action onComplete) { this._animator = animator; this._animation = animation; this._control = control; this._onComplete = onComplete; + this._clock = clock; } + public void Dispose() { _lastInstance?.Dispose(); @@ -53,7 +56,7 @@ namespace Avalonia.Animation _lastInstance?.Dispose(); if (matchVal) { - _lastInstance = _animator.Run(_animation, _control, _onComplete); + _lastInstance = _animator.Run(_animation, _control, _clock, _onComplete); } _lastMatch = matchVal; }