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 8bfcc1f9db..c3f0dc0056 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 { @@ -165,6 +166,14 @@ namespace Avalonia.Controls nameof(VerticalScrollBarVisibility), ScrollBarVisibility.Auto); + /// + /// Defines the event. + /// + public static readonly RoutedEvent ScrollChangedEvent = + RoutedEvent.Register( + nameof(ScrollChanged), + RoutingStrategies.Bubble); + internal const double DefaultSmallChange = 16; private IDisposable _childSubscription; @@ -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, new Size(DefaultSmallChange, 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