diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index 6ac9a83f89..f771a6b9fe 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -8,6 +8,10 @@ namespace Avalonia.Controls int OverflowCount { get; } + double AverageItemSize { get; } + + double PixelOffset { get; set; } + Action ArrangeCompleted { get; set; } } } diff --git a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs index a270896441..4cb4bcef54 100644 --- a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs @@ -17,6 +17,7 @@ namespace Avalonia.Controls.Presenters _panel = new VirtualizingStackPanel(); _panel.ArrangeCompleted = CheckPanel; Child = _panel; + CheckPanel(); } } @@ -24,28 +25,31 @@ namespace Avalonia.Controls.Presenters Action IScrollable.InvalidateScroll { get; set; } - Size IScrollable.Extent => new Size(1, 100); + Size IScrollable.Extent => new Size(1, 100 * AverageItemSize ); Vector IScrollable.Offset { get { - return new Vector(0, _firstIndex); + return new Vector(0, _firstIndex * AverageItemSize); } set { var count = _lastIndex - _firstIndex; - _firstIndex = (int)Math.Round(value.Y); + _firstIndex = (int)(value.Y / AverageItemSize); _lastIndex = _firstIndex + count; + _panel.PixelOffset = value.Y % AverageItemSize; Renumber(); } } - Size IScrollable.Viewport => new Size(1, _lastIndex - _firstIndex); + Size IScrollable.Viewport => new Size(1, (_lastIndex - _firstIndex) * AverageItemSize); Size IScrollable.ScrollSize => new Size(0, 1); Size IScrollable.PageScrollSize => new Size(0, 1); + private double AverageItemSize => _panel?.AverageItemSize ?? 1; + protected override Size ArrangeOverride(Size finalSize) { var result = base.ArrangeOverride(finalSize); diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index bc21a60aac..1364bb1250 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using Avalonia.Controls.Primitives; using System; using System.Collections.Specialized; @@ -11,6 +10,9 @@ namespace Avalonia.Controls { private double _takenSpace; private int _canBeRemoved; + private double _averageItemSize; + private int _averageCount; + private double _pixelOffset; bool IVirtualizingPanel.IsFull { @@ -24,12 +26,30 @@ namespace Avalonia.Controls int IVirtualizingPanel.OverflowCount => _canBeRemoved; + double IVirtualizingPanel.AverageItemSize => _averageItemSize; + + double IVirtualizingPanel.PixelOffset + { + get { return _pixelOffset; } + + set + { + if (_pixelOffset != value) + { + _pixelOffset = value; + InvalidateArrange(); + } + } + } + Action IVirtualizingPanel.ArrangeCompleted { get; set; } protected override Size ArrangeOverride(Size finalSize) { _canBeRemoved = 0; _takenSpace = 0; + _averageItemSize = 0; + _averageCount = 0; var result = base.ArrangeOverride(finalSize); ((IVirtualizingPanel)this).ArrangeCompleted?.Invoke(); return result; @@ -44,7 +64,7 @@ namespace Avalonia.Controls case NotifyCollectionChangedAction.Add: foreach (IControl control in e.NewItems) { - UpdatePhysicalSizeForAdd(control); + UpdateAdd(control); } break; @@ -52,7 +72,7 @@ namespace Avalonia.Controls case NotifyCollectionChangedAction.Remove: foreach (IControl control in e.OldItems) { - UpdatePhysicalSizeForRemove(control); + UpdateRemove(control); } break; @@ -65,64 +85,93 @@ namespace Avalonia.Controls Size panelSize, Orientation orientation) { - base.ArrangeChild(child, rect, panelSize, orientation); - - if (orientation == Orientation.Horizontal) + if (orientation == Orientation.Vertical) { - if (rect.X >= panelSize.Width) + rect = new Rect(rect.X, rect.Y - _pixelOffset, rect.Width, rect.Height); + child.Arrange(rect); + + if (rect.Y >= panelSize.Height) { ++_canBeRemoved; } - if (rect.Right >= _takenSpace) + if (rect.Bottom >= _takenSpace) { - _takenSpace = rect.Right; + _takenSpace = rect.Bottom; } + + AddToAverageItemSize(rect.Height); } else { - if (rect.Y >= panelSize.Height) + rect = new Rect(rect.X - _pixelOffset, rect.Y, rect.Width, rect.Height); + child.Arrange(rect); + + if (rect.X >= panelSize.Width) { ++_canBeRemoved; } - if (rect.Bottom >= _takenSpace) + if (rect.Right >= _takenSpace) { - _takenSpace = rect.Bottom; + _takenSpace = rect.Right; } + + AddToAverageItemSize(rect.Width); } } - private void UpdatePhysicalSizeForAdd(IControl child) + private void UpdateAdd(IControl child) { var bounds = Bounds; var gap = Gap; child.Measure(bounds.Size); + ++_averageCount; if (Orientation == Orientation.Vertical) { - _takenSpace += child.DesiredSize.Height + gap; + var height = child.DesiredSize.Height; + _takenSpace += height + gap; + AddToAverageItemSize(height); } else { - _takenSpace += child.DesiredSize.Width + gap; + var width = child.DesiredSize.Width; + _takenSpace += width + gap; + AddToAverageItemSize(width); } } - private void UpdatePhysicalSizeForRemove(IControl child) + private void UpdateRemove(IControl child) { var bounds = Bounds; var gap = Gap; if (Orientation == Orientation.Vertical) { - _takenSpace -= child.DesiredSize.Height + gap; + var height = child.DesiredSize.Height; + _takenSpace -= height + gap; + RemoveFromAverageItemSize(height); } else { - _takenSpace -= child.DesiredSize.Width + gap; + var width = child.DesiredSize.Width; + _takenSpace -= width + gap; + RemoveFromAverageItemSize(width); } } + + private void AddToAverageItemSize(double value) + { + ++_averageCount; + _averageItemSize += (value - _averageItemSize) / _averageCount; + } + + private void RemoveFromAverageItemSize(double value) + { + _averageItemSize = ((_averageItemSize * _averageCount) - value) / (_averageCount - 1); + --_averageCount; + } } }