Browse Source

Initial import of ItemsRepeater from the WinUI library.

https://github.com/microsoft/microsoft-ui-xaml
pull/2603/head
Steven Kirk 7 years ago
parent
commit
ece1b3e334
  1. 1
      samples/ControlCatalog/MainView.xaml
  2. 7
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  3. 25
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  4. 1
      src/Avalonia.Controls/Properties/AssemblyInfo.cs
  5. 11
      src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs
  6. 12
      src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs
  7. 426
      src/Avalonia.Controls/Repeaters/ElementManager.cs
  8. 689
      src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs
  9. 43
      src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs
  10. 10
      src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs
  11. 53
      src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs
  12. 631
      src/Avalonia.Controls/Repeaters/ItemsRepeater.cs
  13. 11
      src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs
  14. 27
      src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs
  15. 25
      src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs
  16. 91
      src/Avalonia.Controls/Repeaters/ItemsSourceView.cs
  17. 24
      src/Avalonia.Controls/Repeaters/Layout.cs
  18. 13
      src/Avalonia.Controls/Repeaters/LayoutContext.cs
  19. 29
      src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs
  20. 78
      src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs
  21. 101
      src/Avalonia.Controls/Repeaters/RecyclePool.cs
  22. 59
      src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs
  23. 298
      src/Avalonia.Controls/Repeaters/StackLayout.cs
  24. 53
      src/Avalonia.Controls/Repeaters/StackLayoutState.cs
  25. 54
      src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs
  26. 787
      src/Avalonia.Controls/Repeaters/ViewManager.cs
  27. 463
      src/Avalonia.Controls/Repeaters/ViewportManager.cs
  28. 121
      src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs
  29. 47
      src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs
  30. 190
      src/Avalonia.Controls/Repeaters/VirtualizingLayoutContext.cs
  31. 4
      src/Avalonia.Controls/StackPanel.cs
  32. 32
      src/Avalonia.Controls/Utils/ListUtils.cs
  33. 1
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

1
samples/ControlCatalog/MainView.xaml

@ -26,6 +26,7 @@
<TabItem Header="Drag+Drop"><pages:DragAndDropPage/></TabItem>
<TabItem Header="Expander"><pages:ExpanderPage/></TabItem>
<TabItem Header="Image"><pages:ImagePage/></TabItem>
<TabItem Header="ItemsRepeater"><pages:ItemsRepeaterPage/></TabItem>
<TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem>
<TabItem Header="ListBox"><pages:ListBoxPage/></TabItem>
<TabItem Header="Menu"><pages:MenuPage/></TabItem>

7
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@ -0,0 +1,7 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ItemsRepeaterPage">
<ScrollViewer HorizontalScrollBarVisibility="Disabled">
<ItemsRepeater Items="{Binding}"/>
</ScrollViewer>
</UserControl>

25
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs

@ -0,0 +1,25 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages
{
public class ItemsRepeaterPage : UserControl
{
public ItemsRepeaterPage()
{
this.InitializeComponent();
DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" )
.ToArray();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

1
src/Avalonia.Controls/Properties/AssemblyInfo.cs

@ -13,6 +13,7 @@ using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Embedding")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Presenters")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Repeaters")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Shapes")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Templates")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Notifications")]

11
src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs

@ -0,0 +1,11 @@
using System;
namespace Avalonia.Controls.Repeaters
{
public sealed class ElementFactoryGetArgs : EventArgs
{
public object Data { get; set; }
public IControl Parent { get; set; }
internal int Index { get; set; }
}
}

12
src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Controls.Repeaters
{
public sealed class ElementFactoryRecycleArgs : EventArgs
{
public IControl Element { get; set; }
public IControl Parent { get; set; }
}
}

426
src/Avalonia.Controls/Repeaters/ElementManager.cs

@ -0,0 +1,426 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
using Avalonia.Controls.Utils;
namespace Avalonia.Controls.Repeaters
{
internal class ElementManager
{
private readonly List<IControl> _realizedElements = new List<IControl>();
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(Orientation 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 IControl GetAt(int realizedIndex)
{
IControl element;
if (IsVirtualizingContext)
{
if (_realizedElements[realizedIndex] == null)
{
// Sentinel. Create the element now since we need it.
int dataIndex = GetDataIndexFromRealizedRangeIndex(realizedIndex);
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(IControl element, int dataIndex)
{
if (_realizedElements.Count == 0)
{
_firstRealizedDataIndex = dataIndex;
}
_realizedElements.Add(element);
_realizedElementLayoutBounds.Add(default);
}
public void Insert(int realizedIndex, int dataIndex, IControl 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) => currentIndex >= 0 && currentIndex < _context.ItemCount;
public IControl 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);
}
}
}
public bool IsWindowConnected(in Rect window, Orientation orientation, bool scrollOrientationSameAsFlow)
{
bool intersects = false;
if (_realizedElementLayoutBounds.Count > 0)
{
var firstElementBounds = GetLayoutBoundsForRealizedIndex(0);
var lastElementBounds = GetLayoutBoundsForRealizedIndex(GetRealizedElementCount() - 1);
var effectiveOrientation = scrollOrientationSameAsFlow ?
(orientation == Orientation.Vertical ? Orientation.Horizontal : Orientation.Vertical) :
orientation;
var windowStart = effectiveOrientation == Orientation.Vertical ? window.Y : window.X;
var windowEnd = effectiveOrientation == Orientation.Vertical ? window.Y + window.Height : window.X + window.Width;
var firstElementStart = effectiveOrientation == Orientation.Vertical ? firstElementBounds.Y : firstElementBounds.X;
var lastElementEnd = effectiveOrientation == Orientation.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:
{
OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count);
OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
}
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(IControl 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, Orientation 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, Orientation orientation)
{
var lhsStart = orientation == Orientation.Vertical ? lhs.Y : lhs.X;
var lhsEnd = orientation == Orientation.Vertical ? lhs.Y + lhs.Height : lhs.X + lhs.Width;
var rhsStart = orientation == Orientation.Vertical ? rhs.Y : rhs.X;
var rhsEnd = orientation == Orientation.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;
}
}
}
}

689
src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs

@ -0,0 +1,689 @@
using System;
using System.Collections.Specialized;
namespace Avalonia.Controls.Repeaters
{
internal class FlowLayoutAlgorithm
{
private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures();
private readonly ElementManager _elementManager = new ElementManager();
private Size _lastAvailableSize;
private double _lastItemSpacing;
private bool _collectionChangePending;
private VirtualizingLayoutContext _context;
private IFlowLayoutAlgorithmDelegates _algorithmCallbacks;
private Rect _lastExtent;
private int _firstRealizedDataIndexInsideRealizationWindow = -1;
private int _lastRealizedDataIndexInsideRealizationWindow = -1;
// If the scroll orientation is the same as the folow orientation
// we will only have one line since we will never wrap. In that case
// we do not want to align the line. We could potentially switch the
// meaning of line alignment in this case, but I'll hold off on that
// feature until someone asks for it - This is not a common scenario
// anyway.
private bool _scrollOrientationSameAsFlow;
public Rect LastExtent => _lastExtent;
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;
}
}
private Rect RealizationRect => IsVirtualizingContext ? _context.RealizationRect : new Rect(Size.Infinity);
public void InitializeForContext(VirtualizingLayoutContext context, IFlowLayoutAlgorithmDelegates callbacks)
{
_algorithmCallbacks = callbacks;
_context = context;
_elementManager.SetContext(context);
}
public void UninitializeForContext(VirtualizingLayoutContext context)
{
if (IsVirtualizingContext)
{
// This layout is about to be detached. Let go of all elements
// being held and remove the layout state from the context.
_elementManager.ClearRealizedRange();
}
context.LayoutState = null;
}
public Size Measure(
Size availableSize,
VirtualizingLayoutContext context,
bool isWrapping,
double minItemSpacing,
double lineSpacing,
Orientation orientation,
string layoutId)
{
_orientation.ScrollOrientation = orientation;
// If minor size is infinity, there is only one line and no need to align that line.
_scrollOrientationSameAsFlow = double.IsInfinity(_orientation.Minor(availableSize));
var realizationRect = RealizationRect;
var suggestedAnchorIndex = _context.RecommendedAnchorIndex;
if (_elementManager.IsIndexValidInData(suggestedAnchorIndex))
{
var anchorRealized = _elementManager.IsDataIndexRealized(suggestedAnchorIndex);
if (!anchorRealized)
{
MakeAnchor(_context, suggestedAnchorIndex, availableSize);
}
}
_elementManager.OnBeginMeasure(orientation);
int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId);
Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId);
Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId);
if (isWrapping && IsReflowRequired())
{
var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
_orientation.SetMinorStart(ref firstElementBounds, 0);
_elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds);
Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, layoutId);
}
RaiseLineArranged();
_collectionChangePending = false;
_lastExtent = EstimateExtent(availableSize, layoutId);
SetLayoutOrigin();
return new Size(_lastExtent.Width, _lastExtent.Height);
}
public Size Arrange(
Size finalSize,
VirtualizingLayoutContext context,
LineAlignment lineAlignment,
string layoutId)
{
ArrangeVirtualizingLayout(finalSize, lineAlignment, layoutId);
return new Size(
Math.Max(finalSize.Width, _lastExtent.Width),
Math.Max(finalSize.Height, _lastExtent.Height));
}
public void OnItemsSourceChanged(
object source,
NotifyCollectionChangedEventArgs args,
VirtualizingLayoutContext context)
{
_elementManager.DataSourceChanged(source, args);
_collectionChangePending = true;
}
public Size MeasureElement(
IControl element,
int index,
Size availableSize,
VirtualizingLayoutContext context)
{
var measureSize = _algorithmCallbacks.Algorithm_GetMeasureSize(index, availableSize, context);
element.Measure(measureSize);
var provisionalArrangeSize = _algorithmCallbacks.Algorithm_GetProvisionalArrangeSize(index, measureSize, element.DesiredSize, context);
_algorithmCallbacks.Algorithm_OnElementMeasured(element, index, availableSize, measureSize, element.DesiredSize, provisionalArrangeSize, context);
return provisionalArrangeSize;
}
private int GetAnchorIndex(
Size availableSize,
bool isWrapping,
double minItemSpacing,
string layoutId)
{
int anchorIndex = -1;
var anchorPosition= new Point();
var context = _context;
if (!IsVirtualizingContext)
{
// Non virtualizing host, start generating from the element 0
anchorIndex = context.ItemCount > 0 ? 0 : -1;
}
else
{
bool isRealizationWindowConnected = _elementManager.IsWindowConnected(RealizationRect, _orientation.ScrollOrientation, _scrollOrientationSameAsFlow);
// Item spacing and size in non-virtualizing direction change can cause elements to reflow
// and get a new column position. In that case we need the anchor to be positioned in the
// correct column.
bool needAnchorColumnRevaluation = isWrapping && (
_orientation.Minor(_lastAvailableSize) != _orientation.Minor(availableSize) ||
_lastItemSpacing != minItemSpacing ||
_collectionChangePending);
var suggestedAnchorIndex = _context.RecommendedAnchorIndex;
var isAnchorSuggestionValid = suggestedAnchorIndex >= 0 &&
_elementManager.IsDataIndexRealized(suggestedAnchorIndex);
if (isAnchorSuggestionValid)
{
anchorIndex = _algorithmCallbacks.Algorithm_GetAnchorForTargetElement(
suggestedAnchorIndex,
availableSize,
context).Index;
if (_elementManager.IsDataIndexRealized(anchorIndex))
{
var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex);
if (needAnchorColumnRevaluation)
{
// We were provided a valid anchor, but its position might be incorrect because for example it is in
// the wrong column. We do know that the anchor is the first element in the row, so we can force the minor position
// to start at 0.
anchorPosition = _orientation.MinorMajorPoint(0, _orientation.MajorStart(anchorBounds));
}
else
{
anchorPosition = new Point(anchorBounds.X, anchorBounds.Y);
}
}
else
{
// It is possible to end up in a situation during a collection change where GetAnchorForTargetElement returns an index
// which is not in the realized range. Eg. insert one item at index 0 for a grid layout.
// SuggestedAnchor will be 1 (used to be 0) and GetAnchorForTargetElement will return 0 (left most item in row). However 0 is not in the
// realized range yet. In this case we realize the gap between the target anchor and the suggested anchor.
int firstRealizedDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);
for (int i = firstRealizedDataIndex - 1; i >= anchorIndex; --i)
{
_elementManager.EnsureElementRealized(false /*forward*/, i, layoutId);
}
var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(suggestedAnchorIndex);
anchorPosition = _orientation.MinorMajorPoint(0, _orientation.MajorStart(anchorBounds));
}
}
else if (needAnchorColumnRevaluation || !isRealizationWindowConnected)
{
// The anchor is based on the realization window because a connected ItemsRepeater might intersect the realization window
// but not the visible window. In that situation, we still need to produce a valid anchor.
var anchorInfo = _algorithmCallbacks.Algorithm_GetAnchorForRealizationRect(availableSize, context);
anchorIndex = anchorInfo.Index;
anchorPosition = _orientation.MinorMajorPoint(0, anchorInfo.Offset);
}
else
{
// No suggestion - just pick first in realized range
anchorIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);
var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
anchorPosition = new Point(firstElementBounds.X, firstElementBounds.Y);
}
}
_firstRealizedDataIndexInsideRealizationWindow = _lastRealizedDataIndexInsideRealizationWindow = anchorIndex;
if (_elementManager.IsIndexValidInData(anchorIndex))
{
if (!_elementManager.IsDataIndexRealized(anchorIndex))
{
// Disconnected, throw everything and create new anchor
_elementManager.ClearRealizedRange();
var anchor = _context.GetOrCreateElementAt(anchorIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
_elementManager.Add(anchor, anchorIndex);
}
var anchorElement = _elementManager.GetRealizedElement(anchorIndex);
var desiredSize = MeasureElement(anchorElement, anchorIndex, availableSize, _context);
var layoutBounds = new Rect(anchorPosition.X, anchorPosition.Y, desiredSize.Width, desiredSize.Height);
_elementManager.SetLayoutBoundsForDataIndex(anchorIndex, layoutBounds);
}
else
{
_elementManager.ClearRealizedRange();
}
// TODO: Perhaps we can track changes in the property setter
_lastAvailableSize = availableSize;
_lastItemSpacing = minItemSpacing;
return anchorIndex;
}
private void Generate(
GenerateDirection direction,
int anchorIndex,
Size availableSize,
double minItemSpacing,
double lineSpacing,
string layoutId)
{
if (anchorIndex != -1)
{
int step = (direction == GenerateDirection.Forward) ? 1 : -1;
int previousIndex = anchorIndex;
int currentIndex = anchorIndex + step;
var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex);
var lineOffset = _orientation.MajorStart(anchorBounds);
var lineMajorSize = _orientation.MajorSize(anchorBounds);
int countInLine = 1;
bool lineNeedsReposition = false;
while (_elementManager.IsIndexValidInData(currentIndex) &&
ShouldContinueFillingUpSpace(previousIndex, direction))
{
// Ensure layout element.
_elementManager.EnsureElementRealized(direction == GenerateDirection.Forward, currentIndex, layoutId);
var currentElement = _elementManager.GetRealizedElement(currentIndex);
var desiredSize = MeasureElement(currentElement, currentIndex, availableSize, _context);
// Lay it out.
var previousElement = _elementManager.GetRealizedElement(previousIndex);
var currentBounds = new Rect(0, 0, desiredSize.Width, desiredSize.Height);
var previousElementBounds = _elementManager.GetLayoutBoundsForDataIndex(previousIndex);
if (direction == GenerateDirection.Forward)
{
double remainingSpace = _orientation.Minor(availableSize) - (_orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing + _orientation.Minor(desiredSize));
if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
{
// No more space in this row. wrap to next row.
_orientation.SetMinorStart(ref currentBounds, 0);
_orientation.SetMajorStart(ref currentBounds, _orientation.MajorStart(previousElementBounds) + lineMajorSize + lineSpacing);
if (lineNeedsReposition)
{
// reposition the previous line (countInLine items)
for (int i = 0; i < countInLine; i++)
{
var dataIndex = currentIndex - 1 - i;
var bounds = _elementManager.GetLayoutBoundsForDataIndex(dataIndex);
_orientation.SetMajorSize(ref bounds, lineMajorSize);
_elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds);
}
}
// Setup for next line.
lineMajorSize = _orientation.MajorSize(currentBounds);
lineOffset = _orientation.MajorStart(currentBounds);
lineNeedsReposition = false;
countInLine = 1;
}
else
{
// More space is available in this row.
_orientation.SetMinorStart(ref currentBounds, _orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing);
_orientation.SetMajorStart(ref currentBounds, lineOffset);
lineMajorSize = Math.Max(lineMajorSize, _orientation.MajorSize(currentBounds));
lineNeedsReposition = _orientation.MajorSize(previousElementBounds) != _orientation.MajorSize(currentBounds);
countInLine++;
}
}
else
{
// Backward
double remainingSpace = _orientation.MinorStart(previousElementBounds) - (_orientation.Minor(desiredSize) + minItemSpacing);
if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
{
// Does not fit, wrap to the previous row
var availableSizeMinor = _orientation.Minor(availableSize);
_orientation.SetMinorStart(ref currentBounds, double.IsInfinity(availableSizeMinor) ? availableSizeMinor - _orientation.Minor(desiredSize) : 0);
_orientation.SetMajorStart(ref currentBounds, lineOffset - _orientation.Major(desiredSize) - lineSpacing);
if (lineNeedsReposition)
{
var previousLineOffset = _orientation.MajorStart(_elementManager.GetLayoutBoundsForDataIndex(currentIndex + countInLine + 1));
// reposition the previous line (countInLine items)
for (int i = 0; i < countInLine; i++)
{
var dataIndex = currentIndex + 1 + i;
if (dataIndex != anchorIndex)
{
var bounds = _elementManager.GetLayoutBoundsForDataIndex(dataIndex);
_orientation.SetMajorStart(ref bounds, previousLineOffset - lineMajorSize - lineSpacing);
_orientation.SetMajorSize(ref bounds, lineMajorSize);
_elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds);
}
}
}
// Setup for next line.
lineMajorSize = _orientation.MajorSize(currentBounds);
lineOffset = _orientation.MajorStart(currentBounds);
lineNeedsReposition = false;
countInLine = 1;
}
else
{
// Fits in this row. put it in the previous position
_orientation.SetMinorStart(ref currentBounds, _orientation.MinorStart(previousElementBounds) - _orientation.Minor(desiredSize) - minItemSpacing);
_orientation.SetMajorStart(ref currentBounds, lineOffset);
lineMajorSize = Math.Max(lineMajorSize, _orientation.MajorSize(currentBounds));
lineNeedsReposition = _orientation.MajorSize(previousElementBounds) != _orientation.MajorSize(currentBounds);
countInLine++;
}
}
_elementManager.SetLayoutBoundsForDataIndex(currentIndex, currentBounds);
previousIndex = currentIndex;
currentIndex += step;
}
// If we did not reach the top or bottom of the extent, we realized one
// extra item before we knew we were outside the realization window. Do not
// account for that element in the indicies inside the realization window.
if (direction == GenerateDirection.Forward)
{
int dataCount = _context.ItemCount;
_lastRealizedDataIndexInsideRealizationWindow = previousIndex == dataCount - 1 ? dataCount - 1 : previousIndex - 1;
_lastRealizedDataIndexInsideRealizationWindow = Math.Max(0, _lastRealizedDataIndexInsideRealizationWindow);
}
else
{
int dataCount = _context.ItemCount;
_firstRealizedDataIndexInsideRealizationWindow = previousIndex == 0 ? 0 : previousIndex + 1;
_firstRealizedDataIndexInsideRealizationWindow = Math.Min(dataCount - 1, _firstRealizedDataIndexInsideRealizationWindow);
}
_elementManager.DiscardElementsOutsideWindow(direction == GenerateDirection.Forward, currentIndex);
}
}
private void MakeAnchor(
VirtualizingLayoutContext context,
int index,
Size availableSize)
{
_elementManager.ClearRealizedRange();
// FlowLayout requires that the anchor is the first element in the row.
var internalAnchor = _algorithmCallbacks.Algorithm_GetAnchorForTargetElement(index, availableSize, context);
//MUX_ASSERT(internalAnchor.Index <= index);
// No need to set the position of the anchor.
// (0,0) is fine for now since the extent can
// grow in any direction.
for (int dataIndex = internalAnchor.Index; dataIndex < index + 1; ++dataIndex)
{
var element = context.GetOrCreateElementAt(dataIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
element.Measure(_algorithmCallbacks.Algorithm_GetMeasureSize(dataIndex, availableSize, context));
_elementManager.Add(element, dataIndex);
}
}
private bool IsReflowRequired()
{
// If first element is realized and is not at the very beginning we need to reflow.
return
_elementManager.GetRealizedElementCount() > 0 &&
_elementManager.GetDataIndexFromRealizedRangeIndex(0) == 0 &&
_orientation.MinorStart(_elementManager.GetLayoutBoundsForRealizedIndex(0)) != 0;
}
private bool ShouldContinueFillingUpSpace(
int index,
GenerateDirection direction)
{
bool shouldContinue = false;
if (!IsVirtualizingContext)
{
shouldContinue = true;
}
else
{
var realizationRect = _context.RealizationRect;
var elementBounds = _elementManager.GetLayoutBoundsForDataIndex(index);
var elementMajorStart = _orientation.MajorStart(elementBounds);
var elementMajorEnd = _orientation.MajorEnd(elementBounds);
var rectMajorStart = _orientation.MajorStart(realizationRect);
var rectMajorEnd = _orientation.MajorEnd(realizationRect);
var elementMinorStart = _orientation.MinorStart(elementBounds);
var elementMinorEnd = _orientation.MinorEnd(elementBounds);
var rectMinorStart = _orientation.MinorStart(realizationRect);
var rectMinorEnd = _orientation.MinorEnd(realizationRect);
// Ensure that both minor and major directions are taken into consideration so that if the scrolling direction
// is the same as the flow direction we still stop at the end of the viewport rectangle.
shouldContinue =
(direction == GenerateDirection.Forward && elementMajorStart < rectMajorEnd && elementMinorStart < rectMinorEnd) ||
(direction == GenerateDirection.Backward && elementMajorEnd > rectMajorStart && elementMinorEnd > rectMinorStart);
}
return shouldContinue;
}
private Rect EstimateExtent(Size availableSize, string layoutId)
{
IControl firstRealizedElement = null;
Rect firstBounds = new Rect();
IControl lastRealizedElement = null;
Rect lastBounds = new Rect();
int firstDataIndex = -1;
int lastDataIndex = -1;
if (_elementManager.GetRealizedElementCount() > 0)
{
firstRealizedElement = _elementManager.GetAt(0);
firstBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
firstDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);;
int last = _elementManager.GetRealizedElementCount() - 1;
lastRealizedElement = _elementManager.GetAt(last);
lastDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(last);
lastBounds = _elementManager.GetLayoutBoundsForRealizedIndex(last);
}
Rect extent = _algorithmCallbacks.Algorithm_GetExtent(
availableSize,
_context,
firstRealizedElement,
firstDataIndex,
firstBounds,
lastRealizedElement,
lastDataIndex,
lastBounds);
return extent;
}
private void RaiseLineArranged()
{
var realizationRect = RealizationRect;
if (realizationRect.Width != 0.0f || realizationRect.Height != 0.0f)
{
int realizedElementCount = _elementManager.GetRealizedElementCount();
if (realizedElementCount > 0)
{
//MUX_ASSERT(_firstRealizedDataIndexInsideRealizationWindow != -1 && _lastRealizedDataIndexInsideRealizationWindow != -1);
int countInLine = 0;
var previousElementBounds = _elementManager.GetLayoutBoundsForDataIndex(_firstRealizedDataIndexInsideRealizationWindow);
var currentLineOffset = _orientation.MajorStart(previousElementBounds);
var currentLineSize = _orientation.MajorSize(previousElementBounds);
for (int currentDataIndex = _firstRealizedDataIndexInsideRealizationWindow; currentDataIndex <= _lastRealizedDataIndexInsideRealizationWindow; currentDataIndex++)
{
var currentBounds = _elementManager.GetLayoutBoundsForDataIndex(currentDataIndex);
if (_orientation.MajorStart(currentBounds) != currentLineOffset)
{
// Staring a new line
_algorithmCallbacks.Algorithm_OnLineArranged(currentDataIndex - countInLine, countInLine, currentLineSize, _context);
countInLine = 0;
currentLineOffset = _orientation.MajorStart(currentBounds);
currentLineSize = 0;
}
currentLineSize = Math.Max(currentLineSize, _orientation.MajorSize(currentBounds));
countInLine++;
previousElementBounds = currentBounds;
}
// Raise for the last line.
_algorithmCallbacks.Algorithm_OnLineArranged(_lastRealizedDataIndexInsideRealizationWindow - countInLine + 1, countInLine, currentLineSize, _context);
}
}
}
private void ArrangeVirtualizingLayout(
Size finalSize,
LineAlignment lineAlignment,
string layoutId)
{
// Walk through the realized elements one line at a time and
// align them, Then call element.Arrange with the arranged bounds.
int realizedElementCount = _elementManager.GetRealizedElementCount();
if (realizedElementCount > 0)
{
var countInLine = 1;
var previousElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
var currentLineOffset = _orientation.MajorStart(previousElementBounds);
var spaceAtLineStart = _orientation.MinorStart(previousElementBounds);
var spaceAtLineEnd = 0.0;
var currentLineSize = _orientation.MajorSize(previousElementBounds);
for (int i = 1; i < realizedElementCount; i++)
{
var currentBounds = _elementManager.GetLayoutBoundsForRealizedIndex(i);
if (_orientation.MajorStart(currentBounds) != currentLineOffset)
{
spaceAtLineEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds);
PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, layoutId);
spaceAtLineStart = _orientation.MinorStart(currentBounds);
countInLine = 0;
currentLineOffset = _orientation.MajorStart(currentBounds);
currentLineSize = 0;
}
countInLine++; // for current element
currentLineSize = Math.Max(currentLineSize, _orientation.MajorSize(currentBounds));
previousElementBounds = currentBounds;
}
// Last line - potentially have a property to customize
// aligning the last line or not.
if (countInLine > 0)
{
var spaceAtEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds);
PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, layoutId);
}
}
}
// Align elements within a line. Note that this does not modify LayoutBounds. So if we get
// repeated measures, the LayoutBounds remain the same in each layout.
private void PerformLineAlignment(
int lineStartIndex,
int countInLine,
double spaceAtLineStart,
double spaceAtLineEnd,
double lineSize,
LineAlignment lineAlignment,
string layoutId)
{
for (int rangeIndex = lineStartIndex; rangeIndex < lineStartIndex + countInLine; ++rangeIndex)
{
var bounds = _elementManager.GetLayoutBoundsForRealizedIndex(rangeIndex);
_orientation.SetMajorSize(ref bounds, lineSize);
if (!_scrollOrientationSameAsFlow)
{
// Note: Space at start could potentially be negative
if (spaceAtLineStart != 0 || spaceAtLineEnd != 0)
{
var totalSpace = spaceAtLineStart + spaceAtLineEnd;
var minorStart = _orientation.MinorStart(bounds);
switch (lineAlignment)
{
case LineAlignment.Start:
{
_orientation.SetMinorStart(ref bounds, minorStart - spaceAtLineStart);
break;
}
case LineAlignment.End:
{
_orientation.SetMinorStart(ref bounds, minorStart + spaceAtLineEnd);
break;
}
case LineAlignment.Center:
{
_orientation.SetMinorStart(ref bounds, (minorStart - spaceAtLineStart) + (totalSpace / 2));
break;
}
case LineAlignment.SpaceAround:
{
var interItemSpace = countInLine >= 1 ? totalSpace / (countInLine * 2) : 0;
_orientation.SetMinorStart(
ref bounds,
(minorStart - spaceAtLineStart) + (interItemSpace * ((rangeIndex - lineStartIndex + 1) * 2 - 1)));
break;
}
case LineAlignment.SpaceBetween:
{
var interItemSpace = countInLine > 1 ? totalSpace / (countInLine - 1) : 0;
_orientation.SetMinorStart(
ref bounds,
(minorStart - spaceAtLineStart) + (interItemSpace * (rangeIndex - lineStartIndex)));
break;
}
case LineAlignment.SpaceEvenly:
{
var interItemSpace = countInLine >= 1 ? totalSpace / (countInLine + 1) : 0;
_orientation.SetMinorStart(
ref bounds,
(minorStart - spaceAtLineStart) + (interItemSpace * (rangeIndex - lineStartIndex + 1)));
break;
}
}
}
}
bounds = bounds.Translate(-_lastExtent.Position);
var element = _elementManager.GetAt(rangeIndex);
element.Arrange(bounds);
}
}
void SetLayoutOrigin()
{
if (IsVirtualizingContext)
{
_context.LayoutOrigin = new Point(_lastExtent.X, _lastExtent.Y);
}
else
{
// Should have 0 origin for non-virtualizing layout since we always start from
// the first item
//MUX_ASSERT(m_lastExtent.X == 0 && m_lastExtent.Y == 0);
}
}
public enum LineAlignment
{
Start,
Center,
End,
SpaceAround,
SpaceBetween,
SpaceEvenly,
}
private enum GenerateDirection
{
Forward,
Backward,
}
}
}

43
src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Controls.Repeaters
{
internal struct FlowLayoutAnchorInfo
{
public int Index { get; set; }
public double Offset { get; set; }
}
internal interface IFlowLayoutAlgorithmDelegates
{
Size Algorithm_GetMeasureSize(int index, Size availableSize, VirtualizingLayoutContext context);
Size Algorithm_GetProvisionalArrangeSize(int index, Size measureSize, Size desiredSize, VirtualizingLayoutContext context);
bool Algorithm_ShouldBreakLine(int index, double remainingSpace);
FlowLayoutAnchorInfo Algorithm_GetAnchorForRealizationRect(Size availableSize, VirtualizingLayoutContext context);
FlowLayoutAnchorInfo Algorithm_GetAnchorForTargetElement(int targetIndex, Size availableSize, VirtualizingLayoutContext context);
Rect Algorithm_GetExtent(
Size availableSize,
VirtualizingLayoutContext context,
IControl firstRealized,
int firstRealizedItemIndex,
Rect firstRealizedLayoutBounds,
IControl lastRealized,
int lastRealizedItemIndex,
Rect lastRealizedLayoutBounds);
void Algorithm_OnElementMeasured(
IControl element,
int index,
Size availableSize,
Size measureSize,
Size desiredSize,
Size provisionalArrangeSize,
VirtualizingLayoutContext context);
void Algorithm_OnLineArranged(
int startIndex,
int countInLine,
double lineSize,
VirtualizingLayoutContext context);
}
}

10
src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Controls.Repeaters
{
internal interface ITrackerHandleManager
{
}
}

53
src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Controls.Templates;
namespace Avalonia.Controls.Repeaters
{
internal class ItemTemplateWrapper
{
private readonly IDataTemplate _dataTemplate;
public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate;
public IControl GetElement(ElementFactoryGetArgs args)
{
var selectedTemplate = _dataTemplate;
var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate);
IControl element = null;
if (recyclePool != null)
{
// try to get an element from the recycle pool.
element = recyclePool.TryGetElement(string.Empty, args.Parent);
}
if (element == null)
{
// no element was found in recycle pool, create a new element
element = selectedTemplate.Build(args.Data);
// Associate template with element
element.SetValue(RecyclePool.OriginTemplateProperty, selectedTemplate);
}
return element;
}
public void RecycleElement(ElementFactoryRecycleArgs args)
{
var element = args.Element;
var selectedTemplate = _dataTemplate;
var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate);
if (recyclePool == null)
{
// No Recycle pool in the template, create one.
recyclePool = new RecyclePool();
RecyclePool.SetPoolInstance(selectedTemplate, recyclePool);
}
recyclePool.PutElement(args.Element, "" /* key */, args.Parent);
}
}
}

631
src/Avalonia.Controls/Repeaters/ItemsRepeater.cs

@ -0,0 +1,631 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using Avalonia.Controls.Templates;
using Avalonia.Input;
namespace Avalonia.Controls.Repeaters
{
public class ItemsRepeater : Panel
{
public static readonly AvaloniaProperty<double> HorizontalCacheLengthProperty =
AvaloniaProperty.Register<ItemsRepeater, double>(nameof(HorizontalCacheLength), 2.0);
public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty =
ItemsControl.ItemTemplateProperty.AddOwner<ItemsRepeater>();
public static readonly DirectProperty<ItemsRepeater, IEnumerable> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<ItemsRepeater>(o => o.Items, (o, v) => o.Items = v);
public static readonly AvaloniaProperty<Layout> LayoutProperty =
AvaloniaProperty.Register<ItemsRepeater, Layout>(nameof(Layout), new StackLayout());
public static readonly AvaloniaProperty<double> VerticalCacheLengthProperty =
AvaloniaProperty.Register<ItemsRepeater, double>(nameof(VerticalCacheLength), 2.0);
private static readonly AttachedProperty<VirtualizationInfo> VirtualizationInfoProperty =
AvaloniaProperty.RegisterAttached<ItemsRepeater, IControl, VirtualizationInfo>("VirtualizationInfo");
internal static readonly Rect InvalidRect = new Rect(-1, -1, -1, -1);
internal static readonly Point ClearedElementsArrangePosition = new Point(-10000.0, -10000.0);
private readonly ViewManager _viewManager;
private readonly ViewportManager _viewportManager;
private IEnumerable _items;
private VirtualizingLayoutContext _layoutContext;
private NotifyCollectionChangedEventArgs _processingItemsSourceChange;
private Size _lastAvailableSize;
private bool _isLayoutInProgress;
private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs;
private ItemsRepeaterElementClearingEventArgs _elementClearingArgs;
private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs;
public ItemsRepeater()
{
_viewManager = new ViewManager(this);
_viewportManager = new ViewportManager(this);
KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once);
OnLayoutChanged(null, Layout);
}
static ItemsRepeater()
{
ClipToBoundsProperty.OverrideDefaultValue<ItemsRepeater>(true);
}
public Layout Layout
{
get => GetValue(LayoutProperty);
set => SetValue(LayoutProperty, value);
}
public IEnumerable Items
{
get => _items;
set => SetAndRaise(ItemsProperty, ref _items, value);
}
public IDataTemplate ItemTemplate
{
get => GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
public double HorizontalCacheLength
{
get => GetValue(HorizontalCacheLengthProperty);
set => SetValue(HorizontalCacheLengthProperty, value);
}
public double VerticalCacheLength
{
get => GetValue(VerticalCacheLengthProperty);
set => SetValue(VerticalCacheLengthProperty, value);
}
public ItemsSourceView ItemsSourceView { get; private set; }
internal ItemTemplateWrapper ItemTemplateShim { get; set; }
internal Point LayoutOrigin { get; set; }
internal object LayoutState { get; set; }
internal IControl MadeAnchor => _viewportManager.MadeAnchor;
internal Rect RealizationWindow => _viewportManager.GetLayoutRealizationWindow();
internal IControl SuggestedAnchor => _viewportManager.SuggestedAnchor;
private bool IsProcessingCollectionChange => _processingItemsSourceChange != null;
private LayoutContext LayoutContext
{
get
{
if (_layoutContext == null)
{
_layoutContext = new RepeaterLayoutContext(this);
}
return _layoutContext;
}
}
public event EventHandler<ItemsRepeaterElementClearingEventArgs> ElementClearing;
public event EventHandler<ItemsRepeaterElementIndexChangedEventArgs> ElementIndexChanged;
public event EventHandler<ItemsRepeaterElementPreparedEventArgs> ElementPrepared;
public int GetElementIndex(IControl element) => GetElementIndexImpl(element);
public IControl TryGetElement(int index) => GetElementFromIndexImpl(index);
public void PinElement(IControl element) => _viewManager.UpdatePin(element, true);
public void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false);
public IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index);
internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element)
{
var value = element.GetValue(VirtualizationInfoProperty);
return value;
}
internal static VirtualizationInfo CreateAndInitializeVirtualizationInfo(IControl element)
{
if (TryGetVirtualizationInfo(element) != null)
{
throw new InvalidOperationException("VirtualizationInfo already created.");
}
var result = new VirtualizationInfo();
element.SetValue(VirtualizationInfoProperty, result);
return result;
}
internal static VirtualizationInfo GetVirtualizationInfo(IControl element)
{
var result = element.GetValue(VirtualizationInfoProperty);
if (result == null)
{
result = new VirtualizationInfo();
element.SetValue(VirtualizationInfoProperty, result);
}
return result;
}
protected override Size MeasureOverride(Size availableSize)
{
if (_isLayoutInProgress)
{
throw new AvaloniaInternalException("Reentrancy detected during layout.");
}
if (IsProcessingCollectionChange)
{
throw new NotSupportedException("Cannot run layout in the middle of a collection change.");
}
_viewportManager.OnOwnerMeasuring();
_isLayoutInProgress = true;
try
{
_viewManager.PrunePinnedElements();
var extent = new Rect();
var desiredSize = new Size();
var layout = Layout;
if (layout != null)
{
var layoutContext = GetLayoutContext();
desiredSize = layout.Measure(layoutContext, availableSize);
extent = new Rect(LayoutOrigin.X, LayoutOrigin.Y, desiredSize.Width, desiredSize.Height);
// Clear auto recycle candidate elements that have not been kept alive by layout - i.e layout did not
// call GetElementAt(index).
foreach (var element in Children)
{
var virtInfo = GetVirtualizationInfo(element);
if (virtInfo.Owner == ElementOwner.Layout &&
virtInfo.AutoRecycleCandidate &&
!virtInfo.KeepAlive)
{
ClearElementImpl(element);
}
}
}
_viewportManager.SetLayoutExtent(extent);
_lastAvailableSize = availableSize;
return desiredSize;
}
finally
{
_isLayoutInProgress = false;
}
}
protected override Size ArrangeOverride(Size finalSize)
{
if (_isLayoutInProgress)
{
throw new AvaloniaInternalException("Reentrancy detected during layout.");
}
if (IsProcessingCollectionChange)
{
throw new NotSupportedException("Cannot run layout in the middle of a collection change.");
}
_isLayoutInProgress = true;
try
{
var arrangeSize = Layout?.Arrange(GetLayoutContext(), finalSize) ?? default;
// The view manager might clear elements during this call.
// That's why we call it before arranging cleared elements
// off screen.
_viewManager.OnOwnerArranged();
foreach (var element in Children)
{
var virtInfo = GetVirtualizationInfo(element);
virtInfo.KeepAlive = false;
if (virtInfo.Owner == ElementOwner.ElementFactory ||
virtInfo.Owner == ElementOwner.PinnedPool)
{
// Toss it away. And arrange it with size 0 so that XYFocus won't use it.
element.Arrange(new Rect(
ClearedElementsArrangePosition.X - element.DesiredSize.Width,
ClearedElementsArrangePosition.Y - element.DesiredSize.Height,
0,
0));
}
else
{
var newBounds = element.Bounds;
//if (virtInfo.ArrangeBounds != ItemsRepeater.InvalidRect &&
// newBounds != virtInfo.ArrangeBounds)
//{
// _animationManager.OnElementBoundsChanged(element, virtInfo.ArrangeBounds, newBounds);
//}
virtInfo.ArrangeBounds = newBounds;
}
}
_viewportManager.OnOwnerArranged();
//_animationManager.OnOwnerArranged();
return arrangeSize;
}
finally
{
_isLayoutInProgress = false;
}
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
InvalidateMeasure();
_viewportManager.ResetScrollers();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
_viewportManager.ResetScrollers();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args)
{
var property = args.Property;
if (property == ItemsProperty)
{
var newValue = (IEnumerable)args.NewValue;
var newDataSource = newValue as ItemsSourceView;
if (newValue != null && newDataSource == null)
{
newDataSource = new ItemsSourceView(newValue);
}
OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
}
else if (property == ItemTemplateProperty)
{
OnItemTemplateChanged((IDataTemplate)args.OldValue, (IDataTemplate)args.NewValue);
}
else if (property == LayoutProperty)
{
OnLayoutChanged((Layout)args.OldValue, (Layout)args.NewValue);
}
//else if (property == AnimatorProperty)
//{
// OnAnimatorChanged((ElementAnimator)args.OldValue, (ElementAnimator)args.NewValue);
//}
else if (property == HorizontalCacheLengthProperty)
{
_viewportManager.HorizontalCacheLength = (double)args.NewValue;
}
else if (property == VerticalCacheLengthProperty)
{
_viewportManager.VerticalCacheLength = (double)args.NewValue;
}
else
{
base.OnPropertyChanged(args);
}
}
internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle)
{
var element = _viewManager.GetElement(index, forceCreate, supressAutoRecycle);
return element;
}
internal void ClearElementImpl(IControl element)
{
// Clearing an element due to a collection change
// is more strict in that pinned elements will be forcibly
// unpinned and sent back to the view generator.
var isClearedDueToCollectionChange =
_processingItemsSourceChange != null &&
(_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Remove ||
_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Replace ||
_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Reset);
_viewManager.ClearElement(element, isClearedDueToCollectionChange);
_viewportManager.OnElementCleared(element);
}
private int GetElementIndexImpl(IControl element)
{
var virtInfo = TryGetVirtualizationInfo(element);
return _viewManager.GetElementIndex(virtInfo);
}
private IControl GetElementFromIndexImpl(int index)
{
IControl result = null;
var children = Children;
for (var i = 0; i < children.Count && result == null; ++i)
{
var element = children[i];
var virtInfo = TryGetVirtualizationInfo(element);
if (virtInfo?.IsRealized == true && virtInfo.Index == index)
{
result = element;
}
}
return result;
}
private IControl GetOrCreateElementImpl(int index)
{
if (index >= 0 && index >= ItemsSourceView.Count)
{
throw new ArgumentException("Argument index is invalid.", "index");
}
if (_isLayoutInProgress)
{
throw new NotSupportedException("GetOrCreateElement invocation is not allowed during layout.");
}
var element = GetElementFromIndexImpl(index);
bool isAnchorOutsideRealizedRange = element == null;
if (isAnchorOutsideRealizedRange)
{
if (Layout == null)
{
throw new InvalidOperationException("Cannot make an Anchor when there is no attached layout.");
}
element = GetLayoutContext().GetOrCreateElementAt(index);
element.Measure(Size.Infinity);
}
_viewportManager.OnMakeAnchor(element, isAnchorOutsideRealizedRange);
InvalidateMeasure();
return element;
}
internal void OnElementPrepared(IControl element, int index)
{
_viewportManager.OnElementPrepared(element);
if (ElementPrepared != null)
{
if (_elementPreparedArgs == null)
{
_elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index);
}
else
{
_elementPreparedArgs.Update(element, index);
}
ElementPrepared(this, _elementPreparedArgs);
}
}
internal void OnElementClearing(IControl element)
{
if (ElementClearing != null)
{
if (_elementClearingArgs == null)
{
_elementClearingArgs = new ItemsRepeaterElementClearingEventArgs(element);
}
else
{
_elementClearingArgs.Update(element);
}
ElementClearing(this, _elementClearingArgs);
}
}
internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex)
{
if (ElementIndexChanged != null)
{
if (_elementIndexChangedArgs == null)
{
_elementIndexChangedArgs = new ItemsRepeaterElementIndexChangedEventArgs(element, oldIndex, newIndex);
}
else
{
_elementIndexChangedArgs.Update(element, oldIndex, newIndex);
}
ElementIndexChanged(this, _elementIndexChangedArgs);
}
}
private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue)
{
if (_isLayoutInProgress)
{
throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout.");
}
ItemsSourceView?.Dispose();
ItemsSourceView = newValue;
if (oldValue != null)
{
oldValue.CollectionChanged -= OnItemsSourceViewChanged;
}
if (newValue != null)
{
newValue.CollectionChanged += OnItemsSourceViewChanged;
}
if (Layout != null)
{
if (Layout is VirtualizingLayout virtualLayout)
{
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
virtualLayout.OnItemsChangedCore(GetLayoutContext(), newValue, args);
}
else if (Layout is NonVirtualizingLayout nonVirtualLayout)
{
// Walk through all the elements and make sure they are cleared for
// non-virtualizing layouts.
foreach (var element in Children)
{
if (GetVirtualizationInfo(element).IsRealized)
{
ClearElementImpl(element);
}
}
}
InvalidateMeasure();
}
}
private void OnItemTemplateChanged(IDataTemplate oldValue, IDataTemplate newValue)
{
if (_isLayoutInProgress && oldValue != null)
{
throw new AvaloniaInternalException("ItemTemplate cannot be changed during layout.");
}
// Since the ItemTemplate has changed, we need to re-evaluate all the items that
// have already been created and are now in the tree. The easiest way to do that
// would be to do a reset.. Note that this has to be done before we change the template
// so that the cleared elements go back into the old template.
if (Layout != null)
{
if (Layout is VirtualizingLayout virtualLayout)
{
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
_processingItemsSourceChange = args;
try
{
virtualLayout.OnItemsChangedCore(GetLayoutContext(), newValue, args);
}
finally
{
_processingItemsSourceChange = null;
}
}
else if (Layout is NonVirtualizingLayout nonVirtualLayout)
{
// Walk through all the elements and make sure they are cleared for
// non-virtualizing layouts.
foreach (var element in Children)
{
if (GetVirtualizationInfo(element).IsRealized)
{
ClearElementImpl(element);
}
}
}
}
ItemTemplateShim = new ItemTemplateWrapper(newValue);
InvalidateMeasure();
}
private void OnLayoutChanged(Layout oldValue, Layout newValue)
{
if (_isLayoutInProgress)
{
throw new InvalidOperationException("Layout cannot be changed during layout.");
}
_viewManager.OnLayoutChanging();
if (oldValue != null)
{
oldValue.UninitializeForContext(LayoutContext);
oldValue.MeasureInvalidated -= InvalidateMeasureForLayout;
oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout;
// Walk through all the elements and make sure they are cleared
foreach (var element in Children)
{
if (GetVirtualizationInfo(element).IsRealized)
{
ClearElementImpl(element);
}
}
LayoutState = null;
}
if (newValue != null)
{
newValue.InitializeForContext(LayoutContext);
newValue.MeasureInvalidated += InvalidateMeasureForLayout;
newValue.ArrangeInvalidated += InvalidateArrangeForLayout;
}
bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout;
_viewportManager.OnLayoutChanged(isVirtualizingLayout);
InvalidateMeasure();
}
private void OnItemsSourceViewChanged(object sender, NotifyCollectionChangedEventArgs args)
{
if (_isLayoutInProgress)
{
// Bad things will follow if the data changes while we are in the middle of a layout pass.
throw new InvalidOperationException("Changes in data source are not allowed during layout.");
}
if (IsProcessingCollectionChange)
{
throw new InvalidOperationException("Changes in the data source are not allowed during another change in the data source.");
}
_processingItemsSourceChange = args;
try
{
//_animationManager.OnItemsSourceChanged(sender, args);
_viewManager.OnItemsSourceChanged(sender, args);
if (Layout != null)
{
if (Layout is VirtualizingLayout virtualLayout)
{
virtualLayout.OnItemsChangedCore(GetLayoutContext(), sender, args);
}
else
{
// NonVirtualizingLayout
InvalidateMeasure();
}
}
}
finally
{
_processingItemsSourceChange = null;
}
}
private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateMeasure();
private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateArrange();
private VirtualizingLayoutContext GetLayoutContext()
{
if (_layoutContext == null)
{
_layoutContext = new RepeaterLayoutContext(this);
}
return _layoutContext;
}
}
}

11
src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs

@ -0,0 +1,11 @@
using System;
namespace Avalonia.Controls.Repeaters
{
public class ItemsRepeaterElementClearingEventArgs : EventArgs
{
internal ItemsRepeaterElementClearingEventArgs(IControl element) => Element = element;
public IControl Element { get; private set; }
internal void Update(IControl element) => Element = element;
}
}

27
src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs

@ -0,0 +1,27 @@
using System;
namespace Avalonia.Controls.Repeaters
{
public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs
{
internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int newIndex, int oldIndex)
{
Element = element;
NewIndex = newIndex;
OldIndex = oldIndex;
}
public IControl Element { get; private set; }
public int NewIndex { get; private set; }
public int OldIndex { get; private set; }
internal void Update(IControl element, int newIndex, int oldIndex)
{
Element = element;
NewIndex = newIndex;
OldIndex = oldIndex;
}
}
}

25
src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Controls.Repeaters
{
public class ItemsRepeaterElementPreparedEventArgs
{
internal ItemsRepeaterElementPreparedEventArgs(IControl element, int index)
{
Element = element;
Index = index;
}
public IControl Element { get; private set; }
public int Index { get; private set; }
internal void Update(IControl element, int index)
{
Element = element;
Index = index;
}
}
}

91
src/Avalonia.Controls/Repeaters/ItemsSourceView.cs

@ -0,0 +1,91 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
namespace Avalonia.Controls.Repeaters
{
public class ItemsSourceView : INotifyCollectionChanged, IDisposable
{
private readonly IList _inner;
private INotifyCollectionChanged _notifyCollectionChanged;
private int _cachedSize = -1;
public ItemsSourceView(IEnumerable source)
{
Contract.Requires<ArgumentNullException>(source != null);
_inner = source as IList;
if (_inner == null && source is IEnumerable<object> objectEnumerable)
{
_inner = new List<object>(objectEnumerable);
}
else
{
_inner = new List<object>(source.Cast<object>());
}
ListenToCollectionChanges();
}
public int Count
{
get
{
if (_cachedSize == -1)
{
_cachedSize = _inner.Count;
}
return _cachedSize;
}
}
public bool HasKeyIndexMapping => false;
public event NotifyCollectionChangedEventHandler CollectionChanged;
public void Dispose()
{
if (_notifyCollectionChanged != null)
{
_notifyCollectionChanged.CollectionChanged -= OnCollectionChanged;
}
}
public object GetAt(int index) => _inner[index];
public string KeyFromIndex(int index)
{
throw new NotImplementedException();
}
public int IndexFromKey(string key)
{
throw new NotImplementedException();
}
protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args)
{
_cachedSize = _inner.Count;
CollectionChanged?.Invoke(this, args);
}
private void ListenToCollectionChanges()
{
if (_inner is INotifyCollectionChanged incc)
{
incc.CollectionChanged += OnCollectionChanged;
_notifyCollectionChanged = incc;
}
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
OnItemsSourceChanged(e);
}
}
}

24
src/Avalonia.Controls/Repeaters/Layout.cs

@ -0,0 +1,24 @@
using System;
namespace Avalonia.Controls.Repeaters
{
public abstract class Layout : AvaloniaObject
{
public string LayoutId { get; set; }
public event EventHandler MeasureInvalidated;
public event EventHandler ArrangeInvalidated;
public abstract void InitializeForContext(LayoutContext context);
public abstract void UninitializeForContext(LayoutContext context);
public abstract Size Measure(LayoutContext context, Size availableSize);
public abstract Size Arrange(LayoutContext context, Size finalSize);
protected void InvalidateMeasure() => MeasureInvalidated?.Invoke(this, EventArgs.Empty);
protected void InvalidateArrange() => ArrangeInvalidated?.Invoke(this, EventArgs.Empty);
}
}

13
src/Avalonia.Controls/Repeaters/LayoutContext.cs

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Controls.Repeaters
{
public class LayoutContext : AvaloniaObject
{
public object LayoutState { get; set; }
protected virtual object LayoutStateCore { get; set; }
}
}

29
src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Controls.Repeaters
{
public class NonVirtualizingLayout : Layout
{
public override Size Arrange(LayoutContext context, Size finalSize)
{
throw new NotImplementedException();
}
public override void InitializeForContext(LayoutContext context)
{
throw new NotImplementedException();
}
public override Size Measure(LayoutContext context, Size availableSize)
{
throw new NotImplementedException();
}
public override void UninitializeForContext(LayoutContext context)
{
throw new NotImplementedException();
}
}
}

78
src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs

@ -0,0 +1,78 @@
namespace Avalonia.Controls.Repeaters
{
internal class OrientationBasedMeasures
{
public Orientation ScrollOrientation { get; set; } = Orientation.Vertical;
public double Major(in Size size) => ScrollOrientation == Orientation.Vertical ? size.Height : size.Width;
public double Minor(in Size size) => ScrollOrientation == Orientation.Vertical ? size.Width : size.Height;
public double MajorSize(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Height : rect.Width;
public double MinorSize(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Width : rect.Height;
public double MajorStart(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Y : rect.X;
public double MinorStart(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.X : rect.Y;
public double MajorEnd(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Bottom : rect.Right;
public double MinorEnd(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Right : rect.Bottom;
public void SetMajorSize(ref Rect rect, double value)
{
if (ScrollOrientation == Orientation.Vertical)
{
rect = rect.WithHeight(value);
}
else
{
rect = rect.WithWidth(value);
}
}
public void SetMinorSize(ref Rect rect, double value)
{
if (ScrollOrientation == Orientation.Vertical)
{
rect = rect.WithWidth(value);
}
else
{
rect = rect.WithHeight(value);
}
}
public void SetMajorStart(ref Rect rect, double value)
{
if (ScrollOrientation == Orientation.Vertical)
{
rect = rect.WithY(value);
}
else
{
rect = rect.WithX(value);
}
}
public void SetMinorStart(ref Rect rect, double value)
{
if (ScrollOrientation == Orientation.Vertical)
{
rect = rect.WithX(value);
}
else
{
rect = rect.WithY(value);
}
}
public Point MinorMajorPoint(double minor, double major)
{
return ScrollOrientation == Orientation.Vertical ?
new Point(minor, major) :
new Point(major, minor);
}
public Size MinorMajorSize(double minor, double major)
{
return ScrollOrientation == Orientation.Vertical ?
new Size(minor, major) :
new Size(major, minor);
}
}
}

101
src/Avalonia.Controls/Repeaters/RecyclePool.cs

@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Controls.Templates;
namespace Avalonia.Controls.Repeaters
{
internal class RecyclePool
{
public static readonly AttachedProperty<IDataTemplate> OriginTemplateProperty =
AvaloniaProperty.RegisterAttached<Control, IDataTemplate>("OriginTemplate", typeof(RecyclePool));
private static ConditionalWeakTable<IDataTemplate, RecyclePool> s_pools = new ConditionalWeakTable<IDataTemplate, RecyclePool>();
private readonly Dictionary<string, List<ElementInfo>> _elements = new Dictionary<string, List<ElementInfo>>();
public static RecyclePool GetPoolInstance(IDataTemplate dataTemplate)
{
s_pools.TryGetValue(dataTemplate, out var result);
return result;
}
public static void SetPoolInstance(IDataTemplate dataTemplate, RecyclePool value) => s_pools.Add(dataTemplate, value);
public void PutElement(IControl element, string key, IControl owner)
{
var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner);
var elementInfo = new ElementInfo(element, ownerAsPanel);
if (!_elements.TryGetValue(key, out var pool))
{
pool = new List<ElementInfo>();
_elements.Add(key, pool);
}
pool.Add(elementInfo);
}
public IControl TryGetElement(string key, IControl owner)
{
if (_elements.TryGetValue(key, out var elements))
{
if (elements.Count > 0)
{
// Prefer an element from the same owner or with no owner so that we don't incur
// the enter/leave cost during recycling.
// TODO: prioritize elements with the same owner to those without an owner.
var elementInfo = elements.FirstOrDefault(x => x.Owner == owner) ?? elements.LastOrDefault();
elements.Remove(elementInfo);
var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner);
if (elementInfo.Owner != null && elementInfo.Owner != ownerAsPanel)
{
// Element is still under its parent. remove it from its parent.
var panel = elementInfo.Owner;
if (panel != null)
{
int childIndex = panel.Children.IndexOf(elementInfo.Element);
if (childIndex == -1)
{
throw new KeyNotFoundException("ItemsRepeater's child not found in its Children collection.");
}
panel.Children.RemoveAt(childIndex);
}
}
return elementInfo.Element;
}
}
return null;
}
private IPanel EnsureOwnerIsPanelOrNull(IControl owner)
{
if (owner is IPanel panel)
{
return panel;
}
else if (owner != null)
{
throw new InvalidOperationException("Owner must be IPanel or null.");
}
return null;
}
private class ElementInfo
{
public ElementInfo(IControl element, IPanel owner)
{
Element = element;
Owner = owner;
}
public IControl Element { get; }
public IPanel Owner { get;}
}
}
}

59
src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Controls.Repeaters
{
internal class RepeaterLayoutContext : VirtualizingLayoutContext
{
private readonly ItemsRepeater _owner;
public RepeaterLayoutContext(ItemsRepeater owner)
{
_owner = owner;
}
protected override Point LayoutOriginCore
{
get => _owner.LayoutOrigin;
set => _owner.LayoutOrigin = value;
}
protected override object LayoutStateCore
{
get => _owner.LayoutState;
set => _owner.LayoutState = value;
}
protected override int RecommendedAnchorIndexCore
{
get
{
int anchorIndex = -1;
var anchor = _owner.SuggestedAnchor;
if (anchor != null)
{
anchorIndex = _owner.GetElementIndex(anchor);
}
return anchorIndex;
}
}
protected override int ItemCountCore() => _owner.ItemsSourceView?.Count ?? 0;
protected override IControl GetOrCreateElementAtCore(int index, ElementRealizationOptions options)
{
return _owner.GetElementImpl(
index,
(options & ElementRealizationOptions.ForceCreate) != 0,
(options & ElementRealizationOptions.SuppressAutoRecycle) != 0);
}
protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index);
protected override void RecycleElementCore(IControl element) => _owner.ClearElementImpl(element);
protected override Rect RealizationRectCore() => _owner.RealizationWindow;
}
}

298
src/Avalonia.Controls/Repeaters/StackLayout.cs

@ -0,0 +1,298 @@
using System;
using System.Collections.Specialized;
namespace Avalonia.Controls.Repeaters
{
public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates
{
public static readonly AvaloniaProperty<Orientation> OrientationProperty
= StackPanel.OrientationProperty.AddOwner<StackLayout>();
public static readonly AvaloniaProperty<double> SpacingProperty
= StackPanel.SpacingProperty.AddOwner<StackLayout>();
private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures();
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
public double Spacing
{
get => GetValue(SpacingProperty);
set => SetValue(SpacingProperty, value);
}
public Rect GetExtent(
Size availableSize,
VirtualizingLayoutContext context,
IControl firstRealized,
int firstRealizedItemIndex,
Rect firstRealizedLayoutBounds,
IControl lastRealized,
int lastRealizedItemIndex,
Rect lastRealizedLayoutBounds)
{
var extent = new Rect();
// Constants
int itemsCount = context.ItemCount;
var stackState = (StackLayoutState)context.LayoutState;
double averageElementSize = GetAverageElementSize(availableSize, context, stackState) + Spacing;
_orientation.SetMinorSize(ref extent, stackState.MaxArrangeBounds);
_orientation.SetMajorSize(ref extent, Math.Max(0.0f, itemsCount * averageElementSize - Spacing));
if (itemsCount > 0)
{
if (firstRealized != null)
{
//MUX_ASSERT(lastRealized);
_orientation.SetMajorStart(
ref extent,
_orientation.MajorStart(firstRealizedLayoutBounds) - firstRealizedItemIndex * averageElementSize);
var remainingItems = itemsCount - lastRealizedItemIndex - 1;
_orientation.SetMajorSize(
ref extent,
_orientation.MajorEnd(lastRealizedLayoutBounds) -
_orientation.MajorStart(extent) +
(remainingItems * averageElementSize));
}
}
return extent;
}
public void OnElementMeasured(
IControl element,
int index,
Size availableSize,
Size measureSize,
Size desiredSize,
Size provisionalArrangeSize,
VirtualizingLayoutContext context)
{
if (context is VirtualizingLayoutContext virtualContext)
{
var stackState = (StackLayoutState)virtualContext.LayoutState;
var provisionalArrangeSizeWinRt = provisionalArrangeSize;
stackState.OnElementMeasured(
index,
_orientation.Major(provisionalArrangeSizeWinRt),
_orientation.Minor(provisionalArrangeSizeWinRt));
}
}
Size IFlowLayoutAlgorithmDelegates.Algorithm_GetMeasureSize(
int index,
Size availableSize,
VirtualizingLayoutContext context) => availableSize;
Size IFlowLayoutAlgorithmDelegates.Algorithm_GetProvisionalArrangeSize(
int index,
Size measureSize,
Size desiredSize,
VirtualizingLayoutContext context)
{
var measureSizeMinor = _orientation.Minor(measureSize);
return _orientation.MinorMajorSize(
double.IsInfinity(measureSizeMinor) ?
Math.Max(measureSizeMinor, _orientation.Minor(desiredSize)) :
_orientation.Minor(desiredSize),
_orientation.Major(desiredSize));
}
bool IFlowLayoutAlgorithmDelegates.Algorithm_ShouldBreakLine(int index, double remainingSpace) => true;
FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForRealizationRect(
Size availableSize,
VirtualizingLayoutContext context) => GetAnchorForRealizationRect(availableSize, context);
FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForTargetElement(
int targetIndex,
Size availableSize,
VirtualizingLayoutContext context)
{
double offset = double.NaN;
int index = -1;
int itemsCount = context.ItemCount;
if (targetIndex >= 0 && targetIndex < itemsCount)
{
index = targetIndex;
var state = (StackLayoutState)context.LayoutState;
double averageElementSize = GetAverageElementSize(availableSize, context, state) + Spacing;
offset = index * averageElementSize + _orientation.MajorStart(state.FlowAlgorithm.LastExtent);
}
return new FlowLayoutAnchorInfo { Index = index, Offset = offset };
}
Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent(
Size availableSize,
VirtualizingLayoutContext context,
IControl firstRealized,
int firstRealizedItemIndex,
Rect firstRealizedLayoutBounds,
IControl lastRealized,
int lastRealizedItemIndex,
Rect lastRealizedLayoutBounds)
{
return GetExtent(
availableSize,
context,
firstRealized,
firstRealizedItemIndex,
firstRealizedLayoutBounds,
lastRealized,
lastRealizedItemIndex,
lastRealizedLayoutBounds);
}
void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(IControl element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context)
{
OnElementMeasured(
element,
index,
availableSize,
measureSize,
desiredSize,
provisionalArrangeSize,
context);
}
void IFlowLayoutAlgorithmDelegates.Algorithm_OnLineArranged(int startIndex, int countInLine, double lineSize, VirtualizingLayoutContext context)
{
}
internal FlowLayoutAnchorInfo GetAnchorForRealizationRect(
Size availableSize,
VirtualizingLayoutContext context)
{
int anchorIndex = -1;
double offset = double.NaN;
// Constants
int itemsCount = context.ItemCount;
if (itemsCount > 0)
{
var realizationRect = context.RealizationRect;
var state = (StackLayoutState)context.LayoutState;
var lastExtent = state.FlowAlgorithm.LastExtent;
double averageElementSize = GetAverageElementSize(availableSize, context, state) + Spacing;
double realizationWindowOffsetInExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent);
double majorSize = _orientation.MajorSize(lastExtent) == 0 ? Math.Max(0.0, averageElementSize * itemsCount - Spacing) : _orientation.MajorSize(lastExtent);
if (itemsCount > 0 &&
_orientation.MajorSize(realizationRect) > 0 &&
realizationWindowOffsetInExtent + _orientation.MajorSize(realizationRect) >= 0 && realizationWindowOffsetInExtent <= majorSize)
{
anchorIndex = (int) (realizationWindowOffsetInExtent / averageElementSize);
offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent);
anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorIndex));
}
}
return new FlowLayoutAnchorInfo { Index = anchorIndex, Offset = offset, };
}
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
{
var state = context.LayoutState;
var stackState = state as StackLayoutState;
if (stackState == null)
{
if (state != null)
{
throw new InvalidOperationException("LayoutState must derive from StackLayoutState.");
}
// Custom deriving layouts could potentially be stateful.
// If that is the case, we will just create the base state required by UniformGridLayout ourselves.
stackState = new StackLayoutState();
}
stackState.InitializeForContext(context, this);
}
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
var stackState = (StackLayoutState)context.LayoutState;
stackState.UninitializeForContext(context);
}
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize,
context,
false,
0,
Spacing,
_orientation.ScrollOrientation,
LayoutId);
return new Size(desiredSize.Width, desiredSize.Height);
}
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
var value = GetFlowAlgorithm(context).Arrange(
finalSize,
context,
FlowLayoutAlgorithm.LineAlignment.Start,
LayoutId);
((StackLayoutState)context.LayoutState).OnArrangeLayoutEnd();
return new Size(value.Width, value.Height);
}
protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
{
GetFlowAlgorithm(context).OnItemsSourceChanged(source, args, context);
// Always invalidate layout to keep the view accurate.
InvalidateLayout();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == OrientationProperty)
{
//Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation.
//Horizontal Orientation means we have a Horizontal ScrollOrientation.
_orientation.ScrollOrientation = (Orientation)e.NewValue;
}
InvalidateLayout();
}
private double GetAverageElementSize(
Size availableSize,
VirtualizingLayoutContext context,
StackLayoutState stackLayoutState)
{
double averageElementSize = 0;
if (context.ItemCount > 0)
{
if (stackLayoutState.TotalElementsMeasured == 0)
{
var tmpElement = context.GetOrCreateElementAt(0, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
stackLayoutState.FlowAlgorithm.MeasureElement(tmpElement, 0, availableSize, context);
context.RecycleElement(tmpElement);
}
averageElementSize = Math.Round(stackLayoutState.TotalElementSize / stackLayoutState.TotalElementsMeasured);
}
return averageElementSize;
}
private void InvalidateLayout() => InvalidateMeasure();
private FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((StackLayoutState)context.LayoutState).FlowAlgorithm;
}
}

53
src/Avalonia.Controls/Repeaters/StackLayoutState.cs

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Controls.Repeaters
{
public class StackLayoutState
{
private const int BufferSize = 100;
private readonly List<double> _estimationBuffer = new List<double>();
internal FlowLayoutAlgorithm FlowAlgorithm { get; } = new FlowLayoutAlgorithm();
internal double MaxArrangeBounds { get; private set; }
internal int TotalElementsMeasured { get; private set; }
internal double TotalElementSize { get; private set; }
internal void InitializeForContext(VirtualizingLayoutContext context, IFlowLayoutAlgorithmDelegates callbacks)
{
FlowAlgorithm.InitializeForContext(context, callbacks);
if (_estimationBuffer.Count == 0)
{
_estimationBuffer.AddRange(Enumerable.Repeat(0.0, BufferSize));
}
context.LayoutState = this;
}
internal void UninitializeForContext(VirtualizingLayoutContext context)
{
FlowAlgorithm.UninitializeForContext(context);
}
internal void OnElementMeasured(int elementIndex, double majorSize, double minorSize)
{
int estimationBufferIndex = elementIndex % _estimationBuffer.Count;
bool alreadyMeasured = _estimationBuffer[estimationBufferIndex] != 0;
if (!alreadyMeasured)
{
TotalElementsMeasured++;
}
TotalElementSize -= _estimationBuffer[estimationBufferIndex];
TotalElementSize += majorSize;
_estimationBuffer[estimationBufferIndex] = majorSize;
MaxArrangeBounds = Math.Max(MaxArrangeBounds, minorSize);
}
internal void OnArrangeLayoutEnd() => MaxArrangeBounds = 0;
}
}

54
src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs

@ -0,0 +1,54 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Controls.Repeaters
{
internal class UniqueIdElementPool : IEnumerable<KeyValuePair<string, IControl>>
{
private readonly Dictionary<string, IControl> _elementMap = new Dictionary<string, IControl>();
private readonly ItemsRepeater _owner;
public UniqueIdElementPool(ItemsRepeater owner) => _owner = owner;
public void Add(IControl element)
{
//MUX_ASSERT(_owner.ItemsSourceView.HasKeyIndexMapping);
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var key = virtInfo.UniqueId;
if (_elementMap.ContainsKey(key))
{
throw new InvalidOperationException($"The unique id provided ({key}) is not unique.");
}
_elementMap.Add(key, element);
}
public IControl Remove(int index)
{
//MUX_ASSERT(_owner.ItemsSourceView.HasKeyIndexMapping);
// Check if there is already a element in the mapping and if so, use it.
string key = _owner.ItemsSourceView.KeyFromIndex(index);
if (_elementMap.TryGetValue(key, out var element))
{
_elementMap.Remove(key);
}
return element;
}
public void Clear()
{
//MUX_ASSERT(_owner.ItemsSourceView.HasKeyIndexMapping);
_elementMap.Clear();
}
public IEnumerator<KeyValuePair<string, IControl>> GetEnumerator() => _elementMap.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

787
src/Avalonia.Controls/Repeaters/ViewManager.cs

@ -0,0 +1,787 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Repeaters
{
internal sealed class ViewManager
{
private const int FirstRealizedElementIndexDefault = int.MaxValue;
private const int LastRealizedElementIndexDefault = int.MinValue;
private readonly ItemsRepeater _owner;
private readonly List<PinnedElementInfo> _pinnedPool = new List<PinnedElementInfo>();
private readonly UniqueIdElementPool _resetPool;
private IControl _lastFocusedElement;
private bool _isDataSourceStableResetPending;
private ElementFactoryGetArgs _elementFactoryGetArgs;
private ElementFactoryRecycleArgs _elementFactoryRecycleArgs;
private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault;
private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault;
private bool _eventsSubscribed;
public ViewManager(ItemsRepeater owner)
{
_owner = owner;
_resetPool = new UniqueIdElementPool(owner);
}
public IControl GetElement(int index, bool forceCreate, bool suppressAutoRecycle)
{
var element = forceCreate ? null : GetElementIfAlreadyHeldByLayout(index);
if (element == null)
{
// check if this is the anchor made through repeater in preparation
// for a bring into view.
var madeAnchor = _owner.MadeAnchor;
if (madeAnchor != null)
{
var anchorVirtInfo = ItemsRepeater.TryGetVirtualizationInfo(madeAnchor);
if (anchorVirtInfo.Index == index)
{
element = madeAnchor;
}
}
}
if (element == null) { element = GetElementFromUniqueIdResetPool(index); };
if (element == null) { element = GetElementFromPinnedElements(index); }
if (element == null) { element = GetElementFromElementFactory(index); }
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
if (suppressAutoRecycle)
{
virtInfo.AutoRecycleCandidate = false;
}
else
{
virtInfo.AutoRecycleCandidate = true;
virtInfo.KeepAlive = true;
}
return element;
}
public void ClearElement(IControl element, bool isClearedDueToCollectionChange)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var index = virtInfo.Index;
bool cleared =
ClearElementToUniqueIdResetPool(element, virtInfo) ||
ClearElementToPinnedPool(element, virtInfo, isClearedDueToCollectionChange);
if (!cleared)
{
ClearElementToElementFactory(element);
}
// Both First and Last indices need to be valid or default.
if (index == _firstRealizedElementIndexHeldByLayout && index == _lastRealizedElementIndexHeldByLayout)
{
// First and last were pointing to the same element and that is going away.
InvalidateRealizedIndicesHeldByLayout();
}
else if (index == _firstRealizedElementIndexHeldByLayout)
{
// The FirstElement is going away, shrink the range by one.
++_firstRealizedElementIndexHeldByLayout;
}
else if (index == _lastRealizedElementIndexHeldByLayout)
{
// Last element is going away, shrink the range by one at the end.
--_lastRealizedElementIndexHeldByLayout;
}
else
{
// Index is either outside the range we are keeping track of or inside the range.
// In both these cases, we just keep the range we have. If this clear was due to
// a collection change, then in the CollectionChanged event, we will invalidate these guys.
}
}
public void ClearElementToElementFactory(IControl element)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var clearedIndex = virtInfo.Index;
_owner.OnElementClearing(element);
if (_elementFactoryRecycleArgs == null)
{
// Create one.
_elementFactoryRecycleArgs = new ElementFactoryRecycleArgs();
}
var context = _elementFactoryRecycleArgs;
context.Element = element;
context.Parent = _owner;
_owner.ItemTemplateShim.RecycleElement(context);
context.Element = null;
context.Parent = null;
virtInfo.MoveOwnershipToElementFactory();
//_phaser.StopPhasing(element, virtInfo);
if (_lastFocusedElement == element)
{
// Focused element is going away. Remove the tracked last focused element
// and pick a reasonable next focus if we can find one within the layout
// realized elements.
MoveFocusFromClearedIndex(clearedIndex);
}
}
private void MoveFocusFromClearedIndex(int clearedIndex)
{
IControl focusedChild = null;
var focusCandidate = FindFocusCandidate(clearedIndex, focusedChild);
if (focusCandidate != null)
{
//var focusState = _lastFocusedElement?.FocusState ?? FocusState.Programmatic;
// If the last focused element has focus, use its focus state, if not use programmatic.
//focusState = focusState == FocusState.Unfocused ? FocusState.Programmatic : focusState;
focusCandidate.Focus();
_lastFocusedElement = focusedChild;
// Add pin to hold the focused element.
UpdatePin(focusedChild, true /* addPin */);
}
else
{
// We could not find a candiate.
_lastFocusedElement = null;
}
}
IControl FindFocusCandidate(int clearedIndex, IControl focusedChild)
{
// Walk through all the children and find elements with index before and after the cleared index.
// Note that during a delete the next element would now have the same index.
int previousIndex = int.MinValue;
int nextIndex = int.MaxValue;
IControl nextElement = null;
IControl previousElement = null;
foreach (var child in _owner.Children)
{
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child);
if (virtInfo?.IsHeldByLayout == true)
{
int currentIndex = virtInfo.Index;
if (currentIndex < clearedIndex)
{
if (currentIndex > previousIndex)
{
previousIndex = currentIndex;
previousElement = child;
}
}
else if (currentIndex >= clearedIndex)
{
// Note that we use >= above because if we deleted the focused element,
// the next element would have the same index now.
if (currentIndex < nextIndex)
{
nextIndex = currentIndex;
nextElement = child;
}
}
}
}
// Find the next element if one exists, if not use the previous element.
// If the container itself is not focusable, find a descendent that is.
IControl focusCandidate = null;
if (nextElement != null)
{
focusCandidate = nextElement as IControl;
if (focusCandidate != null)
{
////var firstFocus = FocusManager.FindFirstFocusableElement(nextElement);
////if (firstFocus != null)
////{
//// focusCandidate = firstFocus as IControl;
////}
}
}
if (focusCandidate == null && previousElement != null)
{
focusCandidate = previousElement as IControl;
if (previousElement != null)
{
////var lastFocus = FocusManager.FindLastFocusableElement(previousElement);
////if (lastFocus != null)
////{
//// focusCandidate = lastFocus as IControl;
////}
}
}
return focusCandidate;
}
public int GetElementIndex(VirtualizationInfo virtInfo)
{
if (virtInfo == null)
{
throw new ArgumentException("Element is not a child of this ItemsRepeater.");
}
return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1;
}
public void PrunePinnedElements()
{
EnsureEventSubscriptions();
// Go through pinned elements and make sure they still have
// a reason to be pinned.
for (var i = 0; i < _pinnedPool.Count; ++i)
{
var elementInfo = _pinnedPool[i];
var virtInfo = elementInfo.VirtualizationInfo;
//MUX_ASSERT(virtInfo.Owner() == ElementOwner.PinnedPool);
if (!virtInfo.IsPinned)
{
_pinnedPool.RemoveAt(i);
--i;
// Pinning was the only thing keeping this element alive.
ClearElementToElementFactory(elementInfo.PinnedElement);
}
}
}
public void UpdatePin(IControl element, bool addPin)
{
var parent = element.VisualParent;
var child = (IVisual)element;
while (parent != null)
{
if (parent is ItemsRepeater repeater)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo((IControl)child);
if (virtInfo.IsRealized)
{
if (addPin)
{
virtInfo.AddPin();
}
else if (virtInfo.IsPinned)
{
if (virtInfo.RemovePin() == 0)
{
// ElementFactory is invoked during the measure pass.
// We will clear the element then.
repeater.InvalidateMeasure();
}
}
}
}
child = parent;
parent = child.VisualParent;
}
}
public void OnItemsSourceChanged(object sender, NotifyCollectionChangedEventArgs args)
{
// Note: For items that have been removed, the index will not be touched. It will hold
// the old index before it was removed. It is not valid anymore.
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
{
var newIndex = args.NewStartingIndex;
var newCount = args.NewItems.Count;
EnsureFirstLastRealizedIndices();
if (newIndex <= _lastRealizedElementIndexHeldByLayout)
{
_lastRealizedElementIndexHeldByLayout += newCount;
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var dataIndex = virtInfo.Index;
if (virtInfo.IsRealized && dataIndex >= newIndex)
{
UpdateElementIndex(element, virtInfo, dataIndex + newCount);
}
}
}
else
{
// Indices held by layout are not affected
// We could still have items in the pinned elements that need updates. This is usually a very small vector.
for (var i = 0; i < _pinnedPool.Count; ++i)
{
var elementInfo = _pinnedPool[i];
var virtInfo = elementInfo.VirtualizationInfo;
var dataIndex = virtInfo.Index;
if (virtInfo.IsRealized && dataIndex >= newIndex)
{
var element = elementInfo.PinnedElement;
UpdateElementIndex(element, virtInfo, dataIndex + newCount);
}
}
}
break;
}
case NotifyCollectionChangedAction.Replace:
{
// Requirement: oldStartIndex == newStartIndex. It is not a replace if this is not true.
// Two cases here
// case 1: oldCount == newCount
// indices are not affected. nothing to do here.
// case 2: oldCount != newCount
// Replaced with less or more items. This is like an insert or remove
// depending on the counts.
var oldStartIndex = args.OldStartingIndex;
var newStartingIndex = args.NewStartingIndex;
var oldCount = args.OldItems.Count;
var newCount = args.NewItems.Count;
if (oldStartIndex != newStartingIndex)
{
throw new NotSupportedException("Replace is only allowed with OldStartingIndex equals to NewStartingIndex.");
}
if (oldCount == 0)
{
throw new NotSupportedException("Replace notification with args.OldItemsCount value of 0 is not allowed. Use Insert action instead.");
}
if (newCount == 0)
{
throw new NotSupportedException("Replace notification with args.NewItemCount value of 0 is not allowed. Use Remove action instead.");
}
int countChange = newCount - oldCount;
if (countChange != 0)
{
// countChange > 0 : countChange items were added
// countChange < 0 : -countChange items were removed
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var dataIndex = virtInfo.Index;
if (virtInfo.IsRealized)
{
if (dataIndex >= oldStartIndex + oldCount)
{
UpdateElementIndex(element, virtInfo, dataIndex + countChange);
}
}
}
EnsureFirstLastRealizedIndices();
_lastRealizedElementIndexHeldByLayout += countChange;
}
break;
}
case NotifyCollectionChangedAction.Remove:
{
var oldStartIndex = args.OldStartingIndex;
var oldCount = args.OldItems.Count;
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var dataIndex = virtInfo.Index;
if (virtInfo.IsRealized)
{
if (virtInfo.AutoRecycleCandidate && oldStartIndex <= dataIndex && dataIndex < oldStartIndex + oldCount)
{
// If we are doing the mapping, remove the element who's data was removed.
_owner.ClearElementImpl(element);
}
else if (dataIndex >= (oldStartIndex + oldCount))
{
UpdateElementIndex(element, virtInfo, dataIndex - oldCount);
}
}
}
InvalidateRealizedIndicesHeldByLayout();
break;
}
case NotifyCollectionChangedAction.Reset:
if (_owner.ItemsSourceView.HasKeyIndexMapping)
{
_isDataSourceStableResetPending = true;
}
// Walk through all the elements and make sure they are cleared, they will go into
// the stable id reset pool.
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate)
{
_owner.ClearElementImpl(element);
}
}
InvalidateRealizedIndicesHeldByLayout();
break;
}
}
private void EnsureFirstLastRealizedIndices()
{
if (_firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault)
{
// This will ensure that the indexes are updated.
GetElementIfAlreadyHeldByLayout(0);
}
}
public void OnLayoutChanging()
{
if (_owner.ItemsSourceView?.HasKeyIndexMapping == true)
{
_isDataSourceStableResetPending = true;
}
}
public void OnOwnerArranged()
{
if (_isDataSourceStableResetPending)
{
_isDataSourceStableResetPending = false;
foreach (var entry in _resetPool)
{
// TODO: Task 14204306: ItemsRepeater: Find better focus candidate when focused element is deleted in the ItemsSource.
// Focused element is getting cleared. Need to figure out semantics on where
// focus should go when the focused element is removed from the data collection.
ClearElement(entry.Value, true /* isClearedDueToCollectionChange */);
}
_resetPool.Clear();
}
}
// We optimize for the case where index is not realized to return null as quickly as we can.
// Flow layouts manage containers on their own and will never ask for an index that is already realized.
// If an index that is realized is requested by the layout, we unfortunately have to walk the
// children. Not ideal, but a reasonable default to provide consistent behavior between virtualizing
// and non-virtualizing hosts.
private IControl GetElementIfAlreadyHeldByLayout(int index)
{
IControl element = null;
bool cachedFirstLastIndicesInvalid = _firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault;
//MUX_ASSERT(!cachedFirstLastIndicesInvalid || m_lastRealizedElementIndexHeldByLayout == LastRealizedElementIndexDefault);
bool isRequestedIndexInRealizedRange = (_firstRealizedElementIndexHeldByLayout <= index && index <= _lastRealizedElementIndexHeldByLayout);
if (cachedFirstLastIndicesInvalid || isRequestedIndexInRealizedRange)
{
// Both First and Last indices need to be valid or default.
//MUX_ASSERT((m_firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault && m_lastRealizedElementIndexHeldByLayout == LastRealizedElementIndexDefault) ||
// (m_firstRealizedElementIndexHeldByLayout != FirstRealizedElementIndexDefault && m_lastRealizedElementIndexHeldByLayout != LastRealizedElementIndexDefault));
foreach (var child in _owner.Children)
{
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child);
if (virtInfo?.IsHeldByLayout == true)
{
// Only give back elements held by layout. If someone else is holding it, they will be served by other methods.
int childIndex = virtInfo.Index;
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, childIndex);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, childIndex);
if (virtInfo.Index == index)
{
element = child;
// If we have valid first/last indices, we don't have to walk the rest, but if we
// do not, then we keep walking through the entire children collection to get accurate
// indices once.
if (!cachedFirstLastIndicesInvalid)
{
break;
}
}
}
}
}
return element;
}
private IControl GetElementFromUniqueIdResetPool(int index)
{
IControl element = null;
// See if you can get it from the reset pool.
if (_isDataSourceStableResetPending)
{
element = _resetPool.Remove(index);
if (element != null)
{
// Make sure that the index is updated to the current one
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool();
UpdateElementIndex(element, virtInfo, index);
}
}
return element;
}
private IControl GetElementFromPinnedElements(int index)
{
IControl element = null;
// See if you can find something among the pinned elements.
for (var i = 0; i < _pinnedPool.Count; ++i)
{
var elementInfo = _pinnedPool[i];
var virtInfo = elementInfo.VirtualizationInfo;
if (virtInfo.Index == index)
{
_pinnedPool.RemoveAt(i);
element = elementInfo.PinnedElement;
elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool();
break;
}
}
return element;
}
private IControl GetElementFromElementFactory(int index)
{
// The view generator is the provider of last resort.
var itemTemplateFactory = _owner.ItemTemplateShim;
if (itemTemplateFactory == null)
{
// If no ItemTemplate was provided, use a default
var factory = FuncDataTemplate.Default;
_owner.ItemTemplate = factory;
itemTemplateFactory = _owner.ItemTemplateShim;
}
var data = _owner.ItemsSourceView.GetAt(index);
if (_elementFactoryGetArgs == null)
{
// Create one.
_elementFactoryGetArgs = new ElementFactoryGetArgs();
}
var args = _elementFactoryGetArgs;
args.Data = data;
args.Parent = _owner;
args.Index= index;
var element = itemTemplateFactory.GetElement(args);
args.Data = null;
args.Parent = null;
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
if (virtInfo == null)
{
virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element);
}
// Prepare the element
// If we are phasing, run phase 0 before setting DataContext. If phase 0 is not
// run before setting DataContext, when setting DataContext all the phases will be
// run in the OnDataContextChanged handler in code generated by the xaml compiler (code-gen).
var extension = false; ////CachedVisualTreeHelpers.GetDataTemplateComponent(element);
if (extension)
{
////// Clear out old data.
////extension.Recycle();
////int nextPhase = VirtualizationInfo.PhaseReachedEnd;
////// Run Phase 0
////extension.ProcessBindings(data, index, 0 /* currentPhase */, nextPhase);
////// Setup phasing information, so that Phaser can pick up any pending phases left.
////// Update phase on virtInfo. Set data and templateComponent only if x:Phase was used.
////virtInfo.UpdatePhasingInfo(nextPhase, nextPhase > 0 ? data : null, nextPhase > 0 ? extension : null);
}
else
{
// Set data context only if no x:Bind was used. ie. No data template component on the root.
element.DataContext = data;
}
virtInfo.MoveOwnershipToLayoutFromElementFactory(
index,
/* uniqueId: */
_owner.ItemsSourceView.HasKeyIndexMapping ?
_owner.ItemsSourceView.KeyFromIndex(index) :
string.Empty);
// The view generator is the only provider that prepares the element.
var repeater = _owner;
// Add the element to the children collection here before raising OnElementPrepared so
// that handlers can walk up the tree in case they want to find their IndexPath in the
// nested case.
var children = repeater.Children;
if (element.VisualParent != repeater)
{
children.Add(element);
}
////repeater.AnimationManager.OnElementPrepared(element);
repeater.OnElementPrepared(element, index);
////_phaser.PhaseElement(element, virtInfo);
// Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
return element;
}
private bool ClearElementToUniqueIdResetPool(IControl element, VirtualizationInfo virtInfo)
{
if (_isDataSourceStableResetPending)
{
_resetPool.Add(element);
virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout();
}
return _isDataSourceStableResetPending;
}
private bool ClearElementToAnimator(IControl element, VirtualizationInfo virtInfo)
{
return false;
////bool cleared = _owner.AnimationManager.ClearElement(element);
////if (cleared)
////{
//// int clearedIndex = virtInfo.Index;
//// virtInfo.MoveOwnershipToAnimator();
//// if (_lastFocusedElement == element)
//// {
//// // Focused element is going away. Remove the tracked last focused element
//// // and pick a reasonable next focus if we can find one within the layout
//// // realized elements.
//// MoveFocusFromClearedIndex(clearedIndex);
//// }
////}
////return cleared;
}
private bool ClearElementToPinnedPool(IControl element, VirtualizationInfo virtInfo, bool isClearedDueToCollectionChange)
{
if (_isDataSourceStableResetPending)
{
_resetPool.Add(element);
virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout();
}
return _isDataSourceStableResetPending;
}
private void UpdateFocusedElement()
{
IControl focusedElement = null;
var child = FocusManager.Instance.Current;
if (child != null)
{
var parent = child.VisualParent;
var owner = _owner;
// Find out if the focused element belongs to one of our direct
// children.
while (parent != null)
{
if (parent is ItemsRepeater repeater)
{
var element = child as IControl;
if (repeater == owner && ItemsRepeater.GetVirtualizationInfo(element).IsRealized)
{
focusedElement = element;
}
break;
}
child = parent as IInputElement;
parent = child.VisualParent;
}
}
// If the focused element has changed,
// we need to unpin the old one and pin the new one.
if (_lastFocusedElement != focusedElement)
{
if (_lastFocusedElement != null)
{
UpdatePin(_lastFocusedElement, false /* addPin */);
}
if (focusedElement != null)
{
UpdatePin(focusedElement, true /* addPin */);
}
_lastFocusedElement = focusedElement;
}
}
private void OnFocusChanged(object sender, RoutedEventArgs e) => UpdateFocusedElement();
private void EnsureEventSubscriptions()
{
if (!_eventsSubscribed)
{
_owner.GotFocus += OnFocusChanged;
_owner.LostFocus += OnFocusChanged;
}
}
private void UpdateElementIndex(IControl element, VirtualizationInfo virtInfo, int index)
{
var oldIndex = virtInfo.Index;
if (oldIndex != index)
{
virtInfo.UpdateIndex(index);
_owner.OnElementIndexChanged(element, oldIndex, index);
}
}
private void InvalidateRealizedIndicesHeldByLayout()
{
_firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault;
_lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault;
}
private struct PinnedElementInfo
{
public PinnedElementInfo(IControl element)
{
PinnedElement = element;
VirtualizationInfo = ItemsRepeater.GetVirtualizationInfo(element);
}
public IControl PinnedElement { get; }
public VirtualizationInfo VirtualizationInfo { get; }
}
}
}

463
src/Avalonia.Controls/Repeaters/ViewportManager.cs

@ -0,0 +1,463 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Layout;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Repeaters
{
internal class ViewportManager
{
private const double CacheBufferPerSideInflationPixelDelta = 40.0;
private readonly ItemsRepeater _owner;
private IControl _makeAnchorElement;
private bool _isAnchorOutsideRealizedRange;
private Task _cacheBuildAction;
private Rect _visibleWindow;
private Rect _layoutExtent;
// This is the expected shift by the layout.
private Point _expectedViewportShift;
// This is what is pending and not been accounted for.
// Sometimes the scrolling surface cannot service a shift (for example
// it is already at the top and cannot shift anymore.)
private Point _pendingViewportShift;
// Unshiftable shift amount that this view manager can
// handle on its own to fake it to the layout as if the shift
// actually happened. This can happen in cases where no scrollviewer
// in the parent chain can scroll in the shift direction.
private Point _unshiftableShift;
private double _maximumHorizontalCacheLength = 2.0;
private double _maximumVerticalCacheLength = 2.0;
private double _horizontalCacheBufferPerSide;
private double _verticalCacheBufferPerSide;
private bool _isBringIntoViewInProgress;
// For non-virtualizing layouts, we do not need to keep
// updating viewports and invalidating measure often. So when
// a non virtualizing layout is used, we stop doing all that work.
bool _managingViewportDisabled;
private IDisposable _effectiveViewportChangedRevoker;
private bool _layoutUpdatedSubscribed;
public ViewportManager(ItemsRepeater owner)
{
_owner = owner;
}
// TODO: Implement
public IControl SuggestedAnchor => null;
// TODO: Implement
public bool HasScroller => false;
public IControl MadeAnchor => _makeAnchorElement;
public double HorizontalCacheLength
{
get => _maximumHorizontalCacheLength;
set
{
if (_maximumHorizontalCacheLength != value)
{
ValidateCacheLength(value);
_maximumHorizontalCacheLength = value;
ResetCacheBuffer();
}
}
}
public double VerticalCacheLength
{
get => _maximumVerticalCacheLength;
set
{
if (_maximumVerticalCacheLength != value)
{
ValidateCacheLength(value);
_maximumVerticalCacheLength = value;
ResetCacheBuffer();
}
}
}
private Rect GetLayoutVisibleWindowDiscardAnchor()
{
var visibleWindow = _visibleWindow;
if (HasScroller)
{
visibleWindow = new Rect(
visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X,
visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y,
visibleWindow.Width,
visibleWindow.Height);
}
return visibleWindow;
}
public Rect GetLayoutVisibleWindow()
{
var visibleWindow = _visibleWindow;
if (_makeAnchorElement != null)
{
// The anchor is not necessarily laid out yet. Its position should default
// to zero and the layout origin is expected to change once layout is done.
// Until then, we need a window that's going to protect the anchor from
// getting recycled.
visibleWindow = visibleWindow.WithX(0).WithY(0);
}
else if (HasScroller)
{
visibleWindow = new Rect(
visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X,
visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y,
visibleWindow.Width,
visibleWindow.Height);
}
return visibleWindow;
}
public Rect GetLayoutRealizationWindow()
{
var realizationWindow = GetLayoutVisibleWindow();
if (HasScroller)
{
realizationWindow = new Rect(
realizationWindow.X - _horizontalCacheBufferPerSide,
realizationWindow.Y - _verticalCacheBufferPerSide,
realizationWindow.Width + _horizontalCacheBufferPerSide * 2.0,
realizationWindow.Height + _verticalCacheBufferPerSide * 2.0);
}
return realizationWindow;
}
public void SetLayoutExtent(Rect extent)
{
_expectedViewportShift = new Point(
_expectedViewportShift.X + _layoutExtent.X - extent.X,
_expectedViewportShift.Y + _layoutExtent.Y - extent.Y);
// We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much.
if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1)
{
// There are cases where we might be expecting a shift but not get it. We will
// be waiting for the effective viewport event but if the scroll viewer is not able
// to perform the shift (perhaps because it cannot scroll in negative offset),
// then we will end up not realizing elements in the visible
// window. To avoid this, we register to layout updated for this layout pass. If we
// get an effective viewport, we know we have a new viewport and we unregister from
// layout updated. If we get the layout updated handler, then we know that the
// scroller was unable to perform the shift and we invalidate measure and unregister
// from the layout updated event.
if (!_layoutUpdatedSubscribed)
{
_owner.LayoutUpdated += OnLayoutUpdated;
_layoutUpdatedSubscribed = true;
}
}
_layoutExtent = extent;
_pendingViewportShift = _expectedViewportShift;
// We just finished a measure pass and have a new extent.
// Let's make sure the scrollers will run its arrange so that they track the anchor.
////if (_scroller != null)
////{
//// ((IControl)_scroller).InvalidateArrange();
////}
}
public Point GetOrigin() => throw new NotImplementedException();
public void OnLayoutChanged(bool isVirtualizing)
{
_managingViewportDisabled = !isVirtualizing;
_layoutExtent = default;
_expectedViewportShift = default;
_pendingViewportShift = default;
_unshiftableShift = default;
ResetCacheBuffer();
_effectiveViewportChangedRevoker?.Dispose();
if (!_managingViewportDisabled)
{
_effectiveViewportChangedRevoker = _owner.GetObservable(Visual.TransformedBoundsProperty)
.Skip(1)
.Subscribe(OnEffectiveViewportChanged);
}
}
public void OnElementPrepared(IControl element)
{
// If we have an anchor element, we do not want the
// scroll anchor provider to start anchoring some other element.
////element.CanBeScrollAnchor(true);
}
public void OnElementCleared(IControl element)
{
////element.CanBeScrollAnchor(false);
}
public void OnOwnerMeasuring()
{
// This is because of a bug that causes effective viewport to not
// fire if you register during arrange.
// Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport
//EnsureScroller();
}
public void OnOwnerArranged()
{
_expectedViewportShift = default;
if (!_managingViewportDisabled)
{
// This is because of a bug that causes effective viewport to not
// fire if you register during arrange.
// Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport
// EnsureScroller();
if (HasScroller)
{
double maximumHorizontalCacheBufferPerSide = _maximumHorizontalCacheLength * _visibleWindow.Width / 2.0;
double maximumVerticalCacheBufferPerSide = _maximumVerticalCacheLength * _visibleWindow.Height / 2.0;
bool continueBuildingCache =
_horizontalCacheBufferPerSide < maximumHorizontalCacheBufferPerSide ||
_verticalCacheBufferPerSide < maximumVerticalCacheBufferPerSide;
if (continueBuildingCache)
{
_horizontalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta;
_verticalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta;
_horizontalCacheBufferPerSide = Math.Min(_horizontalCacheBufferPerSide, maximumHorizontalCacheBufferPerSide);
_verticalCacheBufferPerSide = Math.Min(_verticalCacheBufferPerSide, maximumVerticalCacheBufferPerSide);
// Since we grow the cache buffer at the end of the arrange pass,
// we need to register work even if we just reached cache potential.
RegisterCacheBuildWork();
}
}
}
}
private void OnLayoutUpdated(object sender, EventArgs args)
{
_owner.LayoutUpdated -= OnLayoutUpdated;
if (_managingViewportDisabled)
{
return;
}
// We were expecting a viewport shift but we never got one and we are not going to in this
// layout pass. We likely will never get this shift, so lets assume that we are never going to get it and
// adjust our expected shift to track that. One case where this can happen is when there is no scrollviewer
// that can scroll in the direction where the shift is expected.
if (_pendingViewportShift.X != 0 || _pendingViewportShift.Y != 0)
{
// Assume this is never going to come.
_unshiftableShift = new Point(
_unshiftableShift.X + _pendingViewportShift.X,
_unshiftableShift.Y + _pendingViewportShift.Y);
_pendingViewportShift = default;
_expectedViewportShift = default;
TryInvalidateMeasure();
}
}
public void OnMakeAnchor(IControl anchor, bool isAnchorOutsideRealizedRange)
{
_makeAnchorElement = anchor;
_isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange;
}
public void OnBringIntoViewRequested(RequestBringIntoViewEventArgs args)
{
if (!_managingViewportDisabled)
{
// We do not animate bring-into-view operations where the anchor is disconnected because
// it doesn't look good (the blank space is obvious because the layout can't keep track
// of two realized ranges while the animation is going on).
if (_isAnchorOutsideRealizedRange)
{
////args.AnimationDesired(false);
}
// During the time between a bring into view request and the element coming into view we do not
// want the anchor provider to pick some anchor and jump to it. Instead we want to anchor on the
// element that is being brought into view. We can do this by making just that element as a potential
// anchor candidate and ensure no other element of this repeater is an anchor candidate.
// Once the layout pass is done and we render the frame, the element will be in frame and we can
// switch back to letting the anchor provider pick a suitable anchor.
// get the targetChild - i.e the immediate child of this repeater that is being brought into view.
// Note that the element being brought into view could be a descendant.
var targetChild = GetImmediateChildOfRepeater((IControl)args.TargetObject);
// Make sure that only the target child can be the anchor during the bring into view operation.
foreach (var child in _owner.Children)
{
////if (child.CanBeScrollAnchor && child != targetChild)
////{
//// child.CanBeScrollAnchor = false;
////}
}
// Register to rendering event to go back to how things were before where any child can be the anchor.
_isBringIntoViewInProgress = true;
////if (!m_renderingToken)
////{
//// winrt::Windows::UI::Xaml::Media::CompositionTarget compositionTarget{ nullptr };
//// m_renderingToken = compositionTarget.Rendering(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnCompositionTargetRendering });
////}
}
}
private IControl GetImmediateChildOfRepeater(IControl descendant)
{
var targetChild = descendant;
var parent = descendant.Parent;
while (parent != null && parent != _owner)
{
targetChild = parent;
parent = (IControl)parent.VisualParent;
}
if (parent == null)
{
throw new InvalidOperationException("OnBringIntoViewRequested called with args.target element not under the ItemsRepeater that recieved the call");
}
return targetChild;
}
public void ResetScrollers()
{
////_scroller = null;
////_effectiveViewportChangedRevoker.Dispose();
////m_ensuredScroller = false;
}
private void OnEffectiveViewportChanged(TransformedBounds? bounds)
{
if (!bounds.HasValue)
{
return;
}
var globalClip = bounds.Value.Clip;
var transform = _owner.GetVisualRoot().TransformToVisual(_owner).Value;
var clip = globalClip.TransformToAABB(transform);
var effectiveViewport = clip.Intersect(bounds.Value.Bounds);
UpdateViewport(effectiveViewport);
_pendingViewportShift = default;
_unshiftableShift = default;
if (_visibleWindow.IsEmpty)
{
// We got cleared.
_layoutExtent = default;
}
// We got a new viewport, we dont need to wait for layout updated anymore to
// see if our request for a pending shift was handled.
if (_layoutUpdatedSubscribed)
{
_owner.LayoutUpdated -= OnLayoutUpdated;
}
}
private void UpdateViewport(Rect viewport)
{
//assert(!m_managingViewportDisabled);
var previousVisibleWindow = _visibleWindow;
var currentVisibleWindow = viewport;
if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X &&
-currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y)
{
// We got cleared.
_visibleWindow = default;
}
else
{
_visibleWindow = currentVisibleWindow;
}
TryInvalidateMeasure();
}
private void ResetCacheBuffer()
{
_horizontalCacheBufferPerSide = 0.0;
_verticalCacheBufferPerSide = 0.0;
if (!_managingViewportDisabled)
{
// We need to start building the realization buffer again.
RegisterCacheBuildWork();
}
}
private static void ValidateCacheLength(double cacheLength)
{
if (cacheLength < 0.0 || double.IsInfinity(cacheLength) || double.IsNaN(cacheLength))
{
throw new ArgumentException("The maximum cache length must be equal or superior to zero.");
}
}
private void RegisterCacheBuildWork()
{
////assert(!m_managingViewportDisabled);
if (_owner.Layout != null &&
_cacheBuildAction == null)
{
// We capture 'owner' (a strong refernce on ItemsRepeater) to make sure ItemsRepeater is still around
// when the async action completes. By protecting ItemsRepeater, we also ensure that this instance
// of ViewportManager (referenced by 'this' pointer) is valid because the lifetime of ItemsRepeater
// and ViewportManager is the same (see ItemsRepeater::m_viewportManager).
// We can't simply hold a strong reference on ViewportManager because it's not a COM object.
////auto strongOwner = m_owner->get_strong();
////m_cacheBuildAction.set(
//// m_owner->Dispatcher().RunIdleAsync([this, strongOwner](const winrt::IdleDispatchedHandlerArgs&)
////{
//// OnCacheBuildActionCompleted();
////}));
}
}
private void TryInvalidateMeasure()
{
// Don't invalidate measure if we have an invalid window.
if (!_visibleWindow.IsEmpty)
{
// We invalidate measure instead of just invalidating arrange because
// we don't invalidate measure in UpdateViewport if the view is changing to
// avoid layout cycles.
_owner.InvalidateMeasure();
}
}
private class ScrollerInfo
{
public ScrollerInfo(ScrollViewer scroller)
{
Scroller = scroller;
}
public ScrollViewer Scroller { get; }
}
};
}

121
src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs

@ -0,0 +1,121 @@
using System;
namespace Avalonia.Controls.Repeaters
{
internal enum ElementOwner
{
// All elements are originally owned by the view generator.
ElementFactory,
// Ownership is transferred to the layout when it calls GetElement.
Layout,
// Ownership is transferred to the pinned pool if the element is cleared (outside of
// a 'remove' collection change of course).
PinnedPool,
// Ownership is transfered to the reset pool if the element is cleared by a reset and
// the data source supports unique ids.
UniqueIdResetPool,
// Ownership is transfered to the animator if the element is cleared due to a
// 'remove'-like collection change.
Animator
}
internal class VirtualizationInfo
{
private int _pinCounter;
private object _data;
public Rect ArrangeBounds { get; set; }
public bool AutoRecycleCandidate { get; set; }
public int Index { get; private set; }
public bool IsPinned => _pinCounter > 0;
public bool IsHeldByLayout => Owner == ElementOwner.Layout;
public bool IsRealized => IsHeldByLayout || Owner == ElementOwner.PinnedPool;
public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool;
public bool KeepAlive { get; set; }
public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory;
public string UniqueId { get; private set; }
public void MoveOwnershipToLayoutFromElementFactory(int index, string uniqueId)
{
//MUX_ASSERT(_owner == ElementOwner.ElementFactory);
Owner = ElementOwner.Layout;
Index = index;
UniqueId = uniqueId;
}
public void MoveOwnershipToLayoutFromUniqueIdResetPool()
{
//MUX_ASSERT(_owner == ElementOwner.UniqueIdResetPool);
Owner = ElementOwner.Layout;
}
public void MoveOwnershipToLayoutFromPinnedPool()
{
//MUX_ASSERT(_owner == ElementOwner.PinnedPool);
//MUX_ASSERT(IsPinned());
Owner = ElementOwner.Layout;
}
public void MoveOwnershipToElementFactory()
{
//MUX_ASSERT(_owner != ElementOwner.ElementFactory);
Owner = ElementOwner.ElementFactory;
_pinCounter = 0;
Index = -1;
UniqueId = string.Empty;
ArrangeBounds = ItemsRepeater.InvalidRect;
}
public void MoveOwnershipToUniqueIdResetPoolFromLayout()
{
//MUX_ASSERT(_owner == ElementOwner.Layout);
Owner = ElementOwner.UniqueIdResetPool;
// Keep the pinCounter the same. If the container survives the reset
// it can go on being pinned as if nothing happened.
}
public void MoveOwnershipToAnimator()
{
// During a unique id reset, some elements might get removed.
// Their ownership will go from the UniqueIdResetPool to the Animator.
// The common path though is for ownership to go from Layout to Animator.
//MUX_ASSERT(_owner == ElementOwner.Layout || _owner == ElementOwner.UniqueIdResetPool);
Owner = ElementOwner.Animator;
Index = -1;
_pinCounter = 0;
}
public void MoveOwnershipToPinnedPool()
{
//MUX_ASSERT(_owner == ElementOwner.Layout);
Owner = ElementOwner.PinnedPool;
}
public int AddPin()
{
if (!IsRealized)
{
throw new InvalidOperationException("You can't pin an unrealized element.");
}
return ++_pinCounter;
}
public int RemovePin()
{
if (!IsRealized)
{
throw new InvalidOperationException("You can't unpin an unrealized element.");
}
if (!IsPinned)
{
throw new InvalidOperationException("UnpinElement was called more often than PinElement.");
}
return --_pinCounter;
}
public void UpdateIndex(int newIndex) => Index = newIndex;
}
}

47
src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
namespace Avalonia.Controls.Repeaters
{
public abstract class VirtualizingLayout : Layout
{
public sealed override void InitializeForContext(LayoutContext context)
{
InitializeForContextCore((VirtualizingLayoutContext)context);
}
public sealed override void UninitializeForContext(LayoutContext context)
{
UninitializeForContextCore((VirtualizingLayoutContext)context);
}
public sealed override Size Measure(LayoutContext context, Size availableSize)
{
return MeasureOverride((VirtualizingLayoutContext)context, availableSize);
}
public sealed override Size Arrange(LayoutContext context, Size finalSize)
{
return ArrangeOverride((VirtualizingLayoutContext)context, finalSize);
}
protected virtual void InitializeForContextCore(VirtualizingLayoutContext context)
{
}
protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context)
{
}
protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize);
protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize;
protected internal virtual void OnItemsChangedCore(
VirtualizingLayoutContext context,
object source,
NotifyCollectionChangedEventArgs args) => InvalidateMeasure();
}
}

190
src/Avalonia.Controls/Repeaters/VirtualizingLayoutContext.cs

@ -0,0 +1,190 @@
// 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;
namespace Avalonia.Controls.Repeaters
{
/// <summary>
/// Defines constants that specify whether to suppress automatic recycling of the retrieved
/// element or force creation of a new element.
/// </summary>
/// <remarks>
/// When you call <see cref="VirtualizingLayoutContext.GetOrCreateElementAt(int, ElementRealizationOptions)"/>,
/// you can specify whether to suppress automatic recycling of the retrieved element or force
/// creation of a new element. Elements retrieved with automatic recycling suppressed
/// (SuppressAutoRecycle) are ignored by the automatic recycling logic that clears realized
/// elements that were not retrieved as part of the current layout pass. You must explicitly
/// recycle these elements by passing them to the RecycleElement method to avoid memory leaks.
/// </remarks>
[Flags]
public enum ElementRealizationOptions
{
/// <summary>
/// No option is specified.
/// </summary>
None = 0x0,
/// <summary>
/// Creation of a new element is forced.
/// </summary>
ForceCreate = 0x1,
/// <summary>
/// The element is ignored by the automatic recycling logic.
/// </summary>
SuppressAutoRecycle = 0x2,
};
/// <summary>
/// Represents the base class for layout context types that support virtualization.
/// </summary>
public abstract class VirtualizingLayoutContext : LayoutContext
{
/// <summary>
/// Gets the number of items in the data.
/// </summary>
/// <remarks>
/// This property gets the value returned by ItemCountCore, which must be implemented in
/// a derived class.
/// </remarks>
public int ItemCount => ItemCountCore();
/// <summary>
/// Gets or sets the origin point for the estimated content size.
/// </summary>
/// <remarks>
/// LayoutOrigin is used by virtualizing layouts that rely on estimations when determining
/// the size and position of content. It allows the layout to fix-up the estimated origin
/// of the content as it changes due to on-going estimation or potentially identifying the
/// actual size to use. For example, it’s possible that as a user is scrolling back to the
/// top of the content that the layout's estimates for the content size that it reports as
/// part of its MeasureOverride become increasingly accurate. If the predicted position of
/// the content does not already match the previously predicted position (for example, if
/// the size of the elements ends up being smaller than previously thought), then the
/// layout can indicate a new origin. The viewport provided to the layout on subsequent
/// passes will take into account the adjusted origin.
/// </remarks>
public Point LayoutOrigin { get => LayoutOriginCore; set => LayoutOriginCore = value; }
/// <summary>
/// Gets an area that represents the viewport and buffer that the layout should fill with
/// realized elements.
/// </summary>
public Rect RealizationRect => RealizationRectCore();
/// <summary>
/// Gets the recommended index from which to start the generation and layout of elements.
/// </summary>
/// <remarks>
/// The recommended index might be the result of programmatically realizing an element and
/// requesting that it be brought into view. Or, it may be that a user drags the scrollbar
/// thumb so quickly that the new viewport and the viewport and buffer previously given to
/// the layout do not intersect, so a new index is suggested as the anchor from which to
/// generate and layout other elements.
/// </remarks>
public int RecommendedAnchorIndex => RecommendedAnchorIndexCore;
/// <summary>
/// Implements the behavior of LayoutOrigin in a derived or custom VirtualizingLayoutContext.
/// </summary>
protected abstract Point LayoutOriginCore { get; set; }
/// <summary>
/// Implements the behavior for getting the return value of RecommendedAnchorIndex in a
/// derived or custom <see cref="VirtualizingLayoutContext"/>.
/// </summary>
protected virtual int RecommendedAnchorIndexCore { get; }
/// <summary>
/// Retrieves the data item in the source found at the specified index.
/// </summary>
/// <param name="index">The index of the data item to retrieve.</param>
public object GetItemAt(int index) => GetItemAtCore(index);
/// <summary>
/// Retrieves a UIElement that represents the data item in the source found at the
/// specified index. By default, if an element already exists, it is returned; otherwise,
/// a new element is created.
/// </summary>
/// <param name="index">The index of the data item to retrieve a UIElement for.</param>
/// <remarks>
/// This method calls <see cref="GetOrCreateElementAtCore(int, ElementRealizationOptions)"/>
/// with options set to None. GetElementAtCore must be implemented in a derived class.
/// </remarks>
public IControl GetOrCreateElementAt(int index)
=> GetOrCreateElementAtCore(index, ElementRealizationOptions.None);
/// <summary>
/// Retrieves a UIElement that represents the data item in the source found at the
/// specified index using the specified options.
/// </summary>
/// <param name="index">The index of the data item to retrieve a UIElement for.</param>
/// <param name="options">
/// A value of <see cref="ElementRealizationOptions"/> that specifies whether to suppress
/// automatic recycling of the retrieved element or force creation of a new element.
/// </param>
/// <remarks>
/// This method calls <see cref="GetOrCreateElementAtCore(int, ElementRealizationOptions)"/>,
/// which must be implemented in a derived class. When you request an element for the
/// specified index, you can optionally specify whether to suppress automatic recycling of
/// the retrieved element or force creation of a new element.Elements retrieved with
/// automatic recycling suppressed(SuppressAutoRecycle) are ignored by the automatic
/// recycling logic that clears realized elements that were not retrieved as part of the
/// current layout pass.You must explicitly recycle these elements by passing them to the
/// RecycleElement method to avoid memory leaks. These options are intended for more
/// advanced layouts that choose to explicitly manage the realization and recycling of
/// elements as a performance optimization.
/// </remarks>
public IControl GetOrCreateElementAt(int index, ElementRealizationOptions options)
=> GetOrCreateElementAtCore(index, options);
/// <summary>
/// Clears the specified UIElement and allows it to be either re-used or released.
/// </summary>
/// <param name="element">The element to clear.</param>
/// <remarks>
/// This method calls <see cref="RecycleElementCore(IControl)"/>, which must be implemented
/// in a derived class.
/// </remarks>
public void RecycleElement(IControl element) => RecycleElementCore(element);
/// <summary>
/// When implemented in a derived class, retrieves the number of items in the data.
/// </summary>
protected abstract int ItemCountCore();
/// <summary>
/// When implemented in a derived class, retrieves the data item in the source found at the
/// specified index.
/// </summary>
/// <param name="index">The index of the data item to retrieve.</param>
protected abstract object GetItemAtCore(int index);
/// <summary>
/// When implemented in a derived class, retrieves an area that represents the viewport and
/// buffer that the layout should fill with realized elements.
/// </summary>
protected abstract Rect RealizationRectCore();
/// <summary>
/// When implemented in a derived class, retrieves a UIElement that represents the data item
/// in the source found at the specified index using the specified options.
/// </summary>
/// <param name="index">The index of the data item to retrieve a UIElement for.</param>
/// <param name="options">
/// A value of <see cref="ElementRealizationOptions"/> that specifies whether to suppress
/// automatic recycling of the retrieved element or force creation of a new element.
/// </param>
protected abstract IControl GetOrCreateElementAtCore(int index, ElementRealizationOptions options);
/// <summary>
/// When implemented in a derived class, clears the specified UIElement and allows it to be
/// either re-used or released.
/// </summary>
/// <param name="element">The element to clear.</param>
protected abstract void RecycleElementCore(IControl element);
}
}

4
src/Avalonia.Controls/StackPanel.cs

@ -161,8 +161,8 @@ namespace Avalonia.Controls
/// <returns>The desired size of the control.</returns>
protected override Size MeasureOverride(Size availableSize)
{
double childAvailableWidth = double.PositiveInfinity;
double childAvailableHeight = double.PositiveInfinity;
double childAvailableWidth = availableSize.Width;
double childAvailableHeight = availableSize.Height;
if (Orientation == Orientation.Vertical)
{

32
src/Avalonia.Controls/Utils/ListUtils.cs

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Controls.Utils
{
internal static class ListUtils
{
public static void Resize<T>(this List<T> list, int size, T value)
{
int cur = list.Count;
if (size < cur)
{
list.RemoveRange(size, cur - size);
}
else if (size > cur)
{
if (size > list.Capacity)
{
list.Capacity = size;
}
list.AddRange(Enumerable.Repeat(value, size - cur));
}
}
public static void Resize<T>(this List<T> list, int count)
{
Resize(list, count, default);
}
}
}

1
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -547,7 +547,6 @@ namespace Avalonia.Rendering
}
}
System.Diagnostics.Debug.WriteLine("Invalidated " + rect);
SceneInvalidated(this, new SceneInvalidatedEventArgs((IRenderRoot)_root, rect));
}
}

Loading…
Cancel
Save