Browse Source

More work on virtualization with INCC.

Got removes working a bit better.
pull/558/head
Steven Kirk 10 years ago
parent
commit
72ea9f02c7
  1. 169
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  2. 123
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs

169
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@ -47,18 +47,14 @@ namespace Avalonia.Controls.Presenters
if (delta != 0) if (delta != 0)
{ {
RecycleMoveContainers(delta); RecycleContainersForMove(delta);
FirstIndex += delta;
NextIndex += delta;
} }
} }
else else
{ {
// We're moving to a partially obscured item at the end of the list. // We're moving to a partially obscured item at the end of the list.
var firstIndex = ItemCount - panel.Children.Count; var firstIndex = ItemCount - panel.Children.Count;
RecycleMoveContainers(firstIndex - FirstIndex); RecycleContainersForMove(firstIndex - FirstIndex);
NextIndex = ItemCount;
FirstIndex = NextIndex - panel.Children.Count;
panel.PixelOffset = VirtualizingPanel.PixelOverflow; panel.PixelOffset = VirtualizingPanel.PixelOverflow;
} }
} }
@ -77,7 +73,7 @@ namespace Avalonia.Controls.Presenters
public override void Arranging(Size finalSize) public override void Arranging(Size finalSize)
{ {
CreateRemoveContainers(); CreateAndRemoveContainers();
((ILogicalScrollable)Owner).InvalidateScroll(); ((ILogicalScrollable)Owner).InvalidateScroll();
} }
@ -85,73 +81,44 @@ namespace Avalonia.Controls.Presenters
{ {
base.ItemsChanged(items, e); base.ItemsChanged(items, e);
switch (e.Action) if (items != null)
{ {
case NotifyCollectionChangedAction.Remove: switch (e.Action)
if(e.OldStartingIndex >= FirstIndex && {
e.OldStartingIndex + e.OldItems.Count <= NextIndex) case NotifyCollectionChangedAction.Add:
{ if (e.NewStartingIndex >= FirstIndex &&
if (e.OldStartingIndex == FirstIndex) e.NewStartingIndex + e.NewItems.Count <= NextIndex)
{ {
// We are removing the first in the list. CreateAndRemoveContainers();
VirtualizingPanel.Children.RemoveAt(e.OldStartingIndex - FirstIndex);
Owner.ItemContainerGenerator.Dematerialize(e.OldStartingIndex - FirstIndex, 1);
FirstIndex++; // This may not be necessary, but cant get to work without this.
// If all items are visible we need to reduce the NextIndex too.
if(NextIndex > ItemCount)
{
NextIndex = ItemCount;
}
CreateRemoveContainers();
RecycleContainers(); RecycleContainers();
} }
else if (e.OldStartingIndex + e.OldItems.Count == NextIndex)
{
// We are removing the last one in the list.
VirtualizingPanel.Children.RemoveAt(e.OldStartingIndex - FirstIndex);
Owner.ItemContainerGenerator.Dematerialize(e.OldStartingIndex - FirstIndex, 1);
NextIndex--;
}
else
{
// If all items are visible we need to reduce the NextIndex too.
if (NextIndex > ItemCount)
{
NextIndex = ItemCount;
}
CreateRemoveContainers(); break;
RecycleContainers();
}
}
break;
case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Remove:
if (e.NewStartingIndex >= FirstIndex && if (e.OldStartingIndex >= FirstIndex &&
e.NewStartingIndex + e.NewItems.Count < NextIndex) e.OldStartingIndex + e.OldItems.Count <= NextIndex)
{ {
CreateRemoveContainers(); RecycleContainersOnRemove();
RecycleContainers(); }
}
break; break;
case NotifyCollectionChangedAction.Reset: case NotifyCollectionChangedAction.Reset:
// We could recycle items here if this proves to be inefficient, but RecycleContainersOnRemove();
// Reset indicates a large change and should (?) be quite rare. break;
VirtualizingPanel.Children.Clear(); }
Owner.ItemContainerGenerator.Clear(); }
FirstIndex = NextIndex = 0; else
CreateRemoveContainers(); {
break; Owner.ItemContainerGenerator.Clear();
VirtualizingPanel.Children.Clear();
} }
((ILogicalScrollable)Owner).InvalidateScroll(); ((ILogicalScrollable)Owner).InvalidateScroll();
} }
private void CreateRemoveContainers() private void CreateAndRemoveContainers()
{ {
var generator = Owner.ItemContainerGenerator; var generator = Owner.ItemContainerGenerator;
var panel = VirtualizingPanel; var panel = VirtualizingPanel;
@ -164,8 +131,12 @@ namespace Avalonia.Controls.Presenters
while (!panel.IsFull) while (!panel.IsFull)
{ {
if (index == ItemCount) if (index >= ItemCount)
{ {
// We can fit more containers in the panel, but we're at the end of the
// items. If we're scrolled to the top (FirstIndex == 0), then there are
// no more items to create. Otherwise, go backwards adding containers to
// the beginning of the panel.
if (FirstIndex == 0) if (FirstIndex == 0)
{ {
break; break;
@ -204,13 +175,7 @@ namespace Avalonia.Controls.Presenters
if (panel.OverflowCount > 0) if (panel.OverflowCount > 0)
{ {
var count = panel.OverflowCount; RemoveContainers(panel.OverflowCount);
var index = panel.Children.Count - count;
panel.Children.RemoveRange(index, count);
generator.Dematerialize(FirstIndex + index, count);
NextIndex -= count;
} }
} }
@ -224,29 +189,21 @@ namespace Avalonia.Controls.Presenters
foreach (var container in containers) foreach (var container in containers)
{ {
if (itemIndex < ItemCount) var item = Items.ElementAt(itemIndex);
{
var item = Items.ElementAt(itemIndex);
if (!object.Equals(container.Item, item)) if (!object.Equals(container.Item, item))
{
if (!generator.TryRecycle(itemIndex, itemIndex, item, selector))
{ {
if (!generator.TryRecycle(itemIndex, itemIndex, item, selector)) throw new NotImplementedException();
{
throw new NotImplementedException();
}
} }
} }
else
{
panel.Children.RemoveAt(panel.Children.Count - 1);
}
++itemIndex; ++itemIndex;
} }
} }
private void RecycleMoveContainers(int delta) private void RecycleContainersForMove(int delta)
{ {
var panel = VirtualizingPanel; var panel = VirtualizingPanel;
var generator = Owner.ItemContainerGenerator; var generator = Owner.ItemContainerGenerator;
@ -283,6 +240,52 @@ namespace Avalonia.Controls.Presenters
panel.Children.InsertRange(0, containers); panel.Children.InsertRange(0, containers);
} }
} }
FirstIndex += delta;
NextIndex += delta;
}
private void RecycleContainersOnRemove()
{
var panel = VirtualizingPanel;
if (NextIndex <= ItemCount)
{
// Items have been removed but FirstIndex..NextIndex is still a valid range in the
// items, so just recycle the containers to adapt to the new state.
RecycleContainers();
}
else
{
// Items have been removed and now the range FirstIndex..NextIndex goes out of
// the item bounds. Try to scroll up and then remove any excess containers.
var newFirstIndex = Math.Max(0, FirstIndex - (NextIndex - ItemCount));
var delta = newFirstIndex - FirstIndex;
var newNextIndex = NextIndex + delta;
if (newNextIndex > ItemCount)
{
RemoveContainers(newNextIndex - ItemCount);
}
if (delta != 0)
{
RecycleContainersForMove(delta);
}
else
{
RecycleContainers();
}
}
}
private void RemoveContainers(int count)
{
var index = VirtualizingPanel.Children.Count - count;
VirtualizingPanel.Children.RemoveRange(index, count);
Owner.ItemContainerGenerator.Dematerialize(FirstIndex + index, count);
NextIndex -= count;
} }
} }
} }

123
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs

@ -1,9 +1,11 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
@ -76,7 +78,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
} }
[Fact] [Fact]
public void Should_Add_New_Items_At_Top_When_Control_Is_Scrolled_To_Bottom_And_Enlarged() public void Should_Add_New_Containers_At_Top_When_Control_Is_Scrolled_To_Bottom_And_Enlarged()
{ {
var target = CreateTarget(); var target = CreateTarget();
var items = (IList<string>)target.Items; var items = (IList<string>)target.Items;
@ -161,107 +163,140 @@ namespace Avalonia.Controls.UnitTests.Presenters
var target = CreateTarget(itemCount: 20); var target = CreateTarget(itemCount: 20);
target.ApplyTemplate(); target.ApplyTemplate();
target.Measure(new Size(100, 95)); target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 95)); target.Arrange(new Rect(0, 0, 100, 100));
((ILogicalScrollable)target).Offset = new Vector(0, 5); ((ILogicalScrollable)target).Offset = new Vector(0, 5);
var expected = Enumerable.Range(5, 10).Select(x => $"Item {x}").ToList(); var expected = Enumerable.Range(5, 10).Select(x => $"Item {x}").ToList();
var items = (ObservableCollection<string>)target.Items; var items = (ObservableCollection<string>)target.Items;
var actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal( Assert.Equal(expected, actual);
expected,
target.Panel.Children.Select(x => x.DataContext));
items.Insert(6, "Inserted"); items.Insert(6, "Inserted");
expected.Insert(1, "Inserted"); expected.Insert(1, "Inserted");
expected.RemoveAt(expected.Count - 1); expected.RemoveAt(expected.Count - 1);
var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
[Fact] [Fact]
public void Removing_First_Item_When_Visible_Should_UpdateContainers() public void Removing_First_Materialized_Item_Should_Update_Containers()
{ {
var target = CreateTarget(itemCount: 20); var target = CreateTarget(itemCount: 20);
target.ApplyTemplate(); target.ApplyTemplate();
target.Measure(new Size(100, 195)); target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 195)); target.Arrange(new Rect(0, 0, 100, 100));
((ILogicalScrollable)target).Offset = new Vector(0, 5);
var expected = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(); var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList();
var items = (ObservableCollection<string>)target.Items; var items = (ObservableCollection<string>)target.Items;
var actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal( Assert.Equal(expected, actual);
expected,
target.Panel.Children.Select(x => x.DataContext));
items.Remove(items.First()); items.RemoveAt(0);
expected.Remove(expected.First()); expected = Enumerable.Range(1, 10).Select(x => $"Item {x}").ToList();
var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
[Fact] [Fact]
public void Removing_Items_From_Middle_Should_Update_Containers() public void Removing_Items_From_Middle_Should_Update_Containers_When_All_Items_Visible()
{ {
var target = CreateTarget(itemCount: 20); var target = CreateTarget(itemCount: 20);
target.ApplyTemplate(); target.ApplyTemplate();
target.Measure(new Size(100, 195)); target.Measure(new Size(100, 200));
target.Arrange(new Rect(0, 0, 100, 195)); target.Arrange(new Rect(0, 0, 100, 200));
((ILogicalScrollable)target).Offset = new Vector(0, 5);
var expected = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList();
var items = (ObservableCollection<string>)target.Items; var items = (ObservableCollection<string>)target.Items;
var actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal( Assert.Equal(items, actual);
expected,
target.Panel.Children.Select(x => x.DataContext));
items.RemoveAt(2); items.RemoveAt(2);
expected.RemoveAt(2);
var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual); Assert.Equal(items, actual);
items.RemoveAt(items.Count - 2); items.RemoveAt(items.Count - 2);
expected.RemoveAt(expected.Count -2);
actual = target.Panel.Children.Select(x => x.DataContext).ToList(); actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual); Assert.Equal(items, actual);
} }
[Fact] [Fact]
public void Removing_Last_Item_When_Visible_Should_UpdateContainers() public void Removing_Last_Item_Should_Update_Containers_When_All_Items_Visible()
{ {
var target = CreateTarget(itemCount: 20); var target = CreateTarget(itemCount: 20);
target.ApplyTemplate(); target.ApplyTemplate();
target.Measure(new Size(100, 195)); target.Measure(new Size(100, 200));
target.Arrange(new Rect(0, 0, 100, 195)); target.Arrange(new Rect(0, 0, 100, 200));
((ILogicalScrollable)target).Offset = new Vector(0, 5); ((ILogicalScrollable)target).Offset = new Vector(0, 5);
var expected = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(); var expected = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList();
var items = (ObservableCollection<string>)target.Items; var items = (ObservableCollection<string>)target.Items;
var actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal( Assert.Equal(expected, actual);
expected,
target.Panel.Children.Select(x => x.DataContext));
items.Remove(items.Last()); items.Remove(items.Last());
expected.Remove(expected.Last()); expected.Remove(expected.Last());
actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
}
[Fact]
public void Removing_Items_When_Scrolled_To_End_Should_Add_Containers_At_Top()
{
var target = CreateTarget(itemCount: 20, useAvaloniaList: true);
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
((ILogicalScrollable)target).Offset = new Vector(0, 10);
var expected = Enumerable.Range(10, 10).Select(x => $"Item {x}").ToList();
var items = (AvaloniaList<string>)target.Items;
var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); var actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
items.RemoveRange(18, 2);
expected = Enumerable.Range(8, 10).Select(x => $"Item {x}").ToList();
actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
[Fact]
public void Setting_Items_To_Null_Should_Remove_Containers()
{
var target = CreateTarget(itemCount: 20);
target.ApplyTemplate();
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList();
var items = (ObservableCollection<string>)target.Items;
var actual = target.Panel.Children.Select(x => x.DataContext).ToList();
Assert.Equal(expected, actual);
target.Items = null;
Assert.Empty(target.Panel.Children);
}
public class WithContainers public class WithContainers
{ {
[Fact] [Fact]
@ -342,12 +377,14 @@ namespace Avalonia.Controls.UnitTests.Presenters
private static ItemsPresenter CreateTarget( private static ItemsPresenter CreateTarget(
Orientation orientation = Orientation.Vertical, Orientation orientation = Orientation.Vertical,
bool useContainers = true, bool useContainers = true,
int itemCount = 20) int itemCount = 20,
bool useAvaloniaList = false)
{ {
ItemsPresenter result; ItemsPresenter result;
var itemsSource = Enumerable.Range(0, itemCount).Select(x => $"Item {x}");
var items = new ObservableCollection<string>( var items = useAvaloniaList ?
Enumerable.Range(0, itemCount).Select(x => $"Item {x}")); (IEnumerable)new AvaloniaList<string>(itemsSource) :
(IEnumerable)new ObservableCollection<string>(itemsSource);
var scroller = new ScrollContentPresenter var scroller = new ScrollContentPresenter
{ {

Loading…
Cancel
Save