ItemVirtualizerSimple.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. // Copyright (c) The Avalonia Project. All rights reserved.
  2. // Licensed under the MIT license. See licence.md file in the project root for full license information.
  3. using System;
  4. using System.Collections;
  5. using System.Collections.Specialized;
  6. using System.Linq;
  7. using Avalonia.Controls.Primitives;
  8. using Avalonia.Controls.Utils;
  9. using Avalonia.Input;
  10. using Avalonia.Layout;
  11. using Avalonia.Utilities;
  12. using Avalonia.VisualTree;
  13. namespace Avalonia.Controls.Presenters
  14. {
  15. /// <summary>
  16. /// Handles virtualization in an <see cref="ItemsPresenter"/> for
  17. /// <see cref="ItemVirtualizationMode.Simple"/>.
  18. /// </summary>
  19. internal class ItemVirtualizerSimple : ItemVirtualizer
  20. {
  21. /// <summary>
  22. /// Initializes a new instance of the <see cref="ItemVirtualizerSimple"/> class.
  23. /// </summary>
  24. /// <param name="owner"></param>
  25. public ItemVirtualizerSimple(ItemsPresenter owner)
  26. : base(owner)
  27. {
  28. // Don't need to add children here as UpdateControls should be called by the panel
  29. // measure/arrange.
  30. }
  31. /// <inheritdoc/>
  32. public override bool IsLogicalScrollEnabled => true;
  33. /// <inheritdoc/>
  34. public override double ExtentValue => ItemCount;
  35. /// <inheritdoc/>
  36. public override double OffsetValue
  37. {
  38. get
  39. {
  40. var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0;
  41. return FirstIndex + offset;
  42. }
  43. set
  44. {
  45. var panel = VirtualizingPanel;
  46. var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0;
  47. var delta = (int)(value - (FirstIndex + offset));
  48. if (delta != 0)
  49. {
  50. var newLastIndex = (NextIndex - 1) + delta;
  51. if (newLastIndex < ItemCount)
  52. {
  53. if (panel.PixelOffset > 0)
  54. {
  55. panel.PixelOffset = 0;
  56. delta += 1;
  57. }
  58. if (delta != 0)
  59. {
  60. RecycleContainersForMove(delta);
  61. }
  62. }
  63. else
  64. {
  65. // We're moving to a partially obscured item at the end of the list so
  66. // offset the panel by the height of the first item.
  67. var firstIndex = ItemCount - panel.Children.Count;
  68. RecycleContainersForMove(firstIndex - FirstIndex);
  69. double pixelOffset;
  70. var child = panel.Children[0];
  71. if (child.IsArrangeValid)
  72. {
  73. pixelOffset = VirtualizingPanel.ScrollDirection == Orientation.Vertical ?
  74. child.Bounds.Height :
  75. child.Bounds.Width;
  76. }
  77. else
  78. {
  79. pixelOffset = VirtualizingPanel.ScrollDirection == Orientation.Vertical ?
  80. child.DesiredSize.Height :
  81. child.DesiredSize.Width;
  82. }
  83. panel.PixelOffset = pixelOffset;
  84. }
  85. }
  86. }
  87. }
  88. /// <inheritdoc/>
  89. public override double ViewportValue
  90. {
  91. get
  92. {
  93. // If we can't fit the last item in the panel fully, subtract 1 from the viewport.
  94. var overflow = VirtualizingPanel.PixelOverflow > 0 ? 1 : 0;
  95. return VirtualizingPanel.Children.Count - overflow;
  96. }
  97. }
  98. /// <inheritdoc/>
  99. public override Size MeasureOverride(Size availableSize)
  100. {
  101. var scrollable = (ILogicalScrollable)Owner;
  102. var visualRoot = Owner.GetVisualRoot();
  103. var maxAvailableSize = (visualRoot as WindowBase)?.PlatformImpl?.MaxClientSize
  104. ?? (visualRoot as TopLevel)?.ClientSize;
  105. // If infinity is passed as the available size and we're virtualized then we need to
  106. // fill the available space, but to do that we *don't* want to materialize all our
  107. // items! Take a look at the root of the tree for a MaxClientSize and use that as
  108. // the available size.
  109. if (VirtualizingPanel.ScrollDirection == Orientation.Vertical)
  110. {
  111. if (availableSize.Height == double.PositiveInfinity)
  112. {
  113. if (maxAvailableSize.HasValue)
  114. {
  115. availableSize = availableSize.WithHeight(maxAvailableSize.Value.Height);
  116. }
  117. }
  118. if (scrollable.CanHorizontallyScroll)
  119. {
  120. availableSize = availableSize.WithWidth(double.PositiveInfinity);
  121. }
  122. }
  123. else
  124. {
  125. if (availableSize.Width == double.PositiveInfinity)
  126. {
  127. if (maxAvailableSize.HasValue)
  128. {
  129. availableSize = availableSize.WithWidth(maxAvailableSize.Value.Width);
  130. }
  131. }
  132. if (scrollable.CanVerticallyScroll)
  133. {
  134. availableSize = availableSize.WithHeight(double.PositiveInfinity);
  135. }
  136. }
  137. Owner.Panel.Measure(availableSize);
  138. return Owner.Panel.DesiredSize;
  139. }
  140. /// <inheritdoc/>
  141. public override void UpdateControls()
  142. {
  143. CreateAndRemoveContainers();
  144. InvalidateScroll();
  145. }
  146. /// <inheritdoc/>
  147. public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e)
  148. {
  149. base.ItemsChanged(items, e);
  150. var panel = VirtualizingPanel;
  151. if (items != null)
  152. {
  153. switch (e.Action)
  154. {
  155. case NotifyCollectionChangedAction.Add:
  156. CreateAndRemoveContainers();
  157. if (e.NewStartingIndex < NextIndex)
  158. {
  159. RecycleContainers();
  160. }
  161. panel.ForceInvalidateMeasure();
  162. break;
  163. case NotifyCollectionChangedAction.Remove:
  164. if (e.OldStartingIndex >= FirstIndex &&
  165. e.OldStartingIndex < NextIndex)
  166. {
  167. RecycleContainersOnRemove();
  168. }
  169. panel.ForceInvalidateMeasure();
  170. break;
  171. case NotifyCollectionChangedAction.Move:
  172. case NotifyCollectionChangedAction.Replace:
  173. RecycleContainers();
  174. break;
  175. case NotifyCollectionChangedAction.Reset:
  176. RecycleContainersOnRemove();
  177. CreateAndRemoveContainers();
  178. panel.ForceInvalidateMeasure();
  179. break;
  180. }
  181. }
  182. else
  183. {
  184. Owner.ItemContainerGenerator.Clear();
  185. VirtualizingPanel.Children.Clear();
  186. FirstIndex = NextIndex = 0;
  187. }
  188. // If we are scrolled to view a partially visible last item but controls were added
  189. // then we need to return to a non-offset scroll position.
  190. if (panel.PixelOffset != 0 && FirstIndex + panel.Children.Count < ItemCount)
  191. {
  192. panel.PixelOffset = 0;
  193. RecycleContainersForMove(1);
  194. }
  195. InvalidateScroll();
  196. }
  197. public override IControl GetControlInDirection(NavigationDirection direction, IControl from)
  198. {
  199. var generator = Owner.ItemContainerGenerator;
  200. var panel = VirtualizingPanel;
  201. var itemIndex = generator.IndexFromContainer(from);
  202. var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical;
  203. if (itemIndex == -1)
  204. {
  205. return null;
  206. }
  207. var newItemIndex = -1;
  208. switch (direction)
  209. {
  210. case NavigationDirection.First:
  211. newItemIndex = 0;
  212. break;
  213. case NavigationDirection.Last:
  214. newItemIndex = ItemCount - 1;
  215. break;
  216. case NavigationDirection.Up:
  217. if (vertical)
  218. {
  219. newItemIndex = itemIndex - 1;
  220. }
  221. break;
  222. case NavigationDirection.Down:
  223. if (vertical)
  224. {
  225. newItemIndex = itemIndex + 1;
  226. }
  227. break;
  228. case NavigationDirection.Left:
  229. if (!vertical)
  230. {
  231. newItemIndex = itemIndex - 1;
  232. }
  233. break;
  234. case NavigationDirection.Right:
  235. if (!vertical)
  236. {
  237. newItemIndex = itemIndex + 1;
  238. }
  239. break;
  240. case NavigationDirection.PageUp:
  241. newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue);
  242. break;
  243. case NavigationDirection.PageDown:
  244. newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue);
  245. break;
  246. }
  247. return ScrollIntoView(newItemIndex);
  248. }
  249. /// <inheritdoc/>
  250. public override void ScrollIntoView(object item)
  251. {
  252. var index = Items.IndexOf(item);
  253. if (index != -1)
  254. {
  255. ScrollIntoView(index);
  256. }
  257. }
  258. /// <summary>
  259. /// Creates and removes containers such that we have at most enough containers to fill
  260. /// the panel.
  261. /// </summary>
  262. private void CreateAndRemoveContainers()
  263. {
  264. var generator = Owner.ItemContainerGenerator;
  265. var panel = VirtualizingPanel;
  266. if (!panel.IsFull && Items != null && panel.IsAttachedToVisualTree)
  267. {
  268. var memberSelector = Owner.MemberSelector;
  269. var index = NextIndex;
  270. var step = 1;
  271. while (!panel.IsFull && index >= 0)
  272. {
  273. if (index >= ItemCount)
  274. {
  275. // We can fit more containers in the panel, but we're at the end of the
  276. // items. If we're scrolled to the top (FirstIndex == 0), then there are
  277. // no more items to create. Otherwise, go backwards adding containers to
  278. // the beginning of the panel.
  279. if (FirstIndex == 0)
  280. {
  281. break;
  282. }
  283. else
  284. {
  285. index = FirstIndex - 1;
  286. step = -1;
  287. }
  288. }
  289. var materialized = generator.Materialize(index, Items.ElementAt(index), memberSelector);
  290. if (step == 1)
  291. {
  292. panel.Children.Add(materialized.ContainerControl);
  293. }
  294. else
  295. {
  296. panel.Children.Insert(0, materialized.ContainerControl);
  297. }
  298. index += step;
  299. }
  300. if (step == 1)
  301. {
  302. NextIndex = index;
  303. }
  304. else
  305. {
  306. NextIndex = ItemCount;
  307. FirstIndex = index + 1;
  308. }
  309. }
  310. if (panel.OverflowCount > 0)
  311. {
  312. RemoveContainers(panel.OverflowCount);
  313. }
  314. }
  315. /// <summary>
  316. /// Updates the containers in the panel to make sure they are displaying the correct item
  317. /// based on <see cref="ItemVirtualizer.FirstIndex"/>.
  318. /// </summary>
  319. /// <remarks>
  320. /// This method requires that <see cref="ItemVirtualizer.FirstIndex"/> + the number of
  321. /// materialized containers is not more than <see cref="ItemVirtualizer.ItemCount"/>.
  322. /// </remarks>
  323. private void RecycleContainers()
  324. {
  325. var panel = VirtualizingPanel;
  326. var generator = Owner.ItemContainerGenerator;
  327. var selector = Owner.MemberSelector;
  328. var containers = generator.Containers.ToList();
  329. var itemIndex = FirstIndex;
  330. foreach (var container in containers)
  331. {
  332. var item = Items.ElementAt(itemIndex);
  333. if (!object.Equals(container.Item, item))
  334. {
  335. if (!generator.TryRecycle(itemIndex, itemIndex, item, selector))
  336. {
  337. throw new NotImplementedException();
  338. }
  339. }
  340. ++itemIndex;
  341. }
  342. }
  343. /// <summary>
  344. /// Recycles containers when a move occurs.
  345. /// </summary>
  346. /// <param name="delta">The delta of the move.</param>
  347. /// <remarks>
  348. /// If the move is less than a page, then this method moves the containers for the items
  349. /// that are still visible to the correct place, and recycles and moves the others. For
  350. /// example: if there are 20 items and 10 containers visible and the user scrolls 5
  351. /// items down, then the bottom 5 containers will be moved to the top and the top 5 will
  352. /// be moved to the bottom and recycled to display the newly visible item. Updates
  353. /// <see cref="ItemVirtualizer.FirstIndex"/> and <see cref="ItemVirtualizer.NextIndex"/>
  354. /// with their new values.
  355. /// </remarks>
  356. private void RecycleContainersForMove(int delta)
  357. {
  358. var panel = VirtualizingPanel;
  359. var generator = Owner.ItemContainerGenerator;
  360. var selector = Owner.MemberSelector;
  361. //validate delta it should never overflow last index or generate index < 0
  362. if (delta > 0)
  363. {
  364. if ((FirstIndex + delta + panel.Children.Count) > ItemCount)
  365. {
  366. delta = ItemCount - FirstIndex - panel.Children.Count;
  367. }
  368. }
  369. else if ((FirstIndex + delta) < 0)
  370. {
  371. delta = -FirstIndex;
  372. }
  373. var sign = delta < 0 ? -1 : 1;
  374. var count = Math.Min(Math.Abs(delta), panel.Children.Count);
  375. var move = count < panel.Children.Count;
  376. var first = delta < 0 && move ? panel.Children.Count + delta : 0;
  377. for (var i = 0; i < count; ++i)
  378. {
  379. var oldItemIndex = FirstIndex + first + i;
  380. var newItemIndex = oldItemIndex + delta + ((panel.Children.Count - count) * sign);
  381. var item = Items.ElementAt(newItemIndex);
  382. if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector))
  383. {
  384. throw new NotImplementedException();
  385. }
  386. }
  387. if (move)
  388. {
  389. if (delta > 0)
  390. {
  391. panel.Children.MoveRange(first, count, panel.Children.Count);
  392. }
  393. else
  394. {
  395. panel.Children.MoveRange(first, count, 0);
  396. }
  397. }
  398. FirstIndex += delta;
  399. NextIndex += delta;
  400. }
  401. /// <summary>
  402. /// Recycles containers due to items being removed.
  403. /// </summary>
  404. private void RecycleContainersOnRemove()
  405. {
  406. var panel = VirtualizingPanel;
  407. if (NextIndex <= ItemCount)
  408. {
  409. // Items have been removed but FirstIndex..NextIndex is still a valid range in the
  410. // items, so just recycle the containers to adapt to the new state.
  411. RecycleContainers();
  412. }
  413. else
  414. {
  415. // Items have been removed and now the range FirstIndex..NextIndex goes out of
  416. // the item bounds. Remove any excess containers, try to scroll up and then recycle
  417. // the containers to make sure they point to the correct item.
  418. var newFirstIndex = Math.Max(0, FirstIndex - (NextIndex - ItemCount));
  419. var delta = newFirstIndex - FirstIndex;
  420. var newNextIndex = NextIndex + delta;
  421. if (newNextIndex > ItemCount)
  422. {
  423. RemoveContainers(newNextIndex - ItemCount);
  424. }
  425. if (delta != 0)
  426. {
  427. RecycleContainersForMove(delta);
  428. }
  429. RecycleContainers();
  430. }
  431. }
  432. /// <summary>
  433. /// Removes the specified number of containers from the end of the panel and updates
  434. /// <see cref="ItemVirtualizer.NextIndex"/>.
  435. /// </summary>
  436. /// <param name="count">The number of containers to remove.</param>
  437. private void RemoveContainers(int count)
  438. {
  439. var index = VirtualizingPanel.Children.Count - count;
  440. VirtualizingPanel.Children.RemoveRange(index, count);
  441. Owner.ItemContainerGenerator.Dematerialize(FirstIndex + index, count);
  442. NextIndex -= count;
  443. }
  444. /// <summary>
  445. /// Scrolls the item with the specified index into view.
  446. /// </summary>
  447. /// <param name="index">The item index.</param>
  448. /// <returns>The container that was brought into view.</returns>
  449. private IControl ScrollIntoView(int index)
  450. {
  451. var panel = VirtualizingPanel;
  452. var generator = Owner.ItemContainerGenerator;
  453. var newOffset = -1.0;
  454. if (index >= 0 && index < ItemCount)
  455. {
  456. if (index < FirstIndex)
  457. {
  458. newOffset = index;
  459. }
  460. else if (index >= NextIndex)
  461. {
  462. newOffset = index - Math.Ceiling(ViewportValue - 1);
  463. }
  464. else if (OffsetValue + ViewportValue >= ItemCount)
  465. {
  466. newOffset = OffsetValue - 1;
  467. }
  468. if (newOffset != -1)
  469. {
  470. OffsetValue = newOffset;
  471. }
  472. var container = generator.ContainerFromIndex(index);
  473. var layoutManager = (Owner.GetVisualRoot() as ILayoutRoot)?.LayoutManager;
  474. // We need to do a layout here because it's possible that the container we moved to
  475. // is only partially visible due to differing item sizes. If the container is only
  476. // partially visible, scroll again. Don't do this if there's no layout manager:
  477. // it means we're running a unit test.
  478. if (container != null && layoutManager != null)
  479. {
  480. layoutManager.ExecuteLayoutPass();
  481. if (panel.ScrollDirection == Orientation.Vertical)
  482. {
  483. if (container.Bounds.Y < panel.Bounds.Y || container.Bounds.Bottom > panel.Bounds.Bottom)
  484. {
  485. OffsetValue += 1;
  486. }
  487. }
  488. else
  489. {
  490. if (container.Bounds.X < panel.Bounds.X || container.Bounds.Right > panel.Bounds.Right)
  491. {
  492. OffsetValue += 1;
  493. }
  494. }
  495. }
  496. return container;
  497. }
  498. return null;
  499. }
  500. /// <summary>
  501. /// Ensures an offset value is within the value range.
  502. /// </summary>
  503. /// <param name="value">The value.</param>
  504. /// <returns>The coerced value.</returns>
  505. private double CoerceOffset(double value)
  506. {
  507. var max = Math.Max(ExtentValue - ViewportValue, 0);
  508. return MathUtilities.Clamp(value, 0, max);
  509. }
  510. }
  511. }