Browse Source

Merge pull request #758 from AvaloniaUI/fixes/633-virt-horizontal-scroll

WIP: Cross-axis scrolling for virtualized lists.
Steven Kirk 9 năm trước cách đây
mục cha
commit
d35c483b64

+ 62 - 5
src/Avalonia.Controls/Presenters/ItemVirtualizer.cs

@@ -16,6 +16,8 @@ namespace Avalonia.Controls.Presenters
     /// </summary>
     internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable
     {
+        private double _crossAxisOffset;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ItemVirtualizer"/> class.
         /// </summary>
@@ -60,7 +62,7 @@ namespace Avalonia.Controls.Presenters
         /// <summary>
         /// Gets a value indicating whether the items should be scroll horizontally or vertically.
         /// </summary>
-        public bool Vertical => VirtualizingPanel.ScrollDirection == Orientation.Vertical;
+        public bool Vertical => VirtualizingPanel?.ScrollDirection == Orientation.Vertical;
 
         /// <summary>
         /// Gets a value indicating whether logical scrolling is enabled.
@@ -85,12 +87,28 @@ namespace Avalonia.Controls.Presenters
         /// <summary>
         /// Gets the <see cref="ExtentValue"/> as a <see cref="Size"/>.
         /// </summary>
-        public Size Extent => Vertical ? new Size(0, ExtentValue) : new Size(ExtentValue, 0);
+        public Size Extent
+        {
+            get
+            {
+                return Vertical ?
+                    new Size(Owner.Panel.DesiredSize.Width, ExtentValue) :
+                    new Size(ExtentValue, Owner.Panel.DesiredSize.Height);
+            }
+        }
 
         /// <summary>
         /// Gets the <see cref="ViewportValue"/> as a <see cref="Size"/>.
         /// </summary>
-        public Size Viewport => Vertical ? new Size(0, ViewportValue) : new Size(ViewportValue, 0);
+        public Size Viewport
+        {
+            get
+            {
+                return Vertical ? 
+                    new Size(Owner.Panel.Bounds.Width, ViewportValue) :
+                    new Size(ViewportValue, Owner.Panel.Bounds.Height);
+            }
+        }
 
         /// <summary>
         /// Gets or sets the <see cref="OffsetValue"/> as a <see cref="Vector"/>.
@@ -99,12 +117,28 @@ namespace Avalonia.Controls.Presenters
         {
             get
             {
-                return Vertical ? new Vector(0, OffsetValue) : new Vector(OffsetValue, 0);
+                return Vertical ? new Vector(_crossAxisOffset, OffsetValue) : new Vector(OffsetValue, _crossAxisOffset);
             }
 
             set
             {
-                OffsetValue = Vertical ? value.Y : value.X;
+                var oldCrossAxisOffset = _crossAxisOffset;
+
+                if (Vertical)
+                {
+                    OffsetValue = value.Y;
+                    _crossAxisOffset = value.X;
+                }
+                else
+                {
+                    OffsetValue = value.X;
+                    _crossAxisOffset = value.Y;
+                }
+
+                if (_crossAxisOffset != oldCrossAxisOffset)
+                {
+                    Owner.InvalidateArrange();
+                }
             }
         }
         
@@ -143,6 +177,29 @@ namespace Avalonia.Controls.Presenters
             return result;
         }
 
+        /// <summary>
+        /// Carries out a measure for the related <see cref="ItemsPresenter"/>.
+        /// </summary>
+        /// <param name="availableSize">The size available to the control.</param>
+        /// <returns>The desired size for the control.</returns>
+        public virtual Size MeasureOverride(Size availableSize)
+        {
+            Owner.Panel.Measure(availableSize);
+            return Owner.Panel.DesiredSize;
+        }
+
+        /// <summary>
+        /// Carries out an arrange for the related <see cref="ItemsPresenter"/>.
+        /// </summary>
+        /// <param name="finalSize">The size available to the control.</param>
+        /// <returns>The actual size used.</returns>
+        public virtual Size ArrangeOverride(Size finalSize)
+        {
+            var origin = Vertical ? new Point(-_crossAxisOffset, 0) : new Point(0, _crossAxisOffset);
+            Owner.Panel.Arrange(new Rect(origin, finalSize));
+            return finalSize;
+        }
+
         /// <inheritdoc/>
         public virtual void UpdateControls()
         {

+ 50 - 2
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@@ -94,6 +94,44 @@ namespace Avalonia.Controls.Presenters
             }
         }
 
+        /// <inheritdoc/>
+        public override Size MeasureOverride(Size availableSize)
+        {
+            var window = Owner.GetVisualRoot() as TopLevel;
+
+            // If infinity is passed as the available size and we're virtualized then we need to
+            // fill the available space, but to do that we *don't* want to materialize all our
+            // items! Take a look at the root of the tree for a MaxClientSize and use that as
+            // the available size.
+            if (VirtualizingPanel.ScrollDirection == Orientation.Vertical)
+            {
+                if (availableSize.Height == double.PositiveInfinity)
+                {
+                    if (window != null)
+                    {
+                        availableSize = availableSize.WithHeight(window.PlatformImpl.MaxClientSize.Height);
+                    }
+                }
+
+                availableSize = availableSize.WithWidth(double.PositiveInfinity);
+            }
+            else
+            {
+                if (availableSize.Width == double.PositiveInfinity)
+                {
+                    if (window != null)
+                    {
+                        availableSize = availableSize.WithWidth(window.PlatformImpl.MaxClientSize.Width);
+                    }
+                }
+
+                availableSize = availableSize.WithHeight(double.PositiveInfinity);
+            }
+
+            Owner.Panel.Measure(availableSize);
+            return Owner.Panel.DesiredSize;
+        }
+
         /// <inheritdoc/>
         public override void UpdateControls()
         {
@@ -481,9 +519,19 @@ namespace Avalonia.Controls.Presenters
                 {
                     layoutManager.ExecuteLayoutPass();
 
-                    if (!new Rect(panel.Bounds.Size).Contains(container.Bounds))
+                    if (panel.ScrollDirection == Orientation.Vertical)
                     {
-                        OffsetValue += 1;
+                        if (container.Bounds.Y < panel.Bounds.Y || container.Bounds.Bottom > panel.Bounds.Bottom)
+                        {
+                            OffsetValue += 1;
+                        }
+                    }
+                    else
+                    {
+                        if (container.Bounds.X < panel.Bounds.X || container.Bounds.Right > panel.Bounds.Right)
+                        {
+                            OffsetValue += 1;
+                        }
                     }
                 }
 

+ 6 - 15
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@@ -91,24 +91,15 @@ namespace Avalonia.Controls.Presenters
             _virtualizer?.ScrollIntoView(item);
         }
 
+        /// <inheritdoc/>
         protected override Size MeasureOverride(Size availableSize)
         {
-            // If infinity is passed as the available size and we're virtualized then we need to
-            // fill the available space, but to do that we *don't* want to materialize all our
-            // items! Take a look at the root of the tree for a MaxClientSize and use that as
-            // the available size.
-            if (availableSize == Size.Infinity && VirtualizationMode != ItemVirtualizationMode.None)
-            {
-                var window = VisualRoot as TopLevel;
-
-                if (window != null)
-                {
-                    availableSize = window.PlatformImpl.MaxClientSize;
-                }
-            }
+            return _virtualizer?.MeasureOverride(availableSize) ?? Size.Empty;
+        }
 
-            Panel.Measure(availableSize);
-            return Panel.DesiredSize;
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            return _virtualizer?.ArrangeOverride(finalSize) ?? Size.Empty;
         }
 
         /// <inheritdoc/>

+ 1 - 1
src/Avalonia.Layout/LayoutManager.cs

@@ -168,7 +168,7 @@ namespace Avalonia.Layout
 
         private void QueueLayoutPass()
         {
-            if (!_queued)
+            if (!_queued && !_running)
             {
                 Dispatcher.UIThread.InvokeAsync(ExecuteLayoutPass, DispatcherPriority.Render);
                 _queued = true;

+ 4 - 4
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs

@@ -111,7 +111,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
             target.Arrange(new Rect(0, 0, 100, 100));
 
             var scroll = (ScrollContentPresenter)target.Parent;
-            Assert.Equal(new Size(0, 20), scroll.Extent);
+            Assert.Equal(new Size(10, 20), scroll.Extent);
             Assert.Equal(new Size(0, 10), scroll.Viewport);
         }
 
@@ -212,8 +212,8 @@ namespace Avalonia.Controls.UnitTests.Presenters
             target.Arrange(new Rect(0, 0, 100, 100));
 
             Assert.Equal(10, target.Panel.Children.Count);
-            Assert.Equal(new Size(0, 20), scroll.Extent);
-            Assert.Equal(new Size(0, 10), scroll.Viewport);
+            Assert.Equal(new Size(10, 20), scroll.Extent);
+            Assert.Equal(new Size(100, 10), scroll.Viewport);
         }
 
         [Fact]
@@ -253,7 +253,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
             scroll.Arrange(new Rect(0, 0, 100, 100));
 
             Assert.Equal(10, target.Panel.Children.Count);
-            Assert.Equal(new Size(0, 20), scroll.Extent);
+            Assert.Equal(new Size(10, 20), scroll.Extent);
             Assert.Equal(new Size(0, 10), scroll.Viewport);
 
             target.VirtualizationMode = ItemVirtualizationMode.None;

+ 16 - 3
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs

@@ -49,7 +49,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
             target.Measure(new Size(100, 100));
             target.Arrange(new Rect(0, 0, 100, 100));
 
-            Assert.Equal(new Size(0, 10), ((ILogicalScrollable)target).Viewport);
+            Assert.Equal(new Size(100, 10), ((ILogicalScrollable)target).Viewport);
         }
 
         [Fact]
@@ -61,7 +61,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
             target.Measure(new Size(100, 100));
             target.Arrange(new Rect(0, 0, 100, 100));
 
-            Assert.Equal(new Size(10, 0), ((ILogicalScrollable)target).Viewport);
+            Assert.Equal(new Size(10, 100), ((ILogicalScrollable)target).Viewport);
         }
 
         [Fact]
@@ -146,7 +146,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
             target.Measure(new Size(100, 95));
             target.Arrange(new Rect(0, 0, 100, 95));
 
-            Assert.Equal(new Size(0, 9), ((ILogicalScrollable)target).Viewport);
+            Assert.Equal(new Size(100, 9), ((ILogicalScrollable)target).Viewport);
         }
 
         [Fact]
@@ -772,6 +772,19 @@ namespace Avalonia.Controls.UnitTests.Presenters
                     Assert.Same(target.Panel.Children[0], result);
                 }
             }
+
+            [Fact]
+            public void Should_Return_Horizontal_Extent_And_Viewport()
+            {
+                var target = CreateTarget();
+
+                target.ApplyTemplate();
+                target.Measure(new Size(5, 100));
+                target.Arrange(new Rect(0, 0, 5, 100));
+
+                Assert.Equal(new Size(10, 20), ((ILogicalScrollable)target).Extent);
+                Assert.Equal(new Size(5, 10), ((ILogicalScrollable)target).Viewport);
+            }
         }
 
         public class Horizontal