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(