| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388 |
- // 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
- {
- /// <summary>
- /// Handles virtualization in an <see cref="ItemsPresenter"/> for
- /// <see cref="ItemVirtualizationMode.Simple"/>.
- /// </summary>
- internal class ItemVirtualizerSimple : ItemVirtualizer
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="ItemVirtualizerSimple"/> class.
- /// </summary>
- /// <param name="owner"></param>
- public ItemVirtualizerSimple(ItemsPresenter owner)
- : base(owner)
- {
- }
- /// <inheritdoc/>
- public override bool IsLogicalScrollEnabled => true;
- /// <inheritdoc/>
- public override double ExtentValue => ItemCount;
- /// <inheritdoc/>
- 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;
- }
- }
- }
- }
- /// <inheritdoc/>
- 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;
- }
- }
- /// <inheritdoc/>
- public override void UpdateControls()
- {
- CreateAndRemoveContainers();
- InvalidateScroll();
- }
- /// <inheritdoc/>
- 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;
- }
- /// <summary>
- /// Creates and removes containers such that we have at most enough containers to fill
- /// the panel.
- /// </summary>
- 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);
- }
- }
- /// <summary>
- /// Updates the containers in the panel to make sure they are displaying the correct item
- /// based on <see cref="ItemVirtualizer.FirstIndex"/>.
- /// </summary>
- /// <remarks>
- /// This method requires that <see cref="ItemVirtualizer.FirstIndex"/> + the number of
- /// materialized containers is not more than <see cref="ItemVirtualizer.ItemCount"/>.
- /// </remarks>
- 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;
- }
- }
- /// <summary>
- /// Recycles containers when a move occurs.
- /// </summary>
- /// <param name="delta">The delta of the move.</param>
- /// <remarks>
- /// 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
- /// <see cref="ItemVirtualizer.FirstIndex"/> and <see cref="ItemVirtualizer.NextIndex"/>
- /// with their new values.
- /// </remarks>
- 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;
- }
- /// <summary>
- /// Recycles containers due to items being removed.
- /// </summary>
- 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();
- }
- }
- /// <summary>
- /// Removes the specified number of containers from the end of the panel and updates
- /// <see cref="ItemVirtualizer.NextIndex"/>.
- /// </summary>
- /// <param name="count">The number of containers to remove.</param>
- private void RemoveContainers(int count)
- {
- var index = VirtualizingPanel.Children.Count - count;
- VirtualizingPanel.Children.RemoveRange(index, count);
- Owner.ItemContainerGenerator.Dematerialize(FirstIndex + index, count);
- NextIndex -= count;
- }
- }
- }
|