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