ItemVirtualizerSimple.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  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.Utils;
  8. using Avalonia.Input;
  9. namespace Avalonia.Controls.Presenters
  10. {
  11. /// <summary>
  12. /// Handles virtualization in an <see cref="ItemsPresenter"/> for
  13. /// <see cref="ItemVirtualizationMode.Simple"/>.
  14. /// </summary>
  15. internal class ItemVirtualizerSimple : ItemVirtualizer
  16. {
  17. /// <summary>
  18. /// Initializes a new instance of the <see cref="ItemVirtualizerSimple"/> class.
  19. /// </summary>
  20. /// <param name="owner"></param>
  21. public ItemVirtualizerSimple(ItemsPresenter owner)
  22. : base(owner)
  23. {
  24. }
  25. /// <inheritdoc/>
  26. public override bool IsLogicalScrollEnabled => true;
  27. /// <inheritdoc/>
  28. public override double ExtentValue => ItemCount;
  29. /// <inheritdoc/>
  30. public override double OffsetValue
  31. {
  32. get
  33. {
  34. var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0;
  35. return FirstIndex + offset;
  36. }
  37. set
  38. {
  39. var panel = VirtualizingPanel;
  40. var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0;
  41. var delta = (int)(value - (FirstIndex + offset));
  42. if (delta != 0)
  43. {
  44. var newLastIndex = (NextIndex - 1) + delta;
  45. if (newLastIndex < ItemCount)
  46. {
  47. if (panel.PixelOffset > 0)
  48. {
  49. panel.PixelOffset = 0;
  50. delta += 1;
  51. }
  52. if (delta != 0)
  53. {
  54. RecycleContainersForMove(delta);
  55. }
  56. }
  57. else
  58. {
  59. // We're moving to a partially obscured item at the end of the list so
  60. // offset the panel by the height of the first item.
  61. var firstIndex = ItemCount - panel.Children.Count;
  62. RecycleContainersForMove(firstIndex - FirstIndex);
  63. panel.PixelOffset = panel.Children[0].Bounds.Height;
  64. }
  65. }
  66. }
  67. }
  68. /// <inheritdoc/>
  69. public override double ViewportValue
  70. {
  71. get
  72. {
  73. // If we can't fit the last item in the panel fully, subtract 1 from the viewport.
  74. var overflow = VirtualizingPanel.PixelOverflow > 0 ? 1 : 0;
  75. return VirtualizingPanel.Children.Count - overflow;
  76. }
  77. }
  78. /// <inheritdoc/>
  79. public override void UpdateControls()
  80. {
  81. CreateAndRemoveContainers();
  82. InvalidateScroll();
  83. }
  84. /// <inheritdoc/>
  85. public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e)
  86. {
  87. base.ItemsChanged(items, e);
  88. if (items != null)
  89. {
  90. switch (e.Action)
  91. {
  92. case NotifyCollectionChangedAction.Add:
  93. if (e.NewStartingIndex >= FirstIndex &&
  94. e.NewStartingIndex + e.NewItems.Count <= NextIndex)
  95. {
  96. CreateAndRemoveContainers();
  97. RecycleContainers();
  98. }
  99. break;
  100. case NotifyCollectionChangedAction.Remove:
  101. if (e.OldStartingIndex >= FirstIndex &&
  102. e.OldStartingIndex + e.OldItems.Count <= NextIndex)
  103. {
  104. RecycleContainersOnRemove();
  105. }
  106. break;
  107. case NotifyCollectionChangedAction.Move:
  108. case NotifyCollectionChangedAction.Replace:
  109. RecycleContainers();
  110. break;
  111. case NotifyCollectionChangedAction.Reset:
  112. RecycleContainersOnRemove();
  113. break;
  114. }
  115. }
  116. else
  117. {
  118. Owner.ItemContainerGenerator.Clear();
  119. VirtualizingPanel.Children.Clear();
  120. }
  121. InvalidateScroll();
  122. }
  123. public override IControl GetControlInDirection(FocusNavigationDirection direction, IControl from)
  124. {
  125. var generator = Owner.ItemContainerGenerator;
  126. var panel = VirtualizingPanel;
  127. var itemIndex = generator.IndexFromContainer(from);
  128. if (itemIndex == -1)
  129. {
  130. return null;
  131. }
  132. var newItemIndex = -1;
  133. if (VirtualizingPanel.ScrollDirection == Orientation.Vertical)
  134. {
  135. switch (direction)
  136. {
  137. case FocusNavigationDirection.Up:
  138. newItemIndex = itemIndex - 1;
  139. break;
  140. case FocusNavigationDirection.Down:
  141. newItemIndex = itemIndex + 1;
  142. break;
  143. }
  144. }
  145. if (newItemIndex >= 0 && newItemIndex < ItemCount)
  146. {
  147. // Get the index of the first and last fully visible items (i.e. excluding any
  148. // partially visible item at the beginning or end).
  149. var firstIndex = panel.PixelOffset == 0 ? FirstIndex : FirstIndex + 1;
  150. var lastIndex = (FirstIndex + ViewportValue) - 1;
  151. if (newItemIndex < firstIndex || newItemIndex > lastIndex)
  152. {
  153. OffsetValue += newItemIndex - itemIndex;
  154. InvalidateScroll();
  155. }
  156. return generator.ContainerFromIndex(newItemIndex);
  157. }
  158. return null;
  159. }
  160. /// <summary>
  161. /// Creates and removes containers such that we have at most enough containers to fill
  162. /// the panel.
  163. /// </summary>
  164. private void CreateAndRemoveContainers()
  165. {
  166. var generator = Owner.ItemContainerGenerator;
  167. var panel = VirtualizingPanel;
  168. if (!panel.IsFull && Items != null)
  169. {
  170. var memberSelector = Owner.MemberSelector;
  171. var index = NextIndex;
  172. var step = 1;
  173. while (!panel.IsFull)
  174. {
  175. if (index >= ItemCount)
  176. {
  177. // We can fit more containers in the panel, but we're at the end of the
  178. // items. If we're scrolled to the top (FirstIndex == 0), then there are
  179. // no more items to create. Otherwise, go backwards adding containers to
  180. // the beginning of the panel.
  181. if (FirstIndex == 0)
  182. {
  183. break;
  184. }
  185. else
  186. {
  187. index = FirstIndex - 1;
  188. step = -1;
  189. }
  190. }
  191. var materialized = generator.Materialize(index, Items.ElementAt(index), memberSelector);
  192. if (step == 1)
  193. {
  194. panel.Children.Add(materialized.ContainerControl);
  195. }
  196. else
  197. {
  198. panel.Children.Insert(0, materialized.ContainerControl);
  199. }
  200. index += step;
  201. }
  202. if (step == 1)
  203. {
  204. NextIndex = index;
  205. }
  206. else
  207. {
  208. NextIndex = ItemCount;
  209. FirstIndex = index + 1;
  210. }
  211. }
  212. if (panel.OverflowCount > 0)
  213. {
  214. RemoveContainers(panel.OverflowCount);
  215. }
  216. }
  217. /// <summary>
  218. /// Updates the containers in the panel to make sure they are displaying the correct item
  219. /// based on <see cref="ItemVirtualizer.FirstIndex"/>.
  220. /// </summary>
  221. /// <remarks>
  222. /// This method requires that <see cref="ItemVirtualizer.FirstIndex"/> + the number of
  223. /// materialized containers is not more than <see cref="ItemVirtualizer.ItemCount"/>.
  224. /// </remarks>
  225. private void RecycleContainers()
  226. {
  227. var panel = VirtualizingPanel;
  228. var generator = Owner.ItemContainerGenerator;
  229. var selector = Owner.MemberSelector;
  230. var containers = generator.Containers.ToList();
  231. var itemIndex = FirstIndex;
  232. foreach (var container in containers)
  233. {
  234. var item = Items.ElementAt(itemIndex);
  235. if (!object.Equals(container.Item, item))
  236. {
  237. if (!generator.TryRecycle(itemIndex, itemIndex, item, selector))
  238. {
  239. throw new NotImplementedException();
  240. }
  241. }
  242. ++itemIndex;
  243. }
  244. }
  245. /// <summary>
  246. /// Recycles containers when a move occurs.
  247. /// </summary>
  248. /// <param name="delta">The delta of the move.</param>
  249. /// <remarks>
  250. /// If the move is less than a page, then this method moves the containers for the items
  251. /// that are still visible to the correct place, and recyles and moves the others. For
  252. /// example: if there are 20 items and 10 containers visible and the user scrolls 5
  253. /// items down, then the bottom 5 containers will be moved to the top and the top 5 will
  254. /// be moved to the bottom and recycled to display the newly visible item. Updates
  255. /// <see cref="ItemVirtualizer.FirstIndex"/> and <see cref="ItemVirtualizer.NextIndex"/>
  256. /// with their new values.
  257. /// </remarks>
  258. private void RecycleContainersForMove(int delta)
  259. {
  260. var panel = VirtualizingPanel;
  261. var generator = Owner.ItemContainerGenerator;
  262. var selector = Owner.MemberSelector;
  263. var sign = delta < 0 ? -1 : 1;
  264. var count = Math.Min(Math.Abs(delta), panel.Children.Count);
  265. var move = count < panel.Children.Count;
  266. var first = delta < 0 && move ? panel.Children.Count + delta : 0;
  267. var containers = panel.Children.GetRange(first, count).ToList();
  268. for (var i = 0; i < count; ++i)
  269. {
  270. var oldItemIndex = FirstIndex + first + i;
  271. var newItemIndex = oldItemIndex + delta + ((panel.Children.Count - count) * sign);
  272. var item = Items.ElementAt(newItemIndex);
  273. if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector))
  274. {
  275. throw new NotImplementedException();
  276. }
  277. }
  278. if (move)
  279. {
  280. if (delta > 0)
  281. {
  282. panel.Children.MoveRange(first, count, panel.Children.Count);
  283. }
  284. else
  285. {
  286. panel.Children.MoveRange(first, count, 0);
  287. }
  288. }
  289. FirstIndex += delta;
  290. NextIndex += delta;
  291. }
  292. /// <summary>
  293. /// Recycles containers due to items being removed.
  294. /// </summary>
  295. private void RecycleContainersOnRemove()
  296. {
  297. var panel = VirtualizingPanel;
  298. if (NextIndex <= ItemCount)
  299. {
  300. // Items have been removed but FirstIndex..NextIndex is still a valid range in the
  301. // items, so just recycle the containers to adapt to the new state.
  302. RecycleContainers();
  303. }
  304. else
  305. {
  306. // Items have been removed and now the range FirstIndex..NextIndex goes out of
  307. // the item bounds. Remove any excess containers, try to scroll up and then recycle
  308. // the containers to make sure they point to the correct item.
  309. var newFirstIndex = Math.Max(0, FirstIndex - (NextIndex - ItemCount));
  310. var delta = newFirstIndex - FirstIndex;
  311. var newNextIndex = NextIndex + delta;
  312. if (newNextIndex > ItemCount)
  313. {
  314. RemoveContainers(newNextIndex - ItemCount);
  315. }
  316. if (delta != 0)
  317. {
  318. RecycleContainersForMove(delta);
  319. }
  320. RecycleContainers();
  321. }
  322. }
  323. /// <summary>
  324. /// Removes the specified number of containers from the end of the panel and updates
  325. /// <see cref="ItemVirtualizer.NextIndex"/>.
  326. /// </summary>
  327. /// <param name="count">The number of containers to remove.</param>
  328. private void RemoveContainers(int count)
  329. {
  330. var index = VirtualizingPanel.Children.Count - count;
  331. VirtualizingPanel.Children.RemoveRange(index, count);
  332. Owner.ItemContainerGenerator.Dematerialize(FirstIndex + index, count);
  333. NextIndex -= count;
  334. }
  335. }
  336. }