VirtualizingStackPanel.cs 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.Specialized;
  4. using System.Diagnostics;
  5. using System.Linq;
  6. using Avalonia.Controls.Primitives;
  7. using Avalonia.Controls.Utils;
  8. using Avalonia.Input;
  9. using Avalonia.Interactivity;
  10. using Avalonia.Layout;
  11. using Avalonia.Utilities;
  12. using Avalonia.VisualTree;
  13. namespace Avalonia.Controls
  14. {
  15. /// <summary>
  16. /// Arranges and virtualizes content on a single line that is oriented either horizontally or vertically.
  17. /// </summary>
  18. public class VirtualizingStackPanel : VirtualizingPanel, IScrollSnapPointsInfo
  19. {
  20. /// <summary>
  21. /// Defines the <see cref="Orientation"/> property.
  22. /// </summary>
  23. public static readonly StyledProperty<Orientation> OrientationProperty =
  24. StackPanel.OrientationProperty.AddOwner<VirtualizingStackPanel>();
  25. /// <summary>
  26. /// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
  27. /// </summary>
  28. public static readonly StyledProperty<bool> AreHorizontalSnapPointsRegularProperty =
  29. AvaloniaProperty.Register<VirtualizingStackPanel, bool>(nameof(AreHorizontalSnapPointsRegular));
  30. /// <summary>
  31. /// Defines the <see cref="AreVerticalSnapPointsRegular"/> property.
  32. /// </summary>
  33. public static readonly StyledProperty<bool> AreVerticalSnapPointsRegularProperty =
  34. AvaloniaProperty.Register<VirtualizingStackPanel, bool>(nameof(AreVerticalSnapPointsRegular));
  35. /// <summary>
  36. /// Defines the <see cref="HorizontalSnapPointsChanged"/> event.
  37. /// </summary>
  38. public static readonly RoutedEvent<RoutedEventArgs> HorizontalSnapPointsChangedEvent =
  39. RoutedEvent.Register<VirtualizingStackPanel, RoutedEventArgs>(
  40. nameof(HorizontalSnapPointsChanged),
  41. RoutingStrategies.Bubble);
  42. /// <summary>
  43. /// Defines the <see cref="VerticalSnapPointsChanged"/> event.
  44. /// </summary>
  45. public static readonly RoutedEvent<RoutedEventArgs> VerticalSnapPointsChangedEvent =
  46. RoutedEvent.Register<VirtualizingStackPanel, RoutedEventArgs>(
  47. nameof(VerticalSnapPointsChanged),
  48. RoutingStrategies.Bubble);
  49. private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
  50. AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, bool>("ItemIsOwnContainer");
  51. private static readonly Rect s_invalidViewport = new(double.PositiveInfinity, double.PositiveInfinity, 0, 0);
  52. private readonly Action<Control, int> _recycleElement;
  53. private readonly Action<Control> _recycleElementOnItemRemoved;
  54. private readonly Action<Control, int, int> _updateElementIndex;
  55. private int _scrollToIndex = -1;
  56. private Control? _scrollToElement;
  57. private bool _isInLayout;
  58. private bool _isWaitingForViewportUpdate;
  59. private double _lastEstimatedElementSizeU = 25;
  60. private RealizedStackElements? _measureElements;
  61. private RealizedStackElements? _realizedElements;
  62. private ScrollViewer? _scrollViewer;
  63. private Rect _viewport = s_invalidViewport;
  64. private Stack<Control>? _recyclePool;
  65. private Control? _unrealizedFocusedElement;
  66. private int _unrealizedFocusedIndex = -1;
  67. public VirtualizingStackPanel()
  68. {
  69. _recycleElement = RecycleElement;
  70. _recycleElementOnItemRemoved = RecycleElementOnItemRemoved;
  71. _updateElementIndex = UpdateElementIndex;
  72. EffectiveViewportChanged += OnEffectiveViewportChanged;
  73. }
  74. /// <summary>
  75. /// Gets or sets the axis along which items are laid out.
  76. /// </summary>
  77. /// <value>
  78. /// One of the enumeration values that specifies the axis along which items are laid out.
  79. /// The default is Vertical.
  80. /// </value>
  81. public Orientation Orientation
  82. {
  83. get => GetValue(OrientationProperty);
  84. set => SetValue(OrientationProperty, value);
  85. }
  86. /// <summary>
  87. /// Occurs when the measurements for horizontal snap points change.
  88. /// </summary>
  89. public event EventHandler<RoutedEventArgs>? HorizontalSnapPointsChanged
  90. {
  91. add => AddHandler(HorizontalSnapPointsChangedEvent, value);
  92. remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value);
  93. }
  94. /// <summary>
  95. /// Occurs when the measurements for vertical snap points change.
  96. /// </summary>
  97. public event EventHandler<RoutedEventArgs>? VerticalSnapPointsChanged
  98. {
  99. add => AddHandler(VerticalSnapPointsChangedEvent, value);
  100. remove => RemoveHandler(VerticalSnapPointsChangedEvent, value);
  101. }
  102. /// <summary>
  103. /// Gets or sets whether the horizontal snap points for the <see cref="VirtualizingStackPanel"/> are equidistant from each other.
  104. /// </summary>
  105. public bool AreHorizontalSnapPointsRegular
  106. {
  107. get { return GetValue(AreHorizontalSnapPointsRegularProperty); }
  108. set { SetValue(AreHorizontalSnapPointsRegularProperty, value); }
  109. }
  110. /// <summary>
  111. /// Gets or sets whether the vertical snap points for the <see cref="VirtualizingStackPanel"/> are equidistant from each other.
  112. /// </summary>
  113. public bool AreVerticalSnapPointsRegular
  114. {
  115. get { return GetValue(AreVerticalSnapPointsRegularProperty); }
  116. set { SetValue(AreVerticalSnapPointsRegularProperty, value); }
  117. }
  118. /// <summary>
  119. /// Gets the index of the first realized element, or -1 if no elements are realized.
  120. /// </summary>
  121. public int FirstRealizedIndex => _realizedElements?.FirstIndex ?? -1;
  122. /// <summary>
  123. /// Gets the index of the last realized element, or -1 if no elements are realized.
  124. /// </summary>
  125. public int LastRealizedIndex => _realizedElements?.LastIndex ?? -1;
  126. protected override Size MeasureOverride(Size availableSize)
  127. {
  128. var items = Items;
  129. if (items.Count == 0)
  130. return default;
  131. // If we're bringing an item into view, ignore any layout passes until we receive a new
  132. // effective viewport.
  133. if (_isWaitingForViewportUpdate)
  134. return DesiredSize;
  135. _isInLayout = true;
  136. try
  137. {
  138. var orientation = Orientation;
  139. _realizedElements ??= new();
  140. _measureElements ??= new();
  141. // We handle horizontal and vertical layouts here so X and Y are abstracted to:
  142. // - Horizontal layouts: U = horizontal, V = vertical
  143. // - Vertical layouts: U = vertical, V = horizontal
  144. var viewport = CalculateMeasureViewport(items);
  145. // If the viewport is disjunct then we can recycle everything.
  146. if (viewport.viewportIsDisjunct)
  147. _realizedElements.RecycleAllElements(_recycleElement);
  148. // Do the measure, creating/recycling elements as necessary to fill the viewport. Don't
  149. // write to _realizedElements yet, only _measureElements.
  150. RealizeElements(items, availableSize, ref viewport);
  151. // Now swap the measureElements and realizedElements collection.
  152. (_measureElements, _realizedElements) = (_realizedElements, _measureElements);
  153. _measureElements.ResetForReuse();
  154. return CalculateDesiredSize(orientation, items.Count, viewport);
  155. }
  156. finally
  157. {
  158. _isInLayout = false;
  159. }
  160. }
  161. protected override Size ArrangeOverride(Size finalSize)
  162. {
  163. if (_realizedElements is null)
  164. return default;
  165. _isInLayout = true;
  166. try
  167. {
  168. var orientation = Orientation;
  169. var u = _realizedElements!.StartU;
  170. for (var i = 0; i < _realizedElements.Count; ++i)
  171. {
  172. var e = _realizedElements.Elements[i];
  173. if (e is not null)
  174. {
  175. var sizeU = _realizedElements.SizeU[i];
  176. var rect = orientation == Orientation.Horizontal ?
  177. new Rect(u, 0, sizeU, finalSize.Height) :
  178. new Rect(0, u, finalSize.Width, sizeU);
  179. e.Arrange(rect);
  180. _scrollViewer?.RegisterAnchorCandidate(e);
  181. u += orientation == Orientation.Horizontal ? rect.Width : rect.Height;
  182. }
  183. }
  184. return finalSize;
  185. }
  186. finally
  187. {
  188. _isInLayout = false;
  189. RaiseEvent(new RoutedEventArgs(Orientation == Orientation.Horizontal ? HorizontalSnapPointsChangedEvent : VerticalSnapPointsChangedEvent));
  190. }
  191. }
  192. protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
  193. {
  194. base.OnAttachedToVisualTree(e);
  195. _scrollViewer = this.FindAncestorOfType<ScrollViewer>();
  196. }
  197. protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
  198. {
  199. base.OnDetachedFromVisualTree(e);
  200. _scrollViewer = null;
  201. }
  202. protected override void OnItemsChanged(IReadOnlyList<object?> items, NotifyCollectionChangedEventArgs e)
  203. {
  204. InvalidateMeasure();
  205. if (_realizedElements is null)
  206. return;
  207. switch (e.Action)
  208. {
  209. case NotifyCollectionChangedAction.Add:
  210. _realizedElements.ItemsInserted(e.NewStartingIndex, e.NewItems!.Count, _updateElementIndex);
  211. break;
  212. case NotifyCollectionChangedAction.Remove:
  213. _realizedElements.ItemsRemoved(e.OldStartingIndex, e.OldItems!.Count, _updateElementIndex, _recycleElementOnItemRemoved);
  214. break;
  215. case NotifyCollectionChangedAction.Replace:
  216. case NotifyCollectionChangedAction.Move:
  217. _realizedElements.ItemsRemoved(e.OldStartingIndex, e.OldItems!.Count, _updateElementIndex, _recycleElementOnItemRemoved);
  218. _realizedElements.ItemsInserted(e.NewStartingIndex, e.NewItems!.Count, _updateElementIndex);
  219. break;
  220. case NotifyCollectionChangedAction.Reset:
  221. _realizedElements.ItemsReset(_recycleElementOnItemRemoved);
  222. break;
  223. }
  224. }
  225. protected override IInputElement? GetControl(NavigationDirection direction, IInputElement? from, bool wrap)
  226. {
  227. var count = Items.Count;
  228. if (count == 0 || from is not Control fromControl)
  229. return null;
  230. var horiz = Orientation == Orientation.Horizontal;
  231. var fromIndex = from != null ? IndexFromContainer(fromControl) : -1;
  232. var toIndex = fromIndex;
  233. switch (direction)
  234. {
  235. case NavigationDirection.First:
  236. toIndex = 0;
  237. break;
  238. case NavigationDirection.Last:
  239. toIndex = count - 1;
  240. break;
  241. case NavigationDirection.Next:
  242. ++toIndex;
  243. break;
  244. case NavigationDirection.Previous:
  245. --toIndex;
  246. break;
  247. case NavigationDirection.Left:
  248. if (horiz)
  249. --toIndex;
  250. break;
  251. case NavigationDirection.Right:
  252. if (horiz)
  253. ++toIndex;
  254. break;
  255. case NavigationDirection.Up:
  256. if (!horiz)
  257. --toIndex;
  258. break;
  259. case NavigationDirection.Down:
  260. if (!horiz)
  261. ++toIndex;
  262. break;
  263. default:
  264. return null;
  265. }
  266. if (fromIndex == toIndex)
  267. return from;
  268. if (wrap)
  269. {
  270. if (toIndex < 0)
  271. toIndex = count - 1;
  272. else if (toIndex >= count)
  273. toIndex = 0;
  274. }
  275. return ScrollIntoView(toIndex);
  276. }
  277. protected internal override IEnumerable<Control>? GetRealizedContainers()
  278. {
  279. return _realizedElements?.Elements.Where(x => x is not null)!;
  280. }
  281. protected internal override Control? ContainerFromIndex(int index)
  282. {
  283. if (index < 0 || index >= Items.Count)
  284. return null;
  285. if (_realizedElements?.GetElement(index) is { } realized)
  286. return realized;
  287. if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty))
  288. return c;
  289. return null;
  290. }
  291. protected internal override int IndexFromContainer(Control container) => _realizedElements?.GetIndex(container) ?? -1;
  292. protected internal override Control? ScrollIntoView(int index)
  293. {
  294. var items = Items;
  295. if (_isInLayout || index < 0 || index >= items.Count || _realizedElements is null)
  296. return null;
  297. if (GetRealizedElement(index) is Control element)
  298. {
  299. element.BringIntoView();
  300. return element;
  301. }
  302. else if (this.GetVisualRoot() is ILayoutRoot root)
  303. {
  304. // Create and measure the element to be brought into view. Store it in a field so that
  305. // it can be re-used in the layout pass.
  306. _scrollToElement = GetOrCreateElement(items, index);
  307. _scrollToElement.Measure(Size.Infinity);
  308. _scrollToIndex = index;
  309. // Get the expected position of the elment and put it in place.
  310. var anchorU = _realizedElements.GetOrEstimateElementU(index, ref _lastEstimatedElementSizeU);
  311. var rect = Orientation == Orientation.Horizontal ?
  312. new Rect(anchorU, 0, _scrollToElement.DesiredSize.Width, _scrollToElement.DesiredSize.Height) :
  313. new Rect(0, anchorU, _scrollToElement.DesiredSize.Width, _scrollToElement.DesiredSize.Height);
  314. _scrollToElement.Arrange(rect);
  315. // If the item being brought into view was added since the last layout pass then
  316. // our bounds won't be updated, so any containing scroll viewers will not have an
  317. // updated extent. Do a layout pass to ensure that the containing scroll viewers
  318. // will be able to scroll the new item into view.
  319. if (!Bounds.Contains(rect) && !_viewport.Contains(rect))
  320. {
  321. _isWaitingForViewportUpdate = true;
  322. root.LayoutManager.ExecuteLayoutPass();
  323. _isWaitingForViewportUpdate = false;
  324. }
  325. // Try to bring the item into view.
  326. _scrollToElement.BringIntoView();
  327. // If the viewport does not contain the item to scroll to, set _isWaitingForViewportUpdate:
  328. // this should cause the following chain of events:
  329. // - Measure is first done with the old viewport (which will be a no-op, see MeasureOverride)
  330. // - The viewport is then updated by the layout system which invalidates our measure
  331. // - Measure is then done with the new viewport.
  332. _isWaitingForViewportUpdate = !_viewport.Contains(rect);
  333. root.LayoutManager.ExecuteLayoutPass();
  334. // If for some reason the layout system didn't give us a new viewport during the layout, we
  335. // need to do another layout pass as the one that took place was a no-op.
  336. if (_isWaitingForViewportUpdate)
  337. {
  338. _isWaitingForViewportUpdate = false;
  339. InvalidateMeasure();
  340. root.LayoutManager.ExecuteLayoutPass();
  341. }
  342. var result = _scrollToElement;
  343. _scrollToElement = null;
  344. _scrollToIndex = -1;
  345. return result;
  346. }
  347. return null;
  348. }
  349. internal IReadOnlyList<Control?> GetRealizedElements()
  350. {
  351. return _realizedElements?.Elements ?? Array.Empty<Control>();
  352. }
  353. private MeasureViewport CalculateMeasureViewport(IReadOnlyList<object?> items)
  354. {
  355. Debug.Assert(_realizedElements is not null);
  356. // If the control has not yet been laid out then the effective viewport won't have been set.
  357. // Try to work it out from an ancestor control.
  358. var viewport = _viewport != s_invalidViewport ? _viewport : EstimateViewport();
  359. // Get the viewport in the orientation direction.
  360. var viewportStart = Orientation == Orientation.Horizontal ? viewport.X : viewport.Y;
  361. var viewportEnd = Orientation == Orientation.Horizontal ? viewport.Right : viewport.Bottom;
  362. // Get or estimate the anchor element from which to start realization.
  363. var itemCount = items?.Count ?? 0;
  364. var (anchorIndex, anchorU) = _realizedElements.GetOrEstimateAnchorElementForViewport(
  365. viewportStart,
  366. viewportEnd,
  367. itemCount,
  368. ref _lastEstimatedElementSizeU);
  369. // Check if the anchor element is not within the currently realized elements.
  370. var disjunct = anchorIndex < _realizedElements.FirstIndex ||
  371. anchorIndex > _realizedElements.LastIndex;
  372. return new MeasureViewport
  373. {
  374. anchorIndex = anchorIndex,
  375. anchorU = anchorU,
  376. viewportUStart = viewportStart,
  377. viewportUEnd = viewportEnd,
  378. viewportIsDisjunct = disjunct,
  379. };
  380. }
  381. private Size CalculateDesiredSize(Orientation orientation, int itemCount, in MeasureViewport viewport)
  382. {
  383. var sizeU = 0.0;
  384. var sizeV = viewport.measuredV;
  385. if (viewport.lastIndex >= 0)
  386. {
  387. var remaining = itemCount - viewport.lastIndex - 1;
  388. sizeU = viewport.realizedEndU + (remaining * _lastEstimatedElementSizeU);
  389. }
  390. return orientation == Orientation.Horizontal ? new(sizeU, sizeV) : new(sizeV, sizeU);
  391. }
  392. private double EstimateElementSizeU()
  393. {
  394. if (_realizedElements is null)
  395. return _lastEstimatedElementSizeU;
  396. var result = _realizedElements.EstimateElementSizeU();
  397. if (result >= 0)
  398. _lastEstimatedElementSizeU = result;
  399. return _lastEstimatedElementSizeU;
  400. }
  401. private Rect EstimateViewport()
  402. {
  403. var c = this.GetVisualParent();
  404. var viewport = new Rect();
  405. if (c is null)
  406. {
  407. return viewport;
  408. }
  409. while (c is not null)
  410. {
  411. if ((c.Bounds.Width != 0 || c.Bounds.Height != 0) &&
  412. c.TransformToVisual(this) is Matrix transform)
  413. {
  414. viewport = new Rect(0, 0, c.Bounds.Width, c.Bounds.Height)
  415. .TransformToAABB(transform);
  416. break;
  417. }
  418. c = c?.GetVisualParent();
  419. }
  420. return viewport;
  421. }
  422. private void RealizeElements(
  423. IReadOnlyList<object?> items,
  424. Size availableSize,
  425. ref MeasureViewport viewport)
  426. {
  427. Debug.Assert(_measureElements is not null);
  428. Debug.Assert(_realizedElements is not null);
  429. Debug.Assert(items.Count > 0);
  430. var index = viewport.anchorIndex;
  431. var horizontal = Orientation == Orientation.Horizontal;
  432. var u = viewport.anchorU;
  433. // If the anchor element is at the beginning of, or before, the start of the viewport
  434. // then we can recycle all elements before it.
  435. if (u <= viewport.anchorU)
  436. _realizedElements.RecycleElementsBefore(viewport.anchorIndex, _recycleElement);
  437. // Start at the anchor element and move forwards, realizing elements.
  438. do
  439. {
  440. var e = GetOrCreateElement(items, index);
  441. e.Measure(availableSize);
  442. var sizeU = horizontal ? e.DesiredSize.Width : e.DesiredSize.Height;
  443. var sizeV = horizontal ? e.DesiredSize.Height : e.DesiredSize.Width;
  444. _measureElements!.Add(index, e, u, sizeU);
  445. viewport.measuredV = Math.Max(viewport.measuredV, sizeV);
  446. u += sizeU;
  447. ++index;
  448. } while (u < viewport.viewportUEnd && index < items.Count);
  449. // Store the last index and end U position for the desired size calculation.
  450. viewport.lastIndex = index - 1;
  451. viewport.realizedEndU = u;
  452. // We can now recycle elements after the last element.
  453. _realizedElements.RecycleElementsAfter(viewport.lastIndex, _recycleElement);
  454. // Next move backwards from the anchor element, realizing elements.
  455. index = viewport.anchorIndex - 1;
  456. u = viewport.anchorU;
  457. while (u > viewport.viewportUStart && index >= 0)
  458. {
  459. var e = GetOrCreateElement(items, index);
  460. e.Measure(availableSize);
  461. var sizeU = horizontal ? e.DesiredSize.Width : e.DesiredSize.Height;
  462. var sizeV = horizontal ? e.DesiredSize.Height : e.DesiredSize.Width;
  463. u -= sizeU;
  464. _measureElements!.Add(index, e, u, sizeU);
  465. viewport.measuredV = Math.Max(viewport.measuredV, sizeV);
  466. --index;
  467. }
  468. // We can now recycle elements before the first element.
  469. _realizedElements.RecycleElementsBefore(index + 1, _recycleElement);
  470. }
  471. private Control GetOrCreateElement(IReadOnlyList<object?> items, int index)
  472. {
  473. var e = GetRealizedElement(index) ??
  474. GetItemIsOwnContainer(items, index) ??
  475. GetRecycledElement(items, index) ??
  476. CreateElement(items, index);
  477. return e;
  478. }
  479. private Control? GetRealizedElement(int index)
  480. {
  481. if (_scrollToIndex == index)
  482. return _scrollToElement;
  483. return _realizedElements?.GetElement(index);
  484. }
  485. private Control? GetItemIsOwnContainer(IReadOnlyList<object?> items, int index)
  486. {
  487. var item = items[index];
  488. if (item is Control controlItem)
  489. {
  490. var generator = ItemContainerGenerator!;
  491. if (controlItem.IsSet(ItemIsOwnContainerProperty))
  492. {
  493. controlItem.IsVisible = true;
  494. return controlItem;
  495. }
  496. else if (generator.IsItemItsOwnContainer(controlItem))
  497. {
  498. generator.PrepareItemContainer(controlItem, controlItem, index);
  499. AddInternalChild(controlItem);
  500. controlItem.SetValue(ItemIsOwnContainerProperty, true);
  501. generator.ItemContainerPrepared(controlItem, item, index);
  502. return controlItem;
  503. }
  504. }
  505. return null;
  506. }
  507. private Control? GetRecycledElement(IReadOnlyList<object?> items, int index)
  508. {
  509. Debug.Assert(ItemContainerGenerator is not null);
  510. var generator = ItemContainerGenerator!;
  511. var item = items[index];
  512. if (_unrealizedFocusedIndex == index && _unrealizedFocusedElement is not null)
  513. {
  514. var element = _unrealizedFocusedElement;
  515. _unrealizedFocusedElement.LostFocus -= OnUnrealizedFocusedElementLostFocus;
  516. _unrealizedFocusedElement = null;
  517. _unrealizedFocusedIndex = -1;
  518. return element;
  519. }
  520. if (_recyclePool?.Count > 0)
  521. {
  522. var recycled = _recyclePool.Pop();
  523. recycled.IsVisible = true;
  524. generator.PrepareItemContainer(recycled, item, index);
  525. generator.ItemContainerPrepared(recycled, item, index);
  526. return recycled;
  527. }
  528. return null;
  529. }
  530. private Control CreateElement(IReadOnlyList<object?> items, int index)
  531. {
  532. Debug.Assert(ItemContainerGenerator is not null);
  533. var generator = ItemContainerGenerator!;
  534. var item = items[index];
  535. var container = generator.CreateContainer();
  536. generator.PrepareItemContainer(container, item, index);
  537. AddInternalChild(container);
  538. generator.ItemContainerPrepared(container, item, index);
  539. return container;
  540. }
  541. private void RecycleElement(Control element, int index)
  542. {
  543. Debug.Assert(ItemContainerGenerator is not null);
  544. _scrollViewer?.UnregisterAnchorCandidate(element);
  545. if (element.IsSet(ItemIsOwnContainerProperty))
  546. {
  547. element.IsVisible = false;
  548. }
  549. else if (element.IsKeyboardFocusWithin)
  550. {
  551. _unrealizedFocusedElement = element;
  552. _unrealizedFocusedIndex = index;
  553. _unrealizedFocusedElement.LostFocus += OnUnrealizedFocusedElementLostFocus;
  554. }
  555. else
  556. {
  557. ItemContainerGenerator!.ClearItemContainer(element);
  558. _recyclePool ??= new();
  559. _recyclePool.Push(element);
  560. element.IsVisible = false;
  561. }
  562. }
  563. private void RecycleElementOnItemRemoved(Control element)
  564. {
  565. Debug.Assert(ItemContainerGenerator is not null);
  566. if (element.IsSet(ItemIsOwnContainerProperty))
  567. {
  568. RemoveInternalChild(element);
  569. }
  570. else
  571. {
  572. ItemContainerGenerator!.ClearItemContainer(element);
  573. _recyclePool ??= new();
  574. _recyclePool.Push(element);
  575. element.IsVisible = false;
  576. }
  577. }
  578. private void UpdateElementIndex(Control element, int oldIndex, int newIndex)
  579. {
  580. Debug.Assert(ItemContainerGenerator is not null);
  581. ItemContainerGenerator.ItemContainerIndexChanged(element, oldIndex, newIndex);
  582. }
  583. private void OnEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e)
  584. {
  585. var vertical = Orientation == Orientation.Vertical;
  586. var oldViewportStart = vertical ? _viewport.Top : _viewport.Left;
  587. var oldViewportEnd = vertical ? _viewport.Bottom : _viewport.Right;
  588. _viewport = e.EffectiveViewport.Intersect(new(Bounds.Size));
  589. _isWaitingForViewportUpdate = false;
  590. var newViewportStart = vertical ? _viewport.Top : _viewport.Left;
  591. var newViewportEnd = vertical ? _viewport.Bottom : _viewport.Right;
  592. if (!MathUtilities.AreClose(oldViewportStart, newViewportStart) ||
  593. !MathUtilities.AreClose(oldViewportEnd, newViewportEnd))
  594. {
  595. InvalidateMeasure();
  596. }
  597. }
  598. private void OnUnrealizedFocusedElementLostFocus(object? sender, RoutedEventArgs e)
  599. {
  600. if (_unrealizedFocusedElement is null || sender != _unrealizedFocusedElement)
  601. return;
  602. _unrealizedFocusedElement.LostFocus -= OnUnrealizedFocusedElementLostFocus;
  603. RecycleElement(_unrealizedFocusedElement, _unrealizedFocusedIndex);
  604. _unrealizedFocusedElement = null;
  605. _unrealizedFocusedIndex = -1;
  606. }
  607. /// <inheritdoc/>
  608. public IReadOnlyList<double> GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment)
  609. {
  610. var snapPoints = new List<double>();
  611. switch (orientation)
  612. {
  613. case Orientation.Horizontal:
  614. if (AreHorizontalSnapPointsRegular)
  615. throw new InvalidOperationException();
  616. if (Orientation == Orientation.Horizontal)
  617. {
  618. var averageElementSize = EstimateElementSizeU();
  619. double snapPoint = 0;
  620. for (var i = 0; i < Items.Count; i++)
  621. {
  622. var container = ContainerFromIndex(i);
  623. if (container != null)
  624. {
  625. switch (snapPointsAlignment)
  626. {
  627. case SnapPointsAlignment.Near:
  628. snapPoint = container.Bounds.Left;
  629. break;
  630. case SnapPointsAlignment.Center:
  631. snapPoint = container.Bounds.Center.X;
  632. break;
  633. case SnapPointsAlignment.Far:
  634. snapPoint = container.Bounds.Right;
  635. break;
  636. }
  637. }
  638. else
  639. {
  640. if (snapPoint == 0)
  641. {
  642. switch (snapPointsAlignment)
  643. {
  644. case SnapPointsAlignment.Center:
  645. snapPoint = averageElementSize / 2;
  646. break;
  647. case SnapPointsAlignment.Far:
  648. snapPoint = averageElementSize;
  649. break;
  650. }
  651. }
  652. else
  653. snapPoint += averageElementSize;
  654. }
  655. snapPoints.Add(snapPoint);
  656. }
  657. }
  658. break;
  659. case Orientation.Vertical:
  660. if (AreVerticalSnapPointsRegular)
  661. throw new InvalidOperationException();
  662. if (Orientation == Orientation.Vertical)
  663. {
  664. var averageElementSize = EstimateElementSizeU();
  665. double snapPoint = 0;
  666. for (var i = 0; i < Items.Count; i++)
  667. {
  668. var container = ContainerFromIndex(i);
  669. if (container != null)
  670. {
  671. switch (snapPointsAlignment)
  672. {
  673. case SnapPointsAlignment.Near:
  674. snapPoint = container.Bounds.Top;
  675. break;
  676. case SnapPointsAlignment.Center:
  677. snapPoint = container.Bounds.Center.Y;
  678. break;
  679. case SnapPointsAlignment.Far:
  680. snapPoint = container.Bounds.Bottom;
  681. break;
  682. }
  683. }
  684. else
  685. {
  686. if (snapPoint == 0)
  687. {
  688. switch (snapPointsAlignment)
  689. {
  690. case SnapPointsAlignment.Center:
  691. snapPoint = averageElementSize / 2;
  692. break;
  693. case SnapPointsAlignment.Far:
  694. snapPoint = averageElementSize;
  695. break;
  696. }
  697. }
  698. else
  699. snapPoint += averageElementSize;
  700. }
  701. snapPoints.Add(snapPoint);
  702. }
  703. }
  704. break;
  705. }
  706. return snapPoints;
  707. }
  708. /// <inheritdoc/>
  709. public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset)
  710. {
  711. offset = 0f;
  712. var firstRealizedChild = _realizedElements?.Elements.FirstOrDefault();
  713. if (firstRealizedChild == null)
  714. {
  715. return 0;
  716. }
  717. double snapPoint = 0;
  718. switch (Orientation)
  719. {
  720. case Orientation.Horizontal:
  721. if (!AreHorizontalSnapPointsRegular)
  722. throw new InvalidOperationException();
  723. snapPoint = firstRealizedChild.Bounds.Width;
  724. switch (snapPointsAlignment)
  725. {
  726. case SnapPointsAlignment.Near:
  727. offset = 0;
  728. break;
  729. case SnapPointsAlignment.Center:
  730. offset = (firstRealizedChild.Bounds.Right - firstRealizedChild.Bounds.Left) / 2;
  731. break;
  732. case SnapPointsAlignment.Far:
  733. offset = firstRealizedChild.Bounds.Width;
  734. break;
  735. }
  736. break;
  737. case Orientation.Vertical:
  738. if (!AreVerticalSnapPointsRegular)
  739. throw new InvalidOperationException();
  740. snapPoint = firstRealizedChild.Bounds.Height;
  741. switch (snapPointsAlignment)
  742. {
  743. case SnapPointsAlignment.Near:
  744. offset = 0;
  745. break;
  746. case SnapPointsAlignment.Center:
  747. offset = (firstRealizedChild.Bounds.Bottom - firstRealizedChild.Bounds.Top) / 2;
  748. break;
  749. case SnapPointsAlignment.Far:
  750. offset = firstRealizedChild.Bounds.Height;
  751. break;
  752. }
  753. break;
  754. }
  755. return snapPoint;
  756. }
  757. private struct MeasureViewport
  758. {
  759. public int anchorIndex;
  760. public double anchorU;
  761. public double viewportUStart;
  762. public double viewportUEnd;
  763. public double measuredV;
  764. public double realizedEndU;
  765. public int lastIndex;
  766. public bool viewportIsDisjunct;
  767. }
  768. }
  769. }