Procházet zdrojové kódy

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
Steven Kirk před 5 roky
rodič
revize
aa5c1a6ed4

+ 45 - 0
src/Avalonia.Controls/ScrollChangedEventArgs.cs

@@ -0,0 +1,45 @@
+using Avalonia.Interactivity;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Describes a change in scrolling state.
+    /// </summary>
+    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;
+        }
+
+        /// <summary>
+        /// Gets the change to the value of <see cref="ScrollViewer.Extent"/>.
+        /// </summary>
+        public Vector ExtentDelta { get; }
+
+        /// <summary>
+        /// Gets the change to the value of <see cref="ScrollViewer.Offset"/>.
+        /// </summary>
+        public Vector OffsetDelta { get; }
+
+        /// <summary>
+        /// Gets the change to the value of <see cref="ScrollViewer.Viewport"/>.
+        /// </summary>
+        public Vector ViewportDelta { get; }
+    }
+}

+ 45 - 4
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);
 
+        /// <summary>
+        /// Defines the <see cref="ScrollChanged"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<ScrollChangedEventArgs> ScrollChangedEvent =
+            RoutedEvent.Register<ScrollViewer, ScrollChangedEventArgs>(
+                nameof(ScrollChanged),
+                RoutingStrategies.Bubble);
+
         private IDisposable _childSubscription;
         private ILogicalScrollable _logicalScrollable;
         private Size _extent;
@@ -191,6 +200,15 @@ namespace Avalonia.Controls
         {
         }
 
+        /// <summary>
+        /// Occurs when changes are detected to the scroll position, extent, or viewport size.
+        /// </summary>
+        public event EventHandler<ScrollChangedEventArgs> ScrollChanged
+        {
+            add => AddHandler(ScrollChangedEvent, value);
+            remove => RemoveHandler(ScrollChangedEvent, value);
+        }
+
         /// <summary>
         /// Gets the extent of the scrollable content.
         /// </summary>
@@ -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)

+ 72 - 1
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