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