From aa5c1a6ed487de0b08cf9d08294c0edf51a0c9cc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Apr 2020 19:03:13 +0200 Subject: [PATCH] Added ScrollViewer.ScrollChanged event. The API for `ScrollChangedEventArgs` is different to WPF's here, because: - Avalonia's `ScrollViewer` exposes `Extent`, `Offset` and `Viewport` as `Size`/`Vector` structs whereas WPF exposes separate `double` values for the X and Y components for each of these - The current values are not included in the event args: then can easily be read from the `sender` - UWP doesn't expose these values at all --- .../ScrollChangedEventArgs.cs | 45 ++++++++++++ src/Avalonia.Controls/ScrollViewer.cs | 49 ++++++++++++- .../ScrollViewerTests.cs | 73 ++++++++++++++++++- 3 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 src/Avalonia.Controls/ScrollChangedEventArgs.cs diff --git a/src/Avalonia.Controls/ScrollChangedEventArgs.cs b/src/Avalonia.Controls/ScrollChangedEventArgs.cs new file mode 100644 index 0000000000..fed23964f5 --- /dev/null +++ b/src/Avalonia.Controls/ScrollChangedEventArgs.cs @@ -0,0 +1,45 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + /// + /// Describes a change in scrolling state. + /// + public class ScrollChangedEventArgs : RoutedEventArgs + { + public ScrollChangedEventArgs( + Vector extentDelta, + Vector offsetDelta, + Vector viewportDelta) + : this(ScrollViewer.ScrollChangedEvent, extentDelta, offsetDelta, viewportDelta) + { + } + + public ScrollChangedEventArgs( + RoutedEvent routedEvent, + Vector extentDelta, + Vector offsetDelta, + Vector viewportDelta) + : base(routedEvent) + { + ExtentDelta = extentDelta; + OffsetDelta = offsetDelta; + ViewportDelta = viewportDelta; + } + + /// + /// Gets the change to the value of . + /// + public Vector ExtentDelta { get; } + + /// + /// Gets the change to the value of . + /// + public Vector OffsetDelta { get; } + + /// + /// Gets the change to the value of . + /// + public Vector ViewportDelta { get; } + } +} diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index f5881a8efe..fa95895fce 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -2,6 +2,7 @@ using System; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Interactivity; namespace Avalonia.Controls { @@ -167,6 +168,14 @@ namespace Avalonia.Controls nameof(VerticalScrollBarVisibility), ScrollBarVisibility.Auto); + /// + /// Defines the event. + /// + public static readonly RoutedEvent ScrollChangedEvent = + RoutedEvent.Register( + nameof(ScrollChanged), + RoutingStrategies.Bubble); + private IDisposable _childSubscription; private ILogicalScrollable _logicalScrollable; private Size _extent; @@ -191,6 +200,15 @@ namespace Avalonia.Controls { } + /// + /// Occurs when changes are detected to the scroll position, extent, or viewport size. + /// + public event EventHandler ScrollChanged + { + add => AddHandler(ScrollChangedEvent, value); + remove => RemoveHandler(ScrollChangedEvent, value); + } + /// /// Gets the extent of the scrollable content. /// @@ -203,9 +221,11 @@ namespace Avalonia.Controls private set { + var old = _extent; + if (SetAndRaise(ExtentProperty, ref _extent, value)) { - CalculatedPropertiesChanged(); + CalculatedPropertiesChanged(extentDelta: value - old); } } } @@ -222,11 +242,13 @@ namespace Avalonia.Controls set { + var old = _offset; + value = ValidateOffset(this, value); if (SetAndRaise(OffsetProperty, ref _offset, value)) { - CalculatedPropertiesChanged(); + CalculatedPropertiesChanged(offsetDelta: value - old); } } } @@ -243,9 +265,11 @@ namespace Avalonia.Controls private set { + var old = _viewport; + if (SetAndRaise(ViewportProperty, ref _viewport, value)) { - CalculatedPropertiesChanged(); + CalculatedPropertiesChanged(viewportDelta: value - old); } } } @@ -525,7 +549,10 @@ namespace Avalonia.Controls } } - private void CalculatedPropertiesChanged() + private void CalculatedPropertiesChanged( + Size extentDelta = default, + Vector offsetDelta = default, + Size viewportDelta = default) { // Pass old values of 0 here because we don't have the old values at this point, // and it shouldn't matter as only the template uses these properies. @@ -546,6 +573,20 @@ namespace Avalonia.Controls SetAndRaise(SmallChangeProperty, ref _smallChange, s_defaultSmallChange); SetAndRaise(LargeChangeProperty, ref _largeChange, Viewport); } + + if (extentDelta != default || offsetDelta != default || viewportDelta != default) + { + using var route = BuildEventRoute(ScrollChangedEvent); + + if (route.HasHandlers) + { + var e = new ScrollChangedEventArgs( + new Vector(extentDelta.Width, extentDelta.Height), + offsetDelta, + new Vector(viewportDelta.Width, viewportDelta.Height)); + route.RaiseEvent(this, e); + } + } } protected override void OnKeyDown(KeyEventArgs e) diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index 5375a244c9..8da1e26f0d 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs @@ -4,7 +4,6 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Layout; -using Avalonia.LogicalTree; using Moq; using Xunit; @@ -147,6 +146,78 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(45, 67), target.LargeChange); } + [Fact] + public void Changing_Extent_Should_Raise_ScrollChanged() + { + var target = new ScrollViewer(); + var raised = 0; + + target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); + target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); + target.Offset = new Vector(10, 10); + + target.ScrollChanged += (s, e) => + { + Assert.Equal(new Vector(11, 12), e.ExtentDelta); + Assert.Equal(default, e.OffsetDelta); + Assert.Equal(default, e.ViewportDelta); + ++raised; + }; + + target.SetValue(ScrollViewer.ExtentProperty, new Size(111, 112)); + + Assert.Equal(1, raised); + + } + + [Fact] + public void Changing_Offset_Should_Raise_ScrollChanged() + { + var target = new ScrollViewer(); + var raised = 0; + + target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); + target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); + target.Offset = new Vector(10, 10); + + target.ScrollChanged += (s, e) => + { + Assert.Equal(default, e.ExtentDelta); + Assert.Equal(new Vector(12, 14), e.OffsetDelta); + Assert.Equal(default, e.ViewportDelta); + ++raised; + }; + + target.Offset = new Vector(22, 24); + + Assert.Equal(1, raised); + + } + + [Fact] + public void Changing_Viewport_Should_Raise_ScrollChanged() + { + var target = new ScrollViewer(); + var raised = 0; + + target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); + target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); + target.Offset = new Vector(10, 10); + + target.ScrollChanged += (s, e) => + { + Assert.Equal(default, e.ExtentDelta); + Assert.Equal(default, e.OffsetDelta); + Assert.Equal(new Vector(6, 8), e.ViewportDelta); + ++raised; + }; + + target.SetValue(ScrollViewer.ViewportProperty, new Size(56, 58)); + + Assert.Equal(1, raised); + + } + private Control CreateTemplate(ScrollViewer control, INameScope scope) { return new Grid