FlowLayoutAlgorithm.cs 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. // This source file is adapted from the WinUI project.
  2. // (https://github.com/microsoft/microsoft-ui-xaml)
  3. //
  4. // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
  5. using System;
  6. using System.Collections.Specialized;
  7. using Avalonia.Logging;
  8. namespace Avalonia.Layout
  9. {
  10. internal class FlowLayoutAlgorithm
  11. {
  12. private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures();
  13. private readonly ElementManager _elementManager = new ElementManager();
  14. private Size _lastAvailableSize;
  15. private double _lastItemSpacing;
  16. private bool _collectionChangePending;
  17. private VirtualizingLayoutContext? _context;
  18. private IFlowLayoutAlgorithmDelegates? _algorithmCallbacks;
  19. private Rect _lastExtent;
  20. private int _firstRealizedDataIndexInsideRealizationWindow = -1;
  21. private int _lastRealizedDataIndexInsideRealizationWindow = -1;
  22. // If the scroll orientation is the same as the follow orientation
  23. // we will only have one line since we will never wrap. In that case
  24. // we do not want to align the line. We could potentially switch the
  25. // meaning of line alignment in this case, but I'll hold off on that
  26. // feature until someone asks for it - This is not a common scenario
  27. // anyway.
  28. private bool _scrollOrientationSameAsFlow;
  29. public Rect LastExtent => _lastExtent;
  30. private bool IsVirtualizingContext
  31. {
  32. get
  33. {
  34. if (_context != null)
  35. {
  36. var rect = _context.RealizationRect;
  37. bool hasInfiniteSize = double.IsInfinity(rect.Height) || double.IsInfinity(rect.Width);
  38. return !hasInfiniteSize;
  39. }
  40. return false;
  41. }
  42. }
  43. private Rect RealizationRect => IsVirtualizingContext ? _context!.RealizationRect : new Rect(Size.Infinity);
  44. public void InitializeForContext(VirtualizingLayoutContext context, IFlowLayoutAlgorithmDelegates callbacks)
  45. {
  46. _algorithmCallbacks = callbacks;
  47. _context = context;
  48. _elementManager.SetContext(context);
  49. }
  50. public void UninitializeForContext(VirtualizingLayoutContext context)
  51. {
  52. if (IsVirtualizingContext)
  53. {
  54. // This layout is about to be detached. Let go of all elements
  55. // being held and remove the layout state from the context.
  56. _elementManager.ClearRealizedRange();
  57. }
  58. context.LayoutState = null;
  59. }
  60. public Size Measure(
  61. Size availableSize,
  62. VirtualizingLayoutContext context,
  63. bool isWrapping,
  64. double minItemSpacing,
  65. double lineSpacing,
  66. int maxItemsPerLine,
  67. ScrollOrientation orientation,
  68. bool disableVirtualization,
  69. string? layoutId)
  70. {
  71. _orientation.ScrollOrientation = orientation;
  72. // If minor size is infinity, there is only one line and no need to align that line.
  73. _scrollOrientationSameAsFlow = double.IsInfinity(_orientation.Minor(availableSize));
  74. var realizationRect = RealizationRect;
  75. Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: MeasureLayout Realization({Rect})",
  76. layoutId,
  77. realizationRect);
  78. var suggestedAnchorIndex = _context!.RecommendedAnchorIndex;
  79. if (_elementManager.IsIndexValidInData(suggestedAnchorIndex))
  80. {
  81. var anchorRealized = _elementManager.IsDataIndexRealized(suggestedAnchorIndex);
  82. if (!anchorRealized)
  83. {
  84. MakeAnchor(_context, suggestedAnchorIndex, availableSize);
  85. }
  86. }
  87. _elementManager.OnBeginMeasure(orientation);
  88. int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId);
  89. Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
  90. Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
  91. if (isWrapping && IsReflowRequired())
  92. {
  93. Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Reflow Pass", layoutId);
  94. var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
  95. _orientation.SetMinorStart(ref firstElementBounds, 0);
  96. _elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds);
  97. Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
  98. }
  99. RaiseLineArranged();
  100. _collectionChangePending = false;
  101. _lastExtent = EstimateExtent(availableSize, layoutId);
  102. SetLayoutOrigin();
  103. return new Size(_lastExtent.Width, _lastExtent.Height);
  104. }
  105. public Size Arrange(
  106. Size finalSize,
  107. VirtualizingLayoutContext context,
  108. bool isWrapping,
  109. LineAlignment lineAlignment,
  110. string? layoutId)
  111. {
  112. Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: ArrangeLayout", layoutId);
  113. ArrangeVirtualizingLayout(finalSize, lineAlignment, isWrapping, layoutId);
  114. return new Size(
  115. Math.Max(finalSize.Width, _lastExtent.Width),
  116. Math.Max(finalSize.Height, _lastExtent.Height));
  117. }
  118. public void OnItemsSourceChanged(
  119. object? source,
  120. NotifyCollectionChangedEventArgs args,
  121. VirtualizingLayoutContext context)
  122. {
  123. _elementManager.DataSourceChanged(source, args);
  124. _collectionChangePending = true;
  125. }
  126. public Size MeasureElement(
  127. Layoutable element,
  128. int index,
  129. Size availableSize,
  130. VirtualizingLayoutContext context)
  131. {
  132. var measureSize = _algorithmCallbacks!.Algorithm_GetMeasureSize(index, availableSize, context);
  133. element.Measure(measureSize);
  134. var provisionalArrangeSize = _algorithmCallbacks.Algorithm_GetProvisionalArrangeSize(index, measureSize, element.DesiredSize, context);
  135. _algorithmCallbacks.Algorithm_OnElementMeasured(element, index, availableSize, measureSize, element.DesiredSize, provisionalArrangeSize, context);
  136. return provisionalArrangeSize;
  137. }
  138. private int GetAnchorIndex(
  139. Size availableSize,
  140. bool isWrapping,
  141. double minItemSpacing,
  142. string? layoutId)
  143. {
  144. int anchorIndex = -1;
  145. var anchorPosition= new Point();
  146. var context = _context;
  147. if (!IsVirtualizingContext)
  148. {
  149. // Non virtualizing host, start generating from the element 0
  150. anchorIndex = context!.ItemCount > 0 ? 0 : -1;
  151. }
  152. else
  153. {
  154. bool isRealizationWindowConnected = _elementManager.IsWindowConnected(RealizationRect, _orientation.ScrollOrientation, _scrollOrientationSameAsFlow);
  155. // Item spacing and size in non-virtualizing direction change can cause elements to reflow
  156. // and get a new column position. In that case we need the anchor to be positioned in the
  157. // correct column.
  158. bool needAnchorColumnRevaluation = isWrapping && (
  159. _orientation.Minor(_lastAvailableSize) != _orientation.Minor(availableSize) ||
  160. _lastItemSpacing != minItemSpacing ||
  161. _collectionChangePending);
  162. var suggestedAnchorIndex = _context!.RecommendedAnchorIndex;
  163. var isAnchorSuggestionValid = suggestedAnchorIndex >= 0 &&
  164. _elementManager.IsDataIndexRealized(suggestedAnchorIndex);
  165. if (isAnchorSuggestionValid)
  166. {
  167. Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Using suggested anchor {Anchor}", layoutId, suggestedAnchorIndex);
  168. anchorIndex = _algorithmCallbacks!.Algorithm_GetAnchorForTargetElement(
  169. suggestedAnchorIndex,
  170. availableSize,
  171. context!).Index;
  172. if (_elementManager.IsDataIndexRealized(anchorIndex))
  173. {
  174. var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex);
  175. if (needAnchorColumnRevaluation)
  176. {
  177. // We were provided a valid anchor, but its position might be incorrect because for example it is in
  178. // the wrong column. We do know that the anchor is the first element in the row, so we can force the minor position
  179. // to start at 0.
  180. anchorPosition = _orientation.MinorMajorPoint(0, _orientation.MajorStart(anchorBounds));
  181. }
  182. else
  183. {
  184. anchorPosition = new Point(anchorBounds.X, anchorBounds.Y);
  185. }
  186. }
  187. else if (anchorIndex >= 0)
  188. {
  189. // It is possible to end up in a situation during a collection change where GetAnchorForTargetElement returns an index
  190. // which is not in the realized range. Eg. insert one item at index 0 for a grid layout.
  191. // SuggestedAnchor will be 1 (used to be 0) and GetAnchorForTargetElement will return 0 (left most item in row). However 0 is not in the
  192. // realized range yet. In this case we realize the gap between the target anchor and the suggested anchor.
  193. int firstRealizedDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);
  194. for (int i = firstRealizedDataIndex - 1; i >= anchorIndex; --i)
  195. {
  196. _elementManager.EnsureElementRealized(false /*forward*/, i, layoutId);
  197. }
  198. var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(suggestedAnchorIndex);
  199. anchorPosition = _orientation.MinorMajorPoint(0, _orientation.MajorStart(anchorBounds));
  200. }
  201. }
  202. else if (needAnchorColumnRevaluation || !isRealizationWindowConnected)
  203. {
  204. if (needAnchorColumnRevaluation) { Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: NeedAnchorColumnReevaluation", layoutId); }
  205. if (!isRealizationWindowConnected) { Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Disconnected Window", layoutId); }
  206. // The anchor is based on the realization window because a connected ItemsRepeater might intersect the realization window
  207. // but not the visible window. In that situation, we still need to produce a valid anchor.
  208. var anchorInfo = _algorithmCallbacks!.Algorithm_GetAnchorForRealizationRect(availableSize, context!);
  209. anchorIndex = anchorInfo.Index;
  210. anchorPosition = _orientation.MinorMajorPoint(0, anchorInfo.Offset);
  211. }
  212. else
  213. {
  214. Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Connected Window - picking first realized element as anchor", layoutId);
  215. // No suggestion - just pick first in realized range
  216. anchorIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);
  217. var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
  218. anchorPosition = new Point(firstElementBounds.X, firstElementBounds.Y);
  219. }
  220. }
  221. Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Picked anchor: {Anchor}", layoutId, anchorIndex);
  222. _firstRealizedDataIndexInsideRealizationWindow = _lastRealizedDataIndexInsideRealizationWindow = anchorIndex;
  223. if (_elementManager.IsIndexValidInData(anchorIndex))
  224. {
  225. if (!_elementManager.IsDataIndexRealized(anchorIndex))
  226. {
  227. // Disconnected, throw everything and create new anchor
  228. Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId} Disconnected Window - throwing away all realized elements", layoutId);
  229. _elementManager.ClearRealizedRange();
  230. var anchor = _context!.GetOrCreateElementAt(anchorIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
  231. _elementManager.Add(anchor, anchorIndex);
  232. }
  233. var anchorElement = _elementManager.GetRealizedElement(anchorIndex);
  234. var desiredSize = MeasureElement(anchorElement!, anchorIndex, availableSize, _context!);
  235. var layoutBounds = new Rect(anchorPosition.X, anchorPosition.Y, desiredSize.Width, desiredSize.Height);
  236. _elementManager.SetLayoutBoundsForDataIndex(anchorIndex, layoutBounds);
  237. Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Layout bounds of anchor {anchor} are ({Bounds})",
  238. layoutId,
  239. anchorIndex,
  240. layoutBounds);
  241. }
  242. else
  243. {
  244. // Throw everything away
  245. Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId} Anchor index is not valid - throwing away all realized elements",
  246. layoutId);
  247. _elementManager.ClearRealizedRange();
  248. }
  249. // TODO: Perhaps we can track changes in the property setter
  250. _lastAvailableSize = availableSize;
  251. _lastItemSpacing = minItemSpacing;
  252. return anchorIndex;
  253. }
  254. private void Generate(
  255. GenerateDirection direction,
  256. int anchorIndex,
  257. Size availableSize,
  258. double minItemSpacing,
  259. double lineSpacing,
  260. int maxItemsPerLine,
  261. bool disableVirtualization,
  262. string? layoutId)
  263. {
  264. if (anchorIndex != -1)
  265. {
  266. int step = (direction == GenerateDirection.Forward) ? 1 : -1;
  267. Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Generating {Direction} from anchor {Anchor}",
  268. layoutId,
  269. direction,
  270. anchorIndex);
  271. int previousIndex = anchorIndex;
  272. int currentIndex = anchorIndex + step;
  273. var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex);
  274. var lineOffset = _orientation.MajorStart(anchorBounds);
  275. var lineMajorSize = _orientation.MajorSize(anchorBounds);
  276. var countInLine = 1;
  277. int count = 0;
  278. bool lineNeedsReposition = false;
  279. while (_elementManager.IsIndexValidInData(currentIndex) &&
  280. (disableVirtualization || ShouldContinueFillingUpSpace(previousIndex, direction)))
  281. {
  282. // Ensure layout element.
  283. _elementManager.EnsureElementRealized(direction == GenerateDirection.Forward, currentIndex, layoutId);
  284. var currentElement = _elementManager.GetRealizedElement(currentIndex);
  285. var desiredSize = MeasureElement(currentElement!, currentIndex, availableSize, _context!);
  286. ++count;
  287. // Lay it out.
  288. var previousElement = _elementManager.GetRealizedElement(previousIndex);
  289. var currentBounds = new Rect(0, 0, desiredSize.Width, desiredSize.Height);
  290. var previousElementBounds = _elementManager.GetLayoutBoundsForDataIndex(previousIndex);
  291. if (direction == GenerateDirection.Forward)
  292. {
  293. double remainingSpace = _orientation.Minor(availableSize) - (_orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing + _orientation.Minor(desiredSize));
  294. if (countInLine >= maxItemsPerLine || _algorithmCallbacks!.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
  295. {
  296. // No more space in this row. wrap to next row.
  297. _orientation.SetMinorStart(ref currentBounds, 0);
  298. _orientation.SetMajorStart(ref currentBounds, _orientation.MajorStart(previousElementBounds) + lineMajorSize + lineSpacing);
  299. if (lineNeedsReposition)
  300. {
  301. // reposition the previous line (countInLine items)
  302. for (int i = 0; i < countInLine; i++)
  303. {
  304. var dataIndex = currentIndex - 1 - i;
  305. var bounds = _elementManager.GetLayoutBoundsForDataIndex(dataIndex);
  306. _orientation.SetMajorSize(ref bounds, lineMajorSize);
  307. _elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds);
  308. }
  309. }
  310. // Setup for next line.
  311. lineMajorSize = _orientation.MajorSize(currentBounds);
  312. lineOffset = _orientation.MajorStart(currentBounds);
  313. lineNeedsReposition = false;
  314. countInLine = 1;
  315. }
  316. else
  317. {
  318. // More space is available in this row.
  319. _orientation.SetMinorStart(ref currentBounds, _orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing);
  320. _orientation.SetMajorStart(ref currentBounds, lineOffset);
  321. lineMajorSize = Math.Max(lineMajorSize, _orientation.MajorSize(currentBounds));
  322. lineNeedsReposition = _orientation.MajorSize(previousElementBounds) != _orientation.MajorSize(currentBounds);
  323. countInLine++;
  324. }
  325. }
  326. else
  327. {
  328. // Backward
  329. double remainingSpace = _orientation.MinorStart(previousElementBounds) - (_orientation.Minor(desiredSize) + minItemSpacing);
  330. if (countInLine >= maxItemsPerLine || _algorithmCallbacks!.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
  331. {
  332. // Does not fit, wrap to the previous row
  333. var availableSizeMinor = _orientation.Minor(availableSize);
  334. _orientation.SetMinorStart(ref currentBounds, !double.IsInfinity(availableSizeMinor) ? availableSizeMinor - _orientation.Minor(desiredSize) : 0);
  335. _orientation.SetMajorStart(ref currentBounds, lineOffset - _orientation.Major(desiredSize) - lineSpacing);
  336. if (lineNeedsReposition)
  337. {
  338. var previousLineOffset = _orientation.MajorStart(_elementManager.GetLayoutBoundsForDataIndex(currentIndex + countInLine + 1));
  339. // reposition the previous line (countInLine items)
  340. for (int i = 0; i < countInLine; i++)
  341. {
  342. var dataIndex = currentIndex + 1 + i;
  343. if (dataIndex != anchorIndex)
  344. {
  345. var bounds = _elementManager.GetLayoutBoundsForDataIndex(dataIndex);
  346. _orientation.SetMajorStart(ref bounds, previousLineOffset - lineMajorSize - lineSpacing);
  347. _orientation.SetMajorSize(ref bounds, lineMajorSize);
  348. _elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds);
  349. Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Corrected Layout bounds of element {Index} are ({Bounds})",
  350. layoutId,
  351. dataIndex,
  352. bounds);
  353. }
  354. }
  355. }
  356. // Setup for next line.
  357. lineMajorSize = _orientation.MajorSize(currentBounds);
  358. lineOffset = _orientation.MajorStart(currentBounds);
  359. lineNeedsReposition = false;
  360. countInLine = 1;
  361. }
  362. else
  363. {
  364. // Fits in this row. put it in the previous position
  365. _orientation.SetMinorStart(ref currentBounds, _orientation.MinorStart(previousElementBounds) - _orientation.Minor(desiredSize) - minItemSpacing);
  366. _orientation.SetMajorStart(ref currentBounds, lineOffset);
  367. lineMajorSize = Math.Max(lineMajorSize, _orientation.MajorSize(currentBounds));
  368. lineNeedsReposition = _orientation.MajorSize(previousElementBounds) != _orientation.MajorSize(currentBounds);
  369. countInLine++;
  370. }
  371. }
  372. _elementManager.SetLayoutBoundsForDataIndex(currentIndex, currentBounds);
  373. Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Layout bounds of element {Index} are ({Bounds}).",
  374. layoutId,
  375. currentIndex,
  376. currentBounds);
  377. previousIndex = currentIndex;
  378. currentIndex += step;
  379. }
  380. // If we did not reach the top or bottom of the extent, we realized one
  381. // extra item before we knew we were outside the realization window. Do not
  382. // account for that element in the indices inside the realization window.
  383. if (direction == GenerateDirection.Forward)
  384. {
  385. int dataCount = _context!.ItemCount;
  386. _lastRealizedDataIndexInsideRealizationWindow = previousIndex == dataCount - 1 ? dataCount - 1 : previousIndex - 1;
  387. _lastRealizedDataIndexInsideRealizationWindow = Math.Max(0, _lastRealizedDataIndexInsideRealizationWindow);
  388. }
  389. else
  390. {
  391. int dataCount = _context!.ItemCount;
  392. _firstRealizedDataIndexInsideRealizationWindow = previousIndex == 0 ? 0 : previousIndex + 1;
  393. _firstRealizedDataIndexInsideRealizationWindow = Math.Min(dataCount - 1, _firstRealizedDataIndexInsideRealizationWindow);
  394. }
  395. _elementManager.DiscardElementsOutsideWindow(direction == GenerateDirection.Forward, currentIndex);
  396. }
  397. }
  398. private void MakeAnchor(
  399. VirtualizingLayoutContext context,
  400. int index,
  401. Size availableSize)
  402. {
  403. _elementManager.ClearRealizedRange();
  404. // FlowLayout requires that the anchor is the first element in the row.
  405. var internalAnchor = _algorithmCallbacks!.Algorithm_GetAnchorForTargetElement(index, availableSize, context);
  406. // No need to set the position of the anchor.
  407. // (0,0) is fine for now since the extent can
  408. // grow in any direction.
  409. for (int dataIndex = internalAnchor.Index; dataIndex < index + 1; ++dataIndex)
  410. {
  411. var element = context.GetOrCreateElementAt(dataIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
  412. element.Measure(_algorithmCallbacks.Algorithm_GetMeasureSize(dataIndex, availableSize, context));
  413. _elementManager.Add(element, dataIndex);
  414. }
  415. }
  416. private bool IsReflowRequired()
  417. {
  418. // If first element is realized and is not at the very beginning we need to reflow.
  419. return
  420. _elementManager.GetRealizedElementCount() > 0 &&
  421. _elementManager.GetDataIndexFromRealizedRangeIndex(0) == 0 &&
  422. _orientation.MinorStart(_elementManager.GetLayoutBoundsForRealizedIndex(0)) != 0;
  423. }
  424. private bool ShouldContinueFillingUpSpace(
  425. int index,
  426. GenerateDirection direction)
  427. {
  428. bool shouldContinue = false;
  429. if (!IsVirtualizingContext)
  430. {
  431. shouldContinue = true;
  432. }
  433. else
  434. {
  435. var realizationRect = _context!.RealizationRect;
  436. var elementBounds = _elementManager.GetLayoutBoundsForDataIndex(index);
  437. var elementMajorStart = _orientation.MajorStart(elementBounds);
  438. var elementMajorEnd = _orientation.MajorEnd(elementBounds);
  439. var rectMajorStart = _orientation.MajorStart(realizationRect);
  440. var rectMajorEnd = _orientation.MajorEnd(realizationRect);
  441. var elementMinorStart = _orientation.MinorStart(elementBounds);
  442. var elementMinorEnd = _orientation.MinorEnd(elementBounds);
  443. var rectMinorStart = _orientation.MinorStart(realizationRect);
  444. var rectMinorEnd = _orientation.MinorEnd(realizationRect);
  445. // Ensure that both minor and major directions are taken into consideration so that if the scrolling direction
  446. // is the same as the flow direction we still stop at the end of the viewport rectangle.
  447. shouldContinue =
  448. (direction == GenerateDirection.Forward && elementMajorStart < rectMajorEnd && elementMinorStart < rectMinorEnd) ||
  449. (direction == GenerateDirection.Backward && elementMajorEnd > rectMajorStart && elementMinorEnd > rectMinorStart);
  450. }
  451. return shouldContinue;
  452. }
  453. private Rect EstimateExtent(Size availableSize, string? layoutId)
  454. {
  455. Layoutable? firstRealizedElement = null;
  456. Rect firstBounds = new Rect();
  457. Layoutable? lastRealizedElement = null;
  458. Rect lastBounds = new Rect();
  459. int firstDataIndex = -1;
  460. int lastDataIndex = -1;
  461. if (_elementManager.GetRealizedElementCount() > 0)
  462. {
  463. firstRealizedElement = _elementManager.GetAt(0);
  464. firstBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
  465. firstDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);
  466. int last = _elementManager.GetRealizedElementCount() - 1;
  467. lastRealizedElement = _elementManager.GetAt(last);
  468. lastDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(last);
  469. lastBounds = _elementManager.GetLayoutBoundsForRealizedIndex(last);
  470. }
  471. Rect extent = _algorithmCallbacks!.Algorithm_GetExtent(
  472. availableSize,
  473. _context!,
  474. firstRealizedElement,
  475. firstDataIndex,
  476. firstBounds,
  477. lastRealizedElement,
  478. lastDataIndex,
  479. lastBounds);
  480. Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId} Extent: ({Bounds})", layoutId, extent);
  481. return extent;
  482. }
  483. private void RaiseLineArranged()
  484. {
  485. var realizationRect = RealizationRect;
  486. if (realizationRect.Width != 0.0f || realizationRect.Height != 0.0f)
  487. {
  488. int realizedElementCount = _elementManager.GetRealizedElementCount();
  489. if (realizedElementCount > 0)
  490. {
  491. int countInLine = 0;
  492. var previousElementBounds = _elementManager.GetLayoutBoundsForDataIndex(_firstRealizedDataIndexInsideRealizationWindow);
  493. var currentLineOffset = _orientation.MajorStart(previousElementBounds);
  494. var currentLineSize = _orientation.MajorSize(previousElementBounds);
  495. for (int currentDataIndex = _firstRealizedDataIndexInsideRealizationWindow; currentDataIndex <= _lastRealizedDataIndexInsideRealizationWindow; currentDataIndex++)
  496. {
  497. var currentBounds = _elementManager.GetLayoutBoundsForDataIndex(currentDataIndex);
  498. if (_orientation.MajorStart(currentBounds) != currentLineOffset)
  499. {
  500. // Staring a new line
  501. _algorithmCallbacks!.Algorithm_OnLineArranged(currentDataIndex - countInLine, countInLine, currentLineSize, _context!);
  502. countInLine = 0;
  503. currentLineOffset = _orientation.MajorStart(currentBounds);
  504. currentLineSize = 0;
  505. }
  506. currentLineSize = Math.Max(currentLineSize, _orientation.MajorSize(currentBounds));
  507. countInLine++;
  508. previousElementBounds = currentBounds;
  509. }
  510. // Raise for the last line.
  511. _algorithmCallbacks!.Algorithm_OnLineArranged(_lastRealizedDataIndexInsideRealizationWindow - countInLine + 1, countInLine, currentLineSize, _context!);
  512. }
  513. }
  514. }
  515. private void ArrangeVirtualizingLayout(
  516. Size finalSize,
  517. LineAlignment lineAlignment,
  518. bool isWrapping,
  519. string? layoutId)
  520. {
  521. // Walk through the realized elements one line at a time and
  522. // align them, Then call element.Arrange with the arranged bounds.
  523. int realizedElementCount = _elementManager.GetRealizedElementCount();
  524. if (realizedElementCount > 0)
  525. {
  526. var countInLine = 1;
  527. var previousElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
  528. var currentLineOffset = _orientation.MajorStart(previousElementBounds);
  529. var spaceAtLineStart = _orientation.MinorStart(previousElementBounds);
  530. var spaceAtLineEnd = 0.0;
  531. var currentLineSize = _orientation.MajorSize(previousElementBounds);
  532. for (int i = 1; i < realizedElementCount; i++)
  533. {
  534. var currentBounds = _elementManager.GetLayoutBoundsForRealizedIndex(i);
  535. if (_orientation.MajorStart(currentBounds) != currentLineOffset)
  536. {
  537. spaceAtLineEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds);
  538. PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId);
  539. spaceAtLineStart = _orientation.MinorStart(currentBounds);
  540. countInLine = 0;
  541. currentLineOffset = _orientation.MajorStart(currentBounds);
  542. currentLineSize = 0;
  543. }
  544. countInLine++; // for current element
  545. currentLineSize = Math.Max(currentLineSize, _orientation.MajorSize(currentBounds));
  546. previousElementBounds = currentBounds;
  547. }
  548. // Last line - potentially have a property to customize
  549. // aligning the last line or not.
  550. if (countInLine > 0)
  551. {
  552. var spaceAtEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds);
  553. PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, isWrapping, finalSize, layoutId);
  554. }
  555. }
  556. }
  557. // Align elements within a line. Note that this does not modify LayoutBounds. So if we get
  558. // repeated measures, the LayoutBounds remain the same in each layout.
  559. private void PerformLineAlignment(
  560. int lineStartIndex,
  561. int countInLine,
  562. double spaceAtLineStart,
  563. double spaceAtLineEnd,
  564. double lineSize,
  565. LineAlignment lineAlignment,
  566. bool isWrapping,
  567. Size finalSize,
  568. string? layoutId)
  569. {
  570. for (int rangeIndex = lineStartIndex; rangeIndex < lineStartIndex + countInLine; ++rangeIndex)
  571. {
  572. var bounds = _elementManager.GetLayoutBoundsForRealizedIndex(rangeIndex);
  573. _orientation.SetMajorSize(ref bounds, lineSize);
  574. if (!_scrollOrientationSameAsFlow)
  575. {
  576. // Note: Space at start could potentially be negative
  577. if (spaceAtLineStart != 0 || spaceAtLineEnd != 0)
  578. {
  579. var totalSpace = spaceAtLineStart + spaceAtLineEnd;
  580. var minorStart = _orientation.MinorStart(bounds);
  581. switch (lineAlignment)
  582. {
  583. case LineAlignment.Start:
  584. {
  585. _orientation.SetMinorStart(ref bounds, minorStart - spaceAtLineStart);
  586. break;
  587. }
  588. case LineAlignment.End:
  589. {
  590. _orientation.SetMinorStart(ref bounds, minorStart + spaceAtLineEnd);
  591. break;
  592. }
  593. case LineAlignment.Center:
  594. {
  595. _orientation.SetMinorStart(ref bounds, (minorStart - spaceAtLineStart) + (totalSpace / 2));
  596. break;
  597. }
  598. case LineAlignment.SpaceAround:
  599. {
  600. var interItemSpace = countInLine >= 1 ? totalSpace / (countInLine * 2) : 0;
  601. _orientation.SetMinorStart(
  602. ref bounds,
  603. (minorStart - spaceAtLineStart) + (interItemSpace * ((rangeIndex - lineStartIndex + 1) * 2 - 1)));
  604. break;
  605. }
  606. case LineAlignment.SpaceBetween:
  607. {
  608. var interItemSpace = countInLine > 1 ? totalSpace / (countInLine - 1) : 0;
  609. _orientation.SetMinorStart(
  610. ref bounds,
  611. (minorStart - spaceAtLineStart) + (interItemSpace * (rangeIndex - lineStartIndex)));
  612. break;
  613. }
  614. case LineAlignment.SpaceEvenly:
  615. {
  616. var interItemSpace = countInLine >= 1 ? totalSpace / (countInLine + 1) : 0;
  617. _orientation.SetMinorStart(
  618. ref bounds,
  619. (minorStart - spaceAtLineStart) + (interItemSpace * (rangeIndex - lineStartIndex + 1)));
  620. break;
  621. }
  622. }
  623. }
  624. }
  625. bounds = bounds.Translate(-_lastExtent.Position);
  626. if (!isWrapping)
  627. {
  628. _orientation.SetMinorSize(
  629. ref bounds,
  630. Math.Max(_orientation.MinorSize(bounds), _orientation.Minor(finalSize)));
  631. }
  632. var element = _elementManager.GetAt(rangeIndex);
  633. Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Arranging element {Index} at ({Bounds})",
  634. layoutId,
  635. _elementManager.GetDataIndexFromRealizedRangeIndex(rangeIndex),
  636. bounds);
  637. element.Arrange(bounds);
  638. }
  639. }
  640. private void SetLayoutOrigin()
  641. {
  642. if (IsVirtualizingContext)
  643. {
  644. _context!.LayoutOrigin = new Point(_lastExtent.X, _lastExtent.Y);
  645. }
  646. }
  647. public Layoutable? GetElementIfRealized(int dataIndex)
  648. {
  649. if (_elementManager.IsDataIndexRealized(dataIndex))
  650. {
  651. return _elementManager.GetRealizedElement(dataIndex);
  652. }
  653. return null;
  654. }
  655. public bool TryAddElement0(Layoutable element)
  656. {
  657. if (_elementManager.GetRealizedElementCount() == 0)
  658. {
  659. _elementManager.Add(element, 0);
  660. return true;
  661. }
  662. return false;
  663. }
  664. public enum LineAlignment
  665. {
  666. Start,
  667. Center,
  668. End,
  669. SpaceAround,
  670. SpaceBetween,
  671. SpaceEvenly,
  672. }
  673. private enum GenerateDirection
  674. {
  675. Forward,
  676. Backward,
  677. }
  678. }
  679. }