| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463 |
- // This source file is adapted from the WinUI project.
- // (https://github.com/microsoft/microsoft-ui-xaml)
- //
- // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
- using System;
- using System.Collections.Generic;
- using System.Collections.Specialized;
- using Avalonia.Layout.Utils;
- using Avalonia.Logging;
- namespace Avalonia.Layout
- {
- internal class ElementManager
- {
- private readonly List<ILayoutable> _realizedElements = new List<ILayoutable>();
- private readonly List<Rect> _realizedElementLayoutBounds = new List<Rect>();
- private int _firstRealizedDataIndex;
- private VirtualizingLayoutContext _context;
- private bool IsVirtualizingContext
- {
- get
- {
- if (_context != null)
- {
- var rect = _context.RealizationRect;
- bool hasInfiniteSize = double.IsInfinity(rect.Height) || double.IsInfinity(rect.Width);
- return !hasInfiniteSize;
- }
- return false;
- }
- }
- public void SetContext(VirtualizingLayoutContext virtualContext) => _context = virtualContext;
- public void OnBeginMeasure(ScrollOrientation orientation)
- {
- if (_context != null)
- {
- if (IsVirtualizingContext)
- {
- // We proactively clear elements laid out outside of the realizaton
- // rect so that they are available for reuse during the current
- // measure pass.
- // This is useful during fast panning scenarios in which the realization
- // window is constantly changing and we want to reuse elements from
- // the end that's opposite to the panning direction.
- DiscardElementsOutsideWindow(_context.RealizationRect, orientation);
- }
- else
- {
- // If we are initialized with a non-virtualizing context, make sure that
- // we have enough space to hold the bounds for all the elements.
- int count = _context.ItemCount;
- if (_realizedElementLayoutBounds.Count != count)
- {
- // Make sure there is enough space for the bounds.
- // Note: We could optimize when the count becomes smaller, but keeping
- // it always up to date is the simplest option for now.
- _realizedElementLayoutBounds.Resize(count);
- }
- }
- }
- }
- public int GetRealizedElementCount()
- {
- return IsVirtualizingContext ? _realizedElements.Count : _context.ItemCount;
- }
- public ILayoutable GetAt(int realizedIndex)
- {
- ILayoutable element;
- if (IsVirtualizingContext)
- {
- if (_realizedElements[realizedIndex] == null)
- {
- // Sentinel. Create the element now since we need it.
- int dataIndex = GetDataIndexFromRealizedRangeIndex(realizedIndex);
- Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "Creating element for sentinal with data index {Index}", dataIndex);
- element = _context.GetOrCreateElementAt(
- dataIndex,
- ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
- _realizedElements[realizedIndex] = element;
- }
- else
- {
- element = _realizedElements[realizedIndex];
- }
- }
- else
- {
- // realizedIndex and dataIndex are the same (everything is realized)
- element = _context.GetOrCreateElementAt(
- realizedIndex,
- ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
- }
- return element;
- }
- public void Add(ILayoutable element, int dataIndex)
- {
- if (_realizedElements.Count == 0)
- {
- _firstRealizedDataIndex = dataIndex;
- }
- _realizedElements.Add(element);
- _realizedElementLayoutBounds.Add(default);
- }
- public void Insert(int realizedIndex, int dataIndex, ILayoutable element)
- {
- if (realizedIndex == 0)
- {
- _firstRealizedDataIndex = dataIndex;
- }
- _realizedElements.Insert(realizedIndex, element);
- // Set bounds to an invalid rect since we do not know it yet.
- _realizedElementLayoutBounds.Insert(realizedIndex, new Rect(-1, -1, -1, -1));
- }
- public void ClearRealizedRange(int realizedIndex, int count)
- {
- for (int i = 0; i < count; i++)
- {
- // Clear from the edges so that ItemsRepeater can optimize on maintaining
- // realized indices without walking through all the children every time.
- int index = realizedIndex == 0 ? realizedIndex + i : (realizedIndex + count - 1) - i;
- var elementRef = _realizedElements[index];
- if (elementRef != null)
- {
- _context.RecycleElement(elementRef);
- }
- }
- int endIndex = realizedIndex + count;
- _realizedElements.RemoveRange(realizedIndex, endIndex - realizedIndex);
- _realizedElementLayoutBounds.RemoveRange(realizedIndex, endIndex - realizedIndex);
- if (realizedIndex == 0)
- {
- _firstRealizedDataIndex = _realizedElements.Count == 0 ?
- -1 : _firstRealizedDataIndex + count;
- }
- }
- public void DiscardElementsOutsideWindow(bool forward, int startIndex)
- {
- // Remove layout elements that are outside the realized range.
- if (IsDataIndexRealized(startIndex))
- {
- int rangeIndex = GetRealizedRangeIndexFromDataIndex(startIndex);
- if (forward)
- {
- ClearRealizedRange(rangeIndex, GetRealizedElementCount() - rangeIndex);
- }
- else
- {
- ClearRealizedRange(0, rangeIndex + 1);
- }
- }
- }
- public void ClearRealizedRange() => ClearRealizedRange(0, GetRealizedElementCount());
- public Rect GetLayoutBoundsForDataIndex(int dataIndex)
- {
- int realizedIndex = GetRealizedRangeIndexFromDataIndex(dataIndex);
- return _realizedElementLayoutBounds[realizedIndex];
- }
- public void SetLayoutBoundsForDataIndex(int dataIndex, in Rect bounds)
- {
- int realizedIndex = GetRealizedRangeIndexFromDataIndex(dataIndex);
- _realizedElementLayoutBounds[realizedIndex] = bounds;
- }
- public Rect GetLayoutBoundsForRealizedIndex(int realizedIndex) => _realizedElementLayoutBounds[realizedIndex];
- public void SetLayoutBoundsForRealizedIndex(int realizedIndex, in Rect bounds)
- {
- _realizedElementLayoutBounds[realizedIndex] = bounds;
- }
- public bool IsDataIndexRealized(int index)
- {
- if (IsVirtualizingContext)
- {
- int realizedCount = GetRealizedElementCount();
- return
- realizedCount > 0 &&
- GetDataIndexFromRealizedRangeIndex(0) <= index &&
- GetDataIndexFromRealizedRangeIndex(realizedCount - 1) >= index;
- }
- else
- {
- // Non virtualized - everything is realized
- return index >= 0 && index < _context.ItemCount;
- }
- }
- public bool IsIndexValidInData(int currentIndex) => (uint)currentIndex < _context.ItemCount;
- public ILayoutable GetRealizedElement(int dataIndex)
- {
- return IsVirtualizingContext ?
- GetAt(GetRealizedRangeIndexFromDataIndex(dataIndex)) :
- _context.GetOrCreateElementAt(
- dataIndex,
- ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
- }
- public void EnsureElementRealized(bool forward, int dataIndex, string layoutId)
- {
- if (IsDataIndexRealized(dataIndex) == false)
- {
- var element = _context.GetOrCreateElementAt(
- dataIndex,
- ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
- if (forward)
- {
- Add(element, dataIndex);
- }
- else
- {
- Insert(0, dataIndex, element);
- }
- Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Created element for index {index}", layoutId, dataIndex);
- }
- }
- public bool IsWindowConnected(in Rect window, ScrollOrientation orientation, bool scrollOrientationSameAsFlow)
- {
- bool intersects = false;
- if (_realizedElementLayoutBounds.Count > 0)
- {
- var firstElementBounds = GetLayoutBoundsForRealizedIndex(0);
- var lastElementBounds = GetLayoutBoundsForRealizedIndex(GetRealizedElementCount() - 1);
- var effectiveOrientation = scrollOrientationSameAsFlow ?
- (orientation == ScrollOrientation.Vertical ? ScrollOrientation.Horizontal : ScrollOrientation.Vertical) :
- orientation;
- var windowStart = effectiveOrientation == ScrollOrientation.Vertical ? window.Y : window.X;
- var windowEnd = effectiveOrientation == ScrollOrientation.Vertical ? window.Y + window.Height : window.X + window.Width;
- var firstElementStart = effectiveOrientation == ScrollOrientation.Vertical ? firstElementBounds.Y : firstElementBounds.X;
- var lastElementEnd = effectiveOrientation == ScrollOrientation.Vertical ? lastElementBounds.Y + lastElementBounds.Height : lastElementBounds.X + lastElementBounds.Width;
- intersects =
- firstElementStart <= windowEnd &&
- lastElementEnd >= windowStart;
- }
- return intersects;
- }
- public void DataSourceChanged(object source, NotifyCollectionChangedEventArgs args)
- {
- if (_realizedElements.Count > 0)
- {
- switch (args.Action)
- {
- case NotifyCollectionChangedAction.Add:
- {
- OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
- }
- break;
- case NotifyCollectionChangedAction.Replace:
- {
- int oldSize = args.OldItems.Count;
- int newSize = args.NewItems.Count;
- int oldStartIndex = args.OldStartingIndex;
- int newStartIndex = args.NewStartingIndex;
- if (oldSize == newSize &&
- oldStartIndex == newStartIndex &&
- IsDataIndexRealized(oldStartIndex) &&
- IsDataIndexRealized(oldStartIndex + oldSize - 1))
- {
- // Straight up replace of n items within the realization window.
- // Removing and adding might causes us to lose the anchor causing us
- // to throw away all containers and start from scratch.
- // Instead, we can just clear those items and set the element to
- // null (sentinel) and let the next measure get new containers for them.
- var startRealizedIndex = GetRealizedRangeIndexFromDataIndex(oldStartIndex);
- for (int realizedIndex = startRealizedIndex; realizedIndex < startRealizedIndex + oldSize; realizedIndex++)
- {
- var elementRef = _realizedElements[realizedIndex];
- if (elementRef != null)
- {
- _context.RecycleElement(elementRef);
- _realizedElements[realizedIndex] = null;
- }
- }
- }
- else
- {
- OnItemsRemoved(oldStartIndex, oldSize);
- OnItemsAdded(newStartIndex, newSize);
- }
- }
- break;
- case NotifyCollectionChangedAction.Remove:
- {
- OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count);
- }
- break;
- case NotifyCollectionChangedAction.Reset:
- ClearRealizedRange();
- break;
- case NotifyCollectionChangedAction.Move:
- throw new NotImplementedException();
- }
- }
- }
- public int GetElementDataIndex(ILayoutable suggestedAnchor)
- {
- var it = _realizedElements.IndexOf(suggestedAnchor);
- return it != -1 ? GetDataIndexFromRealizedRangeIndex(it) : -1;
- }
- public int GetDataIndexFromRealizedRangeIndex(int rangeIndex)
- {
- return IsVirtualizingContext ? rangeIndex + _firstRealizedDataIndex : rangeIndex;
- }
- private int GetRealizedRangeIndexFromDataIndex(int dataIndex)
- {
- return IsVirtualizingContext ? dataIndex - _firstRealizedDataIndex : dataIndex;
- }
- private void DiscardElementsOutsideWindow(in Rect window, ScrollOrientation orientation)
- {
- // The following illustration explains the cutoff indices.
- // We will clear all the realized elements from both ends
- // up to the corresponding cutoff index.
- // '-' means the element is outside the cutoff range.
- // '*' means the element is inside the cutoff range and will be cleared.
- //
- // Window:
- // |______________________________|
- // Realization range:
- // |*****----------------------------------*********|
- // | |
- // frontCutoffIndex backCutoffIndex
- //
- // Note that we tolerate at most one element outside of the window
- // because the FlowLayoutAlgorithm.Generate routine stops *after*
- // it laid out an element outside the realization window.
- // This is also convenient because it protects the anchor
- // during a BringIntoView operation during which the anchor may
- // not be in the realization window (in fact, the realization window
- // might be empty if the BringIntoView is issued before the first
- // layout pass).
- int realizedRangeSize = GetRealizedElementCount();
- int frontCutoffIndex = -1;
- int backCutoffIndex = realizedRangeSize;
- for (int i = 0;
- i < realizedRangeSize &&
- !Intersects(window, _realizedElementLayoutBounds[i], orientation);
- ++i)
- {
- ++frontCutoffIndex;
- }
- for (int i = realizedRangeSize - 1;
- i >= 0 &&
- !Intersects(window, _realizedElementLayoutBounds[i], orientation);
- --i)
- {
- --backCutoffIndex;
- }
- if (backCutoffIndex < realizedRangeSize - 1)
- {
- ClearRealizedRange(backCutoffIndex + 1, realizedRangeSize - backCutoffIndex - 1);
- }
- if (frontCutoffIndex > 0)
- {
- ClearRealizedRange(0, Math.Min(frontCutoffIndex, GetRealizedElementCount()));
- }
- }
- private static bool Intersects(in Rect lhs, in Rect rhs, ScrollOrientation orientation)
- {
- var lhsStart = orientation == ScrollOrientation.Vertical ? lhs.Y : lhs.X;
- var lhsEnd = orientation == ScrollOrientation.Vertical ? lhs.Y + lhs.Height : lhs.X + lhs.Width;
- var rhsStart = orientation == ScrollOrientation.Vertical ? rhs.Y : rhs.X;
- var rhsEnd = orientation == ScrollOrientation.Vertical ? rhs.Y + rhs.Height : rhs.X + rhs.Width;
- return lhsEnd >= rhsStart && lhsStart <= rhsEnd;
- }
- private void OnItemsAdded(int index, int count)
- {
- // Using the old indices here (before it was updated by the collection change)
- // if the insert data index is between the first and last realized data index, we need
- // to insert items.
- int lastRealizedDataIndex = _firstRealizedDataIndex + GetRealizedElementCount() - 1;
- int newStartingIndex = index;
- if (newStartingIndex >= _firstRealizedDataIndex &&
- newStartingIndex <= lastRealizedDataIndex)
- {
- // Inserted within the realized range
- int insertRangeStartIndex = newStartingIndex - _firstRealizedDataIndex;
- for (int i = 0; i < count; i++)
- {
- // Insert null (sentinel) here instead of an element, that way we dont
- // end up creating a lot of elements only to be thrown out in the next layout.
- int insertRangeIndex = insertRangeStartIndex + i;
- int dataIndex = newStartingIndex + i;
- // This is to keep the contiguousness of the mapping
- Insert(insertRangeIndex, dataIndex, null);
- }
- }
- else if (index <= _firstRealizedDataIndex)
- {
- // Items were inserted before the realized range.
- // We need to update m_firstRealizedDataIndex;
- _firstRealizedDataIndex += count;
- }
- }
- private void OnItemsRemoved(int index, int count)
- {
- int lastRealizedDataIndex = _firstRealizedDataIndex + _realizedElements.Count - 1;
- int startIndex = Math.Max(_firstRealizedDataIndex, index);
- int endIndex = Math.Min(lastRealizedDataIndex, index + count - 1);
- bool removeAffectsFirstRealizedDataIndex = (index <= _firstRealizedDataIndex);
- if (endIndex >= startIndex)
- {
- ClearRealizedRange(GetRealizedRangeIndexFromDataIndex(startIndex), endIndex - startIndex + 1);
- }
- if (removeAffectsFirstRealizedDataIndex &&
- _firstRealizedDataIndex != -1)
- {
- _firstRealizedDataIndex -= count;
- }
- }
- }
- }
|