Переглянути джерело

Raise ScrollChanged when layout updated.

To be consistent with WPF/UWP `ScrollChanged` should be raised when layout finishes updating, rather than when the individual properties are changed.
Steven Kirk 5 роки тому
батько
коміт
c3a5c48d6f

+ 38 - 27
src/Avalonia.Controls/ScrollViewer.cs

@@ -181,6 +181,9 @@ namespace Avalonia.Controls
         private Size _extent;
         private Vector _offset;
         private Size _viewport;
+        private Size _oldExtent;
+        private Vector _oldOffset;
+        private Size _oldViewport;
         private Size _largeChange;
         private Size _smallChange = new Size(DefaultSmallChange, DefaultSmallChange);
 
@@ -198,6 +201,7 @@ namespace Avalonia.Controls
         /// </summary>
         public ScrollViewer()
         {
+            LayoutUpdated += OnLayoutUpdated;
         }
 
         /// <summary>
@@ -221,11 +225,9 @@ namespace Avalonia.Controls
 
             private set
             {
-                var old = _extent;
-
                 if (SetAndRaise(ExtentProperty, ref _extent, value))
                 {
-                    CalculatedPropertiesChanged(extentDelta: value - old);
+                    CalculatedPropertiesChanged();
                 }
             }
         }
@@ -242,13 +244,11 @@ namespace Avalonia.Controls
 
             set
             {
-                var old = _offset;
-
                 value = ValidateOffset(this, value);
 
                 if (SetAndRaise(OffsetProperty, ref _offset, value))
                 {
-                    CalculatedPropertiesChanged(offsetDelta: value - old);
+                    CalculatedPropertiesChanged();
                 }
             }
         }
@@ -265,11 +265,9 @@ namespace Avalonia.Controls
 
             private set
             {
-                var old = _viewport;
-
                 if (SetAndRaise(ViewportProperty, ref _viewport, value))
                 {
-                    CalculatedPropertiesChanged(viewportDelta: value - old);
+                    CalculatedPropertiesChanged();
                 }
             }
         }
@@ -549,10 +547,7 @@ namespace Avalonia.Controls
             }
         }
 
-        private void CalculatedPropertiesChanged(
-            Size extentDelta = default,
-            Vector offsetDelta = default,
-            Size viewportDelta = default)
+        private void CalculatedPropertiesChanged()
         {
             // 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.
@@ -573,20 +568,6 @@ 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)
@@ -602,5 +583,35 @@ namespace Avalonia.Controls
                 e.Handled = true;
             }
         }
+
+        /// <summary>
+        /// Called when a change in scrolling state is detected, such as a change in scroll
+        /// position, extent, or viewport size.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        /// <remarks>
+        /// If you override this method, call `base.OnScrollChanged(ScrollChangedEventArgs)` to
+        /// ensure that this event is raised.
+        /// </remarks>
+        protected virtual void OnScrollChanged(ScrollChangedEventArgs e)
+        {
+            RaiseEvent(e);
+        }
+
+        private void OnLayoutUpdated(object sender, EventArgs e) => RaiseScrollChanged();
+
+        private void RaiseScrollChanged()
+        {
+            var e = new ScrollChangedEventArgs(
+                new Vector(Extent.Width - _oldExtent.Width, Extent.Height - _oldExtent.Height),
+                Offset - _oldOffset,
+                new Vector(Viewport.Width - _oldViewport.Width, Viewport.Height - _oldViewport.Height));
+
+            OnScrollChanged(e);
+
+            _oldExtent = Extent;
+            _oldOffset = Offset;
+            _oldViewport = Viewport;
+        }
     }
 }

+ 22 - 3
tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs

@@ -4,6 +4,7 @@ using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Layout;
+using Avalonia.UnitTests;
 using Moq;
 using Xunit;
 
@@ -150,12 +151,15 @@ namespace Avalonia.Controls.UnitTests
         public void Changing_Extent_Should_Raise_ScrollChanged()
         {
             var target = new ScrollViewer();
+            var root = new TestRoot(target);
             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);
 
+            root.LayoutManager.ExecuteInitialLayoutPass(root);
+
             target.ScrollChanged += (s, e) =>
             {
                 Assert.Equal(new Vector(11, 12), e.ExtentDelta);
@@ -166,20 +170,26 @@ namespace Avalonia.Controls.UnitTests
 
             target.SetValue(ScrollViewer.ExtentProperty, new Size(111, 112));
 
-            Assert.Equal(1, raised);
+            Assert.Equal(0, raised);
+
+            root.LayoutManager.ExecuteLayoutPass();
 
+            Assert.Equal(1, raised);
         }
 
         [Fact]
         public void Changing_Offset_Should_Raise_ScrollChanged()
         {
             var target = new ScrollViewer();
+            var root = new TestRoot(target);
             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);
 
+            root.LayoutManager.ExecuteInitialLayoutPass(root);
+
             target.ScrollChanged += (s, e) =>
             {
                 Assert.Equal(default, e.ExtentDelta);
@@ -190,20 +200,26 @@ namespace Avalonia.Controls.UnitTests
 
             target.Offset = new Vector(22, 24);
 
-            Assert.Equal(1, raised);
+            Assert.Equal(0, raised);
 
+            root.LayoutManager.ExecuteLayoutPass();
+
+            Assert.Equal(1, raised);
         }
 
         [Fact]
         public void Changing_Viewport_Should_Raise_ScrollChanged()
         {
             var target = new ScrollViewer();
+            var root = new TestRoot(target);
             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);
 
+            root.LayoutManager.ExecuteInitialLayoutPass(root);
+
             target.ScrollChanged += (s, e) =>
             {
                 Assert.Equal(default, e.ExtentDelta);
@@ -214,8 +230,11 @@ namespace Avalonia.Controls.UnitTests
 
             target.SetValue(ScrollViewer.ViewportProperty, new Size(56, 58));
 
-            Assert.Equal(1, raised);
+            Assert.Equal(0, raised);
+
+            root.LayoutManager.ExecuteLayoutPass();
 
+            Assert.Equal(1, raised);
         }
 
         private Control CreateTemplate(ScrollViewer control, INameScope scope)