diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index b95511e635..b45ed8065b 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -23,6 +23,7 @@ namespace Avalonia.Controls.Presenters public abstract bool IsLogicalScrollEnabled { get; } public abstract Size Extent { get; } + public abstract Vector Offset { get; set; } public abstract Size Viewport { get; } public static ItemVirtualizer Create(ItemsPresenter owner) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index c5ccb2ec0b..919bde7e0f 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -21,18 +21,18 @@ namespace Avalonia.Controls.Presenters public override Size Extent { - get - { - throw new NotSupportedException(); - } + get { throw new NotSupportedException(); } + } + + public override Vector Offset + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } } public override Size Viewport { - get - { - throw new NotSupportedException(); - } + get { throw new NotSupportedException(); } } public override void Arranging(Size finalSize) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index fb40960778..722bd9c15d 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -31,6 +31,75 @@ namespace Avalonia.Controls.Presenters } } + public override Vector Offset + { + get + { + if (VirtualizingPanel.ScrollDirection == Orientation.Vertical) + { + return new Vector(0, FirstIndex); + } + else + { + return new Vector(FirstIndex, 0); + } + } + + set + { + var scroll = (VirtualizingPanel.ScrollDirection == Orientation.Vertical) ? + value.Y : value.X; + var delta = (int)(scroll - FirstIndex); + var panel = VirtualizingPanel; + + if (delta != 0) + { + if (delta >= panel.Children.Count) + { + var index = FirstIndex + delta; + + foreach (var container in panel.Children) + { + container.DataContext = Items.ElementAt(index++); + } + } + else if (delta > 0) + { + var containers = panel.Children.GetRange(0, delta).ToList(); + panel.Children.RemoveRange(0, delta); + + var index = LastIndex + 1; + + foreach (var container in containers) + { + container.DataContext = Items.ElementAt(index++); + } + + panel.Children.AddRange(containers); + } + else + { + var first = panel.Children.Count + delta; + var count = -delta; + var containers = panel.Children.GetRange(first, count).ToList(); + panel.Children.RemoveRange(first, count); + + var index = FirstIndex + delta; + + foreach (var container in containers) + { + container.DataContext = Items.ElementAt(index++); + } + + panel.Children.InsertRange(0, containers); + } + + FirstIndex += delta; + LastIndex += delta; + } + } + } + public override Size Viewport { get diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index fa05d1bbd0..fc20add96c 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -2,14 +2,10 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections; -using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; -using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Utils; using Avalonia.Input; +using static Avalonia.Utilities.MathUtilities; namespace Avalonia.Controls.Presenters { @@ -60,7 +56,11 @@ namespace Avalonia.Controls.Presenters Size IScrollable.Extent => _virtualizer.Extent; /// - Vector IScrollable.Offset { get; set; } + Vector IScrollable.Offset + { + get { return _virtualizer.Offset; } + set { _virtualizer.Offset = CoerceOffset(value); } + } /// Size IScrollable.Viewport => _virtualizer.Viewport; @@ -83,6 +83,7 @@ namespace Avalonia.Controls.Presenters protected override void PanelCreated(IPanel panel) { _virtualizer = ItemVirtualizer.Create(this); + ((IScrollable)this).InvalidateScroll?.Invoke(); if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty)) { @@ -100,5 +101,13 @@ namespace Avalonia.Controls.Presenters { _virtualizer?.ItemsChanged(Items, e); } + + private Vector CoerceOffset(Vector value) + { + var scrollable = (IScrollable)this; + var maxX = Math.Max(scrollable.Extent.Width - scrollable.Viewport.Width, 0); + var maxY = Math.Max(scrollable.Extent.Height - scrollable.Viewport.Height, 0); + return new Vector(Clamp(value.X, 0, maxX), Clamp(value.Y, 0, maxY)); + } } } \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 097ce08171..fe56cd95ac 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -182,6 +182,68 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(8, target.Panel.Children.Count); } + + [Fact] + public void Scrolling_Less_Than_A_Page_Should_Move_Recycled_Items() + { + var target = CreateTarget(); + var items = (IList)target.Items; + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var containers = target.Panel.Children.ToList(); + var scroller = (ScrollContentPresenter)target.Parent; + + scroller.Offset = new Vector(0, 5); + + var scrolledContainers = containers + .Skip(5) + .Take(5) + .Concat(containers.Take(5)).ToList(); + + Assert.Equal(new Vector(0, 5), ((IScrollable)target).Offset); + Assert.Equal(scrolledContainers, target.Panel.Children); + + for (var i = 0; i < target.Panel.Children.Count; ++i) + { + Assert.Equal(items[i + 5], target.Panel.Children[i].DataContext); + } + + scroller.Offset = new Vector(0, 0); + Assert.Equal(new Vector(0, 0), ((IScrollable)target).Offset); + Assert.Equal(containers, target.Panel.Children); + + for (var i = 0; i < target.Panel.Children.Count; ++i) + { + Assert.Equal(items[i], target.Panel.Children[i].DataContext); + } + } + + [Fact] + public void Scrolling_More_Than_A_Page_Should_Recycle_Items() + { + var target = CreateTarget(); + var items = (IList)target.Items; + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var containers = target.Panel.Children.ToList(); + var scroller = (ScrollContentPresenter)target.Parent; + + scroller.Offset = new Vector(0, 10); + + Assert.Equal(new Vector(0, 10), ((IScrollable)target).Offset); + Assert.Equal(containers, target.Panel.Children); + + for (var i = 0; i < target.Panel.Children.Count; ++i) + { + Assert.Equal(items[i + 10], target.Panel.Children[i].DataContext); + } + } } private static ItemsPresenter CreateTarget(