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 11 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> /// </summary>
double AverageItemSize { get; } 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> /// <summary>
/// Gets or sets the current pixel offset of the items in the direction of scroll. /// Gets or sets the current pixel offset of the items in the direction of scroll.
/// </summary> /// </summary>

28
src/Avalonia.Controls/Presenters/ItemVirtualizer.cs

@ -5,6 +5,7 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Specialized; using System.Collections.Specialized;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Controls.Presenters namespace Avalonia.Controls.Presenters
@ -19,14 +20,32 @@ namespace Avalonia.Controls.Presenters
public ItemsPresenter Owner { get; } public ItemsPresenter Owner { get; }
public IVirtualizingPanel VirtualizingPanel => Owner.Panel as IVirtualizingPanel; public IVirtualizingPanel VirtualizingPanel => Owner.Panel as IVirtualizingPanel;
public IEnumerable Items { get; private set; } public IEnumerable Items { get; private set; }
public int ItemCount { get; private set; }
public int FirstIndex { get; 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 bool IsLogicalScrollEnabled { get; }
public abstract Size Extent { get; } public abstract double ExtentValue { get; }
public abstract Vector Offset { get; set; } public abstract double OffsetValue { get; set; }
public abstract Size Viewport { get; } 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) public static ItemVirtualizer Create(ItemsPresenter owner)
{ {
var virtualizingPanel = owner.Panel as IVirtualizingPanel; var virtualizingPanel = owner.Panel as IVirtualizingPanel;
@ -54,6 +73,7 @@ namespace Avalonia.Controls.Presenters
public virtual void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) public virtual void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e)
{ {
Items = items; 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 bool IsLogicalScrollEnabled => false;
public override Size Extent public override double ExtentValue
{ {
get { throw new NotSupportedException(); } get { throw new NotSupportedException(); }
} }
public override Vector Offset public override double OffsetValue
{ {
get { throw new NotSupportedException(); } get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); } set { throw new NotSupportedException(); }
} }
public override Size Viewport public override double ViewportValue
{ {
get { throw new NotSupportedException(); } 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 bool IsLogicalScrollEnabled => true;
public override Size Extent public override double ExtentValue => ItemCount;
{
get
{
if (VirtualizingPanel.ScrollDirection == Orientation.Vertical)
{
return new Size(0, Items.Count());
}
else
{
return new Size(Items.Count(), 0);
}
}
}
public override Vector Offset public override double OffsetValue
{ {
get get
{ {
if (VirtualizingPanel.ScrollDirection == Orientation.Vertical) var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0;
{ return FirstIndex + offset;
return new Vector(0, FirstIndex);
}
else
{
return new Vector(FirstIndex, 0);
}
} }
set set
{ {
var scroll = (VirtualizingPanel.ScrollDirection == Orientation.Vertical) ? var panel = VirtualizingPanel;
value.Y : value.X; var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0;
var delta = (int)(scroll - FirstIndex); var delta = (int)(value - (FirstIndex + offset));
if (delta != 0) if (delta != 0)
{ {
RecycleContainers(delta); if ((NextIndex - 1) + delta < ItemCount)
FirstIndex += delta; {
LastIndex += delta; 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 get
{ {
var panel = VirtualizingPanel; // If we can't fit the last item in the panel fully, subtract 1 from the viewport.
var overflow = VirtualizingPanel.PixelOverflow > 0 ? 1 : 0;
if (panel.ScrollDirection == Orientation.Vertical) return VirtualizingPanel.Children.Count - overflow;
{
return new Size(0, panel.Children.Count);
}
else
{
return new Size(panel.Children.Count, 0);
}
} }
} }
@ -96,8 +91,7 @@ namespace Avalonia.Controls.Presenters
// Reset indicates a large change and should (?) be quite rare. // Reset indicates a large change and should (?) be quite rare.
VirtualizingPanel.Children.Clear(); VirtualizingPanel.Children.Clear();
Owner.ItemContainerGenerator.Clear(); Owner.ItemContainerGenerator.Clear();
FirstIndex = 0; FirstIndex = NextIndex = 0;
LastIndex = -1;
CreateRemoveContainers(); CreateRemoveContainers();
} }
@ -111,7 +105,7 @@ namespace Avalonia.Controls.Presenters
if (!panel.IsFull && Items != null) if (!panel.IsFull && Items != null)
{ {
var index = LastIndex + 1; var index = NextIndex;
var items = Items.Cast<object>().Skip(index); var items = Items.Cast<object>().Skip(index);
var memberSelector = Owner.MemberSelector; var memberSelector = Owner.MemberSelector;
@ -126,7 +120,7 @@ namespace Avalonia.Controls.Presenters
} }
} }
LastIndex = index - 1; NextIndex = index;
} }
if (panel.OverflowCount > 0) if (panel.OverflowCount > 0)
@ -137,7 +131,7 @@ namespace Avalonia.Controls.Presenters
panel.Children.RemoveRange(index, count); panel.Children.RemoveRange(index, count);
generator.Dematerialize(FirstIndex + 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 oldItemIndex = FirstIndex + first + i;
var newItemIndex = oldItemIndex + delta + ((panel.Children.Count - count) * sign); var newItemIndex = oldItemIndex + delta + ((panel.Children.Count - count) * sign);
var item = Items.ElementAt(newItemIndex); var item = Items.ElementAt(newItemIndex);
if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector)) 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.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 double IVirtualizingPanel.PixelOffset
{ {
get { return _pixelOffset; } get { return _pixelOffset; }

21
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs

@ -213,23 +213,32 @@ namespace Avalonia.Controls.UnitTests.Presenters
[Fact] [Fact]
public void Moving_To_And_From_The_End_With_Partial_Item_Should_Set_Panel_PixelOffset() 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.ApplyTemplate();
target.Measure(new Size(100, 95)); target.Measure(new Size(100, 95));
target.Arrange(new Rect(0, 0, 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); var minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index);
Assert.Equal(90, minIndex); Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset);
Assert.Equal(6, ((IVirtualizingPanel)target.Panel).PixelOffset); 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); 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); 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 public class WithContainers

Loading…
Cancel
Save