// 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 System;
using System.Collections;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Utils;
using Avalonia.Input;
namespace Avalonia.Controls.Presenters
{
///
/// Handles virtualization in an for
/// .
///
internal class ItemVirtualizerSimple : ItemVirtualizer
{
///
/// Initializes a new instance of the class.
///
///
public ItemVirtualizerSimple(ItemsPresenter owner)
: base(owner)
{
}
///
public override bool IsLogicalScrollEnabled => true;
///
public override double ExtentValue => ItemCount;
///
public override double OffsetValue
{
get
{
var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0;
return FirstIndex + offset;
}
set
{
var panel = VirtualizingPanel;
var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0;
var delta = (int)(value - (FirstIndex + offset));
if (delta != 0)
{
var newLastIndex = (NextIndex - 1) + delta;
if (newLastIndex < ItemCount)
{
if (panel.PixelOffset > 0)
{
panel.PixelOffset = 0;
delta += 1;
}
if (delta != 0)
{
RecycleContainersForMove(delta);
}
}
else
{
// We're moving to a partially obscured item at the end of the list so
// offset the panel by the height of the first item.
var firstIndex = ItemCount - panel.Children.Count;
RecycleContainersForMove(firstIndex - FirstIndex);
panel.PixelOffset = panel.Children[0].Bounds.Height;
}
}
}
}
///
public override double ViewportValue
{
get
{
// 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;
}
}
///
public override void UpdateControls()
{
CreateAndRemoveContainers();
InvalidateScroll();
}
///
public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e)
{
base.ItemsChanged(items, e);
if (items != null)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
if (e.NewStartingIndex >= FirstIndex &&
e.NewStartingIndex + e.NewItems.Count <= NextIndex)
{
CreateAndRemoveContainers();
RecycleContainers();
}
break;
case NotifyCollectionChangedAction.Remove:
if (e.OldStartingIndex >= FirstIndex &&
e.OldStartingIndex + e.OldItems.Count <= NextIndex)
{
RecycleContainersOnRemove();
}
break;
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace:
RecycleContainers();
break;
case NotifyCollectionChangedAction.Reset:
RecycleContainersOnRemove();
break;
}
}
else
{
Owner.ItemContainerGenerator.Clear();
VirtualizingPanel.Children.Clear();
}
InvalidateScroll();
}
public override IControl GetControlInDirection(FocusNavigationDirection direction, IControl from)
{
var generator = Owner.ItemContainerGenerator;
var panel = VirtualizingPanel;
var itemIndex = generator.IndexFromContainer(from);
if (itemIndex == -1)
{
return null;
}
var newItemIndex = -1;
if (VirtualizingPanel.ScrollDirection == Orientation.Vertical)
{
switch (direction)
{
case FocusNavigationDirection.Up:
newItemIndex = itemIndex - 1;
break;
case FocusNavigationDirection.Down:
newItemIndex = itemIndex + 1;
break;
}
}
if (newItemIndex >= 0 && newItemIndex < ItemCount)
{
// Get the index of the first and last fully visible items (i.e. excluding any
// partially visible item at the beginning or end).
var firstIndex = panel.PixelOffset == 0 ? FirstIndex : FirstIndex + 1;
var lastIndex = (FirstIndex + ViewportValue) - 1;
if (newItemIndex < firstIndex || newItemIndex > lastIndex)
{
OffsetValue += newItemIndex - itemIndex;
InvalidateScroll();
}
return generator.ContainerFromIndex(newItemIndex);
}
return null;
}
///
/// Creates and removes containers such that we have at most enough containers to fill
/// the panel.
///
private void CreateAndRemoveContainers()
{
var generator = Owner.ItemContainerGenerator;
var panel = VirtualizingPanel;
if (!panel.IsFull && Items != null)
{
var memberSelector = Owner.MemberSelector;
var index = NextIndex;
var step = 1;
while (!panel.IsFull)
{
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)
{
break;
}
else
{
index = FirstIndex - 1;
step = -1;
}
}
var materialized = generator.Materialize(index, Items.ElementAt(index), memberSelector);
if (step == 1)
{
panel.Children.Add(materialized.ContainerControl);
}
else
{
panel.Children.Insert(0, materialized.ContainerControl);
}
index += step;
}
if (step == 1)
{
NextIndex = index;
}
else
{
NextIndex = ItemCount;
FirstIndex = index + 1;
}
}
if (panel.OverflowCount > 0)
{
RemoveContainers(panel.OverflowCount);
}
}
///
/// Updates the containers in the panel to make sure they are displaying the correct item
/// based on .
///
///
/// This method requires that + the number of
/// materialized containers is not more than .
///
private void RecycleContainers()
{
var panel = VirtualizingPanel;
var generator = Owner.ItemContainerGenerator;
var selector = Owner.MemberSelector;
var containers = generator.Containers.ToList();
var itemIndex = FirstIndex;
foreach (var container in containers)
{
var item = Items.ElementAt(itemIndex);
if (!object.Equals(container.Item, item))
{
if (!generator.TryRecycle(itemIndex, itemIndex, item, selector))
{
throw new NotImplementedException();
}
}
++itemIndex;
}
}
///
/// Recycles containers when a move occurs.
///
/// The delta of the move.
///
/// If the move is less than a page, then this method moves the containers for the items
/// that are still visible to the correct place, and recyles and moves the others. For
/// example: if there are 20 items and 10 containers visible and the user scrolls 5
/// items down, then the bottom 5 containers will be moved to the top and the top 5 will
/// be moved to the bottom and recycled to display the newly visible item. Updates
/// and
/// with their new values.
///
private void RecycleContainersForMove(int delta)
{
var panel = VirtualizingPanel;
var generator = Owner.ItemContainerGenerator;
var selector = Owner.MemberSelector;
var sign = delta < 0 ? -1 : 1;
var count = Math.Min(Math.Abs(delta), panel.Children.Count);
var move = count < panel.Children.Count;
var first = delta < 0 && move ? panel.Children.Count + delta : 0;
var containers = panel.Children.GetRange(first, count).ToList();
for (var i = 0; i < count; ++i)
{
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))
{
throw new NotImplementedException();
}
}
if (move)
{
if (delta > 0)
{
panel.Children.MoveRange(first, count, panel.Children.Count);
}
else
{
panel.Children.MoveRange(first, count, 0);
}
}
FirstIndex += delta;
NextIndex += delta;
}
///
/// Recycles containers due to items being removed.
///
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. Remove any excess containers, try to scroll up and then recycle
// the containers to make sure they point to the correct item.
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);
}
RecycleContainers();
}
}
///
/// Removes the specified number of containers from the end of the panel and updates
/// .
///
/// The number of containers to remove.
private void RemoveContainers(int count)
{
var index = VirtualizingPanel.Children.Count - count;
VirtualizingPanel.Children.RemoveRange(index, count);
Owner.ItemContainerGenerator.Dematerialize(FirstIndex + index, count);
NextIndex -= count;
}
}
}