From 7f09154020ab54ccb0a64bd9528c6b4144995336 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 26 May 2016 17:54:35 +0200 Subject: [PATCH] Correctly handle partially obscured items. And move logical for selecting horizontal/vertical components of extent/offset/viewport into `ItemVirtualizer` base class. --- src/Avalonia.Controls/IVirtualizingPanel.cs | 10 +++ .../Presenters/ItemVirtualizer.cs | 28 +++++- .../Presenters/ItemVirtualizerNone.cs | 6 +- .../Presenters/ItemVirtualizerSimple.cs | 85 +++++++++---------- .../VirtualizingStackPanel.cs | 10 +++ .../ItemsPresenterTests_Virtualization.cs | 21 +++-- 6 files changed, 102 insertions(+), 58 deletions(-) diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index 2aa356d38c..ce320c0da7 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -30,6 +30,16 @@ namespace Avalonia.Controls /// double AverageItemSize { get; } + /// + /// Gets or sets a size in pixels by which the content is overflowing the panel, in the + /// direction of scroll. + /// + /// + /// This may be non-zero even when is zero if the last item + /// overflows the panel bounds. + /// + double PixelOverflow { get; } + /// /// Gets or sets the current pixel offset of the items in the direction of scroll. /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index a92990b3ba..a7d1de5777 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Specialized; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Utils; using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters @@ -19,14 +20,32 @@ namespace Avalonia.Controls.Presenters public ItemsPresenter Owner { get; } public IVirtualizingPanel VirtualizingPanel => Owner.Panel as IVirtualizingPanel; public IEnumerable Items { get; private set; } + public int ItemCount { get; private set; } public int FirstIndex { get; set; } - public int LastIndex { get; set; } = -1; + public int NextIndex { get; set; } + public bool Vertical => VirtualizingPanel.ScrollDirection == Orientation.Vertical; public abstract bool IsLogicalScrollEnabled { get; } - public abstract Size Extent { get; } - public abstract Vector Offset { get; set; } - public abstract Size Viewport { get; } + public abstract double ExtentValue { get; } + public abstract double OffsetValue { get; set; } + public abstract double ViewportValue { get; } + public Size Extent => Vertical ? new Size(0, ExtentValue) : new Size(ExtentValue, 0); + public Size Viewport => Vertical ? new Size(0, ViewportValue) : new Size(ViewportValue, 0); + + public Vector Offset + { + get + { + return Vertical ? new Vector(0, OffsetValue) : new Vector(OffsetValue, 0); + } + + set + { + OffsetValue = Vertical ? value.Y : value.X; + } + } + public static ItemVirtualizer Create(ItemsPresenter owner) { var virtualizingPanel = owner.Panel as IVirtualizingPanel; @@ -54,6 +73,7 @@ namespace Avalonia.Controls.Presenters public virtual void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) { Items = items; + ItemCount = items.Count(); } } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index 919bde7e0f..a8c24fcf72 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -19,18 +19,18 @@ namespace Avalonia.Controls.Presenters public override bool IsLogicalScrollEnabled => false; - public override Size Extent + public override double ExtentValue { get { throw new NotSupportedException(); } } - public override Vector Offset + public override double OffsetValue { get { throw new NotSupportedException(); } set { throw new NotSupportedException(); } } - public override Size Viewport + public override double ViewportValue { get { throw new NotSupportedException(); } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 41e7c1b072..45c1c927e9 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -19,64 +19,59 @@ namespace Avalonia.Controls.Presenters public override bool IsLogicalScrollEnabled => true; - public override Size Extent - { - get - { - if (VirtualizingPanel.ScrollDirection == Orientation.Vertical) - { - return new Size(0, Items.Count()); - } - else - { - return new Size(Items.Count(), 0); - } - } - } + public override double ExtentValue => ItemCount; - public override Vector Offset + public override double OffsetValue { get { - if (VirtualizingPanel.ScrollDirection == Orientation.Vertical) - { - return new Vector(0, FirstIndex); - } - else - { - return new Vector(FirstIndex, 0); - } + var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0; + return FirstIndex + offset; } set { - var scroll = (VirtualizingPanel.ScrollDirection == Orientation.Vertical) ? - value.Y : value.X; - var delta = (int)(scroll - FirstIndex); + var panel = VirtualizingPanel; + var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0; + var delta = (int)(value - (FirstIndex + offset)); if (delta != 0) { - RecycleContainers(delta); - FirstIndex += delta; - LastIndex += delta; + if ((NextIndex - 1) + delta < ItemCount) + { + if (panel.PixelOffset > 0) + { + panel.PixelOffset = 0; + delta += 1; + } + + if (delta != 0) + { + RecycleContainers(delta); + FirstIndex += delta; + NextIndex += delta; + } + } + else + { + // We're moving to a partially obscured item at the end of the list. + var firstIndex = ItemCount - panel.Children.Count; + RecycleContainers(firstIndex - FirstIndex); + NextIndex = ItemCount; + FirstIndex = NextIndex - panel.Children.Count; + panel.PixelOffset = VirtualizingPanel.PixelOverflow; + } } } } - public override Size Viewport + public override double ViewportValue { get { - var panel = VirtualizingPanel; - - if (panel.ScrollDirection == Orientation.Vertical) - { - return new Size(0, panel.Children.Count); - } - else - { - return new Size(panel.Children.Count, 0); - } + // If we can't fit the last item in the panel fully, subtract 1 from the viewport. + var overflow = VirtualizingPanel.PixelOverflow > 0 ? 1 : 0; + return VirtualizingPanel.Children.Count - overflow; } } @@ -96,8 +91,7 @@ namespace Avalonia.Controls.Presenters // Reset indicates a large change and should (?) be quite rare. VirtualizingPanel.Children.Clear(); Owner.ItemContainerGenerator.Clear(); - FirstIndex = 0; - LastIndex = -1; + FirstIndex = NextIndex = 0; CreateRemoveContainers(); } @@ -111,7 +105,7 @@ namespace Avalonia.Controls.Presenters if (!panel.IsFull && Items != null) { - var index = LastIndex + 1; + var index = NextIndex; var items = Items.Cast().Skip(index); var memberSelector = Owner.MemberSelector; @@ -126,7 +120,7 @@ namespace Avalonia.Controls.Presenters } } - LastIndex = index - 1; + NextIndex = index; } if (panel.OverflowCount > 0) @@ -137,7 +131,7 @@ namespace Avalonia.Controls.Presenters panel.Children.RemoveRange(index, count); generator.Dematerialize(FirstIndex + index, count); - LastIndex -= count; + NextIndex -= count; } } @@ -156,6 +150,7 @@ namespace Avalonia.Controls.Presenters { var oldItemIndex = FirstIndex + first + i; var newItemIndex = oldItemIndex + delta + ((panel.Children.Count - count) * sign); + var item = Items.ElementAt(newItemIndex); if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector)) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index afb691d2e5..eccada7796 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -30,6 +30,16 @@ namespace Avalonia.Controls double IVirtualizingPanel.AverageItemSize => _averageItemSize; + double IVirtualizingPanel.PixelOverflow + { + get + { + var bounds = Orientation == Orientation.Horizontal ? + Bounds.Width : Bounds.Height; + return Math.Max(0, (_takenSpace - _pixelOffset) - bounds); + } + } + double IVirtualizingPanel.PixelOffset { get { return _pixelOffset; } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 9a7f53def0..09143215d2 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -213,23 +213,32 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Moving_To_And_From_The_End_With_Partial_Item_Should_Set_Panel_PixelOffset() { - var target = CreateTarget(); + var target = CreateTarget(itemCount: 20); target.ApplyTemplate(); target.Measure(new Size(100, 95)); target.Arrange(new Rect(0, 0, 100, 95)); - ((ILogicalScrollable)target).Offset = new Vector(0, 91); + ((ILogicalScrollable)target).Offset = new Vector(0, 11); var minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); - Assert.Equal(90, minIndex); - Assert.Equal(6, ((IVirtualizingPanel)target.Panel).PixelOffset); + Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset); + Assert.Equal(10, minIndex); + Assert.Equal(5, ((IVirtualizingPanel)target.Panel).PixelOffset); - ((ILogicalScrollable)target).Offset = new Vector(0, 90); + ((ILogicalScrollable)target).Offset = new Vector(0, 10); minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); - Assert.Equal(90, minIndex); + Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); + Assert.Equal(10, minIndex); Assert.Equal(0, ((IVirtualizingPanel)target.Panel).PixelOffset); + + ((ILogicalScrollable)target).Offset = new Vector(0, 11); + + minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); + Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset); + Assert.Equal(10, minIndex); + Assert.Equal(5, ((IVirtualizingPanel)target.Panel).PixelOffset); } public class WithContainers