Browse Source

Correctly handle partially obscured items.

And move logical for selecting horizontal/vertical components of
extent/offset/viewport into `ItemVirtualizer` base class.
pull/554/head
Steven Kirk 10 years ago
parent
commit
7f09154020
  1. 10
      src/Avalonia.Controls/IVirtualizingPanel.cs
  2. 28
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  3. 6
      src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
  4. 85
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  5. 10
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  6. 21
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs

10
src/Avalonia.Controls/IVirtualizingPanel.cs

@ -30,6 +30,16 @@ namespace Avalonia.Controls
/// </summary>
double AverageItemSize { get; }
/// <summary>
/// Gets or sets a size in pixels by which the content is overflowing the panel, in the
/// direction of scroll.
/// </summary>
/// <remarks>
/// This may be non-zero even when <see cref="OverflowCount"/> is zero if the last item
/// overflows the panel bounds.
/// </remarks>
double PixelOverflow { get; }
/// <summary>
/// Gets or sets the current pixel offset of the items in the direction of scroll.
/// </summary>

28
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();
}
}
}

6
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(); }
}

85
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<object>().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))

10
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; }

21
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

Loading…
Cancel
Save