From 7067fa01462409e5de6a02b03033988efc952e9e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 23 Sep 2016 20:13:39 +0200 Subject: [PATCH 1/2] Cross-axis scrolling for virtualized lists. There are still a few bugs around scrollbar behavior, and maybe Grid. To see them, add a long item in the middle of VirtualizationTest's items, resize the window to require a scrollbar and scroll up and down. --- .../Presenters/ItemVirtualizer.cs | 67 +++++++++++++++++-- .../Presenters/ItemVirtualizerSimple.cs | 38 +++++++++++ .../Presenters/ItemsPresenter.cs | 21 ++---- src/Avalonia.Layout/LayoutManager.cs | 2 +- .../ItemsPresenterTests_Virtualization.cs | 8 +-- ...emsPresenterTests_Virtualization_Simple.cs | 19 +++++- 6 files changed, 127 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 74c7da20d6..3a2cb688cb 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -16,6 +16,8 @@ namespace Avalonia.Controls.Presenters /// internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable { + private double _crossAxisOffset; + /// /// Initializes a new instance of the class. /// @@ -60,7 +62,7 @@ namespace Avalonia.Controls.Presenters /// /// Gets a value indicating whether the items should be scroll horizontally or vertically. /// - public bool Vertical => VirtualizingPanel.ScrollDirection == Orientation.Vertical; + public bool Vertical => VirtualizingPanel?.ScrollDirection == Orientation.Vertical; /// /// Gets a value indicating whether logical scrolling is enabled. @@ -85,12 +87,28 @@ namespace Avalonia.Controls.Presenters /// /// Gets the as a . /// - 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); + } + } /// /// Gets the as a . /// - 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); + } + } /// /// Gets or sets the as a . @@ -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; } + /// + /// Carries out a measure for the related . + /// + /// The size available to the control. + /// The desired size for the control. + public virtual Size MeasureOverride(Size availableSize) + { + Owner.Panel.Measure(availableSize); + return Owner.Panel.DesiredSize; + } + + /// + /// Carries out an arrange for the related . + /// + /// The size available to the control. + /// The actual size used. + 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; + } + /// public virtual void UpdateControls() { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index afa5bb76db..378333e32c 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -94,6 +94,44 @@ namespace Avalonia.Controls.Presenters } } + /// + 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; + } + /// public override void UpdateControls() { diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index fa15766e4d..185193f889 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -91,24 +91,15 @@ namespace Avalonia.Controls.Presenters _virtualizer?.ScrollIntoView(item); } + /// 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; } /// diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 85daff28b9..b7b83bf852 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/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; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 02dffdead6..1433bc7f85 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/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; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 2a59227b52..8d10b01f94 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/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 From 612839c13a94fe163e7bb31743f178e6a999277c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 1 Oct 2016 16:26:48 +0200 Subject: [PATCH 2/2] Fix virtualized list keyboard scrolling. When cross-axis scrollbar is visible. --- .../Presenters/ItemVirtualizerSimple.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 378333e32c..228ad65ffa 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -519,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; + } } }