1 /* Copyright (c) 2021 Samsung Electronics Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
7 * http://www.apache.org/licenses/LICENSE-2.0
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
17 using System.Collections.Generic;
18 using System.ComponentModel;
19 using Tizen.NUI.BaseComponents;
21 namespace Tizen.NUI.Components
24 /// Layouter for CollectionView to display items in grid layout.
26 /// <since_tizen> 9 </since_tizen>
27 public class GridLayouter : ItemsLayouter
29 private CollectionView colView;
30 private (float Width, float Height) sizeCandidate;
31 private int spanSize = 1;
32 private float align = 0.5f;
33 private bool hasHeader;
34 private Extents headerMargin;
35 private float headerSize;
36 private Extents footerMargin;
37 private bool hasFooter;
38 private float footerSize;
39 private bool isGrouped;
40 private readonly List<GroupInfo> groups = new List<GroupInfo>();
41 private float groupHeaderSize;
42 private Extents groupHeaderMargin;
43 private float groupFooterSize;
44 private Extents groupFooterMargin;
45 private GroupInfo Visited;
46 private Timer requestLayoutTimer = null;
49 /// Clean up ItemsLayouter.
51 /// <param name="view"> CollectionView of layouter. </param>
52 /// <since_tizen> 9 </since_tizen>
53 /// <remarks>please note that, view must be type of CollectionView</remarks>
54 public override void Initialize(RecyclerView view)
56 colView = view as CollectionView;
59 throw new ArgumentException("GridLayouter only can be applied CollectionView.", nameof(view));
63 foreach (RecyclerViewItem item in VisibleItems)
65 colView.UnrealizeItem(item, false);
73 IsHorizontal = (colView.ScrollingDirection == ScrollableBase.Direction.Horizontal);
75 RecyclerViewItem header = colView?.Header;
76 RecyclerViewItem footer = colView?.Footer;
78 int count = colView.InternalItemSource.Count;
79 int pureCount = count - (header? 1 : 0) - (footer? 1 : 0);
81 // 2. Get the header / footer and size deligated item and measure the size.
84 MeasureChild(colView, header);
86 width = header.Layout != null? header.Layout.MeasuredWidth.Size.AsRoundedValue() : 0;
87 height = header.Layout != null? header.Layout.MeasuredHeight.Size.AsRoundedValue() : 0;
89 Extents itemMargin = header.Margin;
90 headerSize = IsHorizontal?
91 width + itemMargin.Start + itemMargin.End:
92 height + itemMargin.Top + itemMargin.Bottom;
93 headerMargin = new Extents(itemMargin);
96 colView.UnrealizeItem(header);
101 MeasureChild(colView, footer);
103 width = footer.Layout != null? footer.Layout.MeasuredWidth.Size.AsRoundedValue() : 0;
104 height = footer.Layout != null? footer.Layout.MeasuredHeight.Size.AsRoundedValue() : 0;
106 Extents itemMargin = footer.Margin;
107 footerSize = IsHorizontal?
108 width + itemMargin.Start + itemMargin.End:
109 height + itemMargin.Top + itemMargin.Bottom;
110 footerMargin = new Extents(itemMargin);
111 footer.Index = count - 1;
114 colView.UnrealizeItem(footer);
117 int firstIndex = header? 1 : 0;
119 if (colView.IsGrouped)
123 if (colView.GroupHeaderTemplate != null)
125 while (!colView.InternalItemSource.IsGroupHeader(firstIndex)) firstIndex++;
126 //must be always true
127 if (colView.InternalItemSource.IsGroupHeader(firstIndex))
129 RecyclerViewItem groupHeader = colView.RealizeItem(firstIndex);
132 if (groupHeader == null) throw new Exception("[" + firstIndex + "] Group Header failed to realize!");
134 // Need to Set proper height or width on scroll direction.
135 if (groupHeader.Layout == null)
137 width = groupHeader.WidthSpecification;
138 height = groupHeader.HeightSpecification;
142 MeasureChild(colView, groupHeader);
144 width = groupHeader.Layout.MeasuredWidth.Size.AsRoundedValue();
145 height = groupHeader.Layout.MeasuredHeight.Size.AsRoundedValue();
147 //Console.WriteLine("[NUI] GroupHeader Size {0} :{0}", width, height);
148 // pick the StepCandidate.
149 Extents itemMargin = groupHeader.Margin;
150 groupHeaderSize = IsHorizontal?
151 width + itemMargin.Start + itemMargin.End:
152 height + itemMargin.Top + itemMargin.Bottom;
153 groupHeaderMargin = new Extents(itemMargin);
154 colView.UnrealizeItem(groupHeader);
159 groupHeaderSize = 0F;
162 if (colView.GroupFooterTemplate != null)
164 int firstFooter = firstIndex;
165 while (!colView.InternalItemSource.IsGroupFooter(firstFooter)) firstFooter++;
166 //must be always true
167 if (colView.InternalItemSource.IsGroupFooter(firstFooter))
169 RecyclerViewItem groupFooter = colView.RealizeItem(firstFooter);
171 if (groupFooter == null) throw new Exception("[" + firstFooter + "] Group Footer failed to realize!");
172 // Need to Set proper height or width on scroll direction.
173 if (groupFooter.Layout == null)
175 width = groupFooter.WidthSpecification;
176 height = groupFooter.HeightSpecification;
180 MeasureChild(colView, groupFooter);
182 width = groupFooter.Layout.MeasuredWidth.Size.AsRoundedValue();
183 height = groupFooter.Layout.MeasuredHeight.Size.AsRoundedValue();
185 // pick the StepCandidate.
186 Extents itemMargin = groupFooter.Margin;
187 groupFooterSize = IsHorizontal?
188 width + itemMargin.Start + itemMargin.End:
189 height + itemMargin.Top + itemMargin.Bottom;
190 groupFooterMargin = new Extents(itemMargin);
192 colView.UnrealizeItem(groupFooter);
197 groupFooterSize = 0F;
200 else isGrouped = false;
203 //Final Check of FirstIndex
204 while (colView.InternalItemSource.IsHeader(firstIndex) ||
205 colView.InternalItemSource.IsGroupHeader(firstIndex) ||
206 colView.InternalItemSource.IsGroupFooter(firstIndex))
208 if (colView.InternalItemSource.IsFooter(firstIndex))
217 sizeCandidate = (0, 0);
220 // Get Size Deligate. FIXME if group exist index must be changed.
221 RecyclerViewItem sizeDeligate = colView.RealizeItem(firstIndex);
222 if (sizeDeligate == null)
224 throw new Exception("Cannot create content from DatTemplate.");
226 sizeDeligate.BindingContext = colView.InternalItemSource.GetItem(firstIndex);
228 // Need to Set proper height or width on scroll direction.
229 if (sizeDeligate.Layout == null)
231 width = sizeDeligate.WidthSpecification;
232 height = sizeDeligate.HeightSpecification;
236 MeasureChild(colView, sizeDeligate);
238 width = sizeDeligate.Layout.MeasuredWidth.Size.AsRoundedValue();
239 height = sizeDeligate.Layout.MeasuredHeight.Size.AsRoundedValue();
241 //Console.WriteLine("[NUI] item Size {0} :{1}", width, height);
243 // pick the StepCandidate.
244 Extents itemMargin = sizeDeligate.Margin;
245 width = width + itemMargin.Start + itemMargin.End;
246 height = height + itemMargin.Top + itemMargin.Bottom;
247 StepCandidate = IsHorizontal? width : height;
248 CandidateMargin = new Extents(itemMargin);
249 spanSize = IsHorizontal?
250 Convert.ToInt32(Math.Truncate((double)((colView.Size.Height - Padding.Top - Padding.Bottom) / height))) :
251 Convert.ToInt32(Math.Truncate((double)((colView.Size.Width - Padding.Start - Padding.End) / width)));
253 sizeCandidate = (width, height);
255 colView.UnrealizeItem(sizeDeligate);
258 if (StepCandidate < 1) StepCandidate = 1;
259 if (spanSize < 1) spanSize = 1;
263 float Current = 0.0F;
264 IGroupableItemSource source = colView.InternalItemSource;
265 GroupInfo currentGroup = null;
267 for (int i = 0; i < count; i++)
269 if (i == 0 && hasHeader)
271 Current += headerSize;
273 else if (i == count - 1 && hasFooter)
275 Current += footerSize;
279 //GroupHeader must always exist in group usage.
280 if (source.IsGroupHeader(i))
282 currentGroup = new GroupInfo()
284 GroupParent = source.GetGroupParent(i),
287 GroupSize = groupHeaderSize,
288 GroupPosition = Current
290 groups.Add(currentGroup);
291 Current += groupHeaderSize;
294 else if (source.IsGroupFooter(i))
296 //currentGroup.hasFooter = true;
297 currentGroup.Count++;
298 currentGroup.GroupSize += groupFooterSize;
299 Current += groupFooterSize;
303 currentGroup.Count++;
304 int index = i - currentGroup.StartIndex - 1; // groupHeader must always exist.
305 if ((index % spanSize) == 0)
307 currentGroup.GroupSize += StepCandidate;
308 Current += StepCandidate;
313 ScrollContentSize = Current;
317 // 3. Measure the scroller content size.
318 ScrollContentSize = StepCandidate * Convert.ToInt32(Math.Ceiling((double)pureCount / (double)spanSize));
319 if (hasHeader) ScrollContentSize += headerSize;
320 if (hasFooter) ScrollContentSize += footerSize;
323 ScrollContentSize = IsHorizontal?
324 ScrollContentSize + Padding.Start + Padding.End:
325 ScrollContentSize + Padding.Top + Padding.Bottom;
327 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
328 else colView.ContentContainer.SizeHeight = ScrollContentSize;
330 base.Initialize(colView);
331 //Console.WriteLine("Init Done, StepCnadidate{0}, spanSize{1}, Scroll{2}", StepCandidate, spanSize, ScrollContentSize);
335 /// This is called to find out where items are lain out according to current scroll position.
337 /// <param name="scrollPosition">Scroll position which is calculated by ScrollableBase</param>
338 /// <param name="force">boolean force flag to layouting forcely.</param>
339 /// <since_tizen> 9 </since_tizen>
340 public override void RequestLayout(float scrollPosition, bool force = false)
342 // Layouting is only possible after once it intialized.
343 if (!IsInitialized) return;
344 int LastIndex = colView.InternalItemSource.Count;
346 if (!force && PrevScrollPosition == Math.Abs(scrollPosition)) return;
347 PrevScrollPosition = Math.Abs(scrollPosition);
349 int prevFirstVisible = FirstVisible;
350 int prevLastVisible = LastVisible;
351 bool IsHorizontal = (colView.ScrollingDirection == ScrollableBase.Direction.Horizontal);
353 (float X, float Y) visibleArea = (PrevScrollPosition,
354 PrevScrollPosition + (IsHorizontal? colView.Size.Width : colView.Size.Height)
357 //Console.WriteLine("[NUI] itemsView [{0},{1}] [{2},{3}]", colView.Size.Width, colView.Size.Height, colView.ContentContainer.Size.Width, colView.ContentContainer.Size.Height);
359 // 1. Set First/Last Visible Item Index.
360 (int start, int end) = FindVisibleItems(visibleArea);
361 FirstVisible = start;
364 //Console.WriteLine("[NUI] {0} :visibleArea before [{1},{2}] after [{3},{4}]", scrollPosition, prevFirstVisible, prevLastVisible, FirstVisible, LastVisible);
366 // 2. Unrealize invisible items.
367 List<RecyclerViewItem> unrealizedItems = new List<RecyclerViewItem>();
368 foreach (RecyclerViewItem item in VisibleItems)
370 if (item.Index < FirstVisible || item.Index > LastVisible)
372 //Console.WriteLine("[NUI] Unrealize{0}!", item.Index);
373 unrealizedItems.Add(item);
374 colView.UnrealizeItem(item);
377 VisibleItems.RemoveAll(unrealizedItems.Contains);
379 //Console.WriteLine("Realize Begin [{0} to {1}]", FirstVisible, LastVisible);
380 // 3. Realize and placing visible items.
381 for (int i = FirstVisible; i <= LastVisible; i++)
383 //Console.WriteLine("[NUI] Realize!");
384 RecyclerViewItem item = null;
385 // 4. Get item if visible or realize new.
386 if (i >= prevFirstVisible && i <= prevLastVisible)
388 item = GetVisibleItem(i);
389 if (item != null && !force) continue;
393 item = colView.RealizeItem(i);
394 if (item != null) VisibleItems.Add(item);
395 else throw new Exception("Failed to create RecycerViewItem index of ["+ i + "]");
398 //item Position without Padding and Margin.
399 (float x, float y) = GetItemPosition(i);
400 // 5. Placing item with Padding and Margin.
401 item.Position = new Position(x, y);
403 //Linear Item need to be resized!
404 if (item.IsHeader || item.IsFooter || item.isGroupHeader || item.isGroupFooter)
406 if (IsHorizontal && item.HeightSpecification == LayoutParamPolicies.MatchParent)
408 item.Size = new Size(item.Size.Width, Container.Size.Height - Padding.Top - Padding.Bottom - item.Margin.Top - item.Margin.Bottom);
410 else if (!IsHorizontal && item.WidthSpecification == LayoutParamPolicies.MatchParent)
412 item.Size = new Size(Container.Size.Width - Padding.Start - Padding.End - item.Margin.Start - item.Margin.End, item.Size.Height);
415 //Console.WriteLine("[NUI] ["+item.Index+"] ["+item.Position.X+", "+item.Position.Y+" ==== \n");
417 //Console.WriteLine("Realize Done");
421 /// Clear the current screen and all properties.
423 [EditorBrowsable(EditorBrowsableState.Never)]
424 public override void Clear()
427 if (requestLayoutTimer != null)
429 requestLayoutTimer.Dispose();
434 foreach (GroupInfo group in groups)
436 //group.ItemPosition?.Clear();
443 if (headerMargin != null)
445 headerMargin.Dispose();
448 if (footerMargin != null)
450 footerMargin.Dispose();
453 if (groupHeaderMargin != null)
455 groupHeaderMargin.Dispose();
456 groupHeaderMargin = null;
458 if (groupFooterMargin != null)
460 groupFooterMargin.Dispose();
461 groupFooterMargin = null;
468 public override void NotifyItemSizeChanged(RecyclerViewItem item)
470 // All Item size need to be same in grid!
471 // if you want to change item size, change dataTemplate to re-initing.
476 [EditorBrowsable(EditorBrowsableState.Never)]
477 public override void NotifyItemInserted(IItemSource source, int startIndex)
479 // Insert Single item.
480 if (source == null) throw new ArgumentNullException(nameof(source));
482 // Will be null if not a group.
483 float currentSize = 0;
484 IGroupableItemSource gSource = source as IGroupableItemSource;
486 // Get the first Visible Position to adjust.
488 int topInScreenIndex = 0;
490 (topInScreenIndex, offset) = FindTopItemInScreen();
493 //2. Handle Group Case.
494 if (isGrouped && gSource != null)
496 GroupInfo groupInfo = null;
497 object groupParent = gSource.GetGroupParent(startIndex);
498 int parentIndex = gSource.GetPosition(groupParent);
499 if (gSource.HasHeader) parentIndex--;
501 // Check item is group parent or not
502 // if group parent, add new gorupinfo
503 if (gSource.IsHeader(startIndex))
505 // This is childless group.
506 // create new groupInfo!
507 groupInfo = new GroupInfo()
509 GroupParent = groupParent,
510 StartIndex = startIndex,
512 GroupSize = groupHeaderSize,
515 if (parentIndex >= groups.Count)
517 groupInfo.GroupPosition = ScrollContentSize;
518 groups.Add(groupInfo);
522 groupInfo.GroupPosition = groups[parentIndex].GroupPosition;
523 groups.Insert(parentIndex, groupInfo);
526 currentSize = groupHeaderSize;
530 // If not group parent, add item into the groupinfo.
531 if (parentIndex >= groups.Count) throw new Exception("group parent is bigger than group counts.");
532 groupInfo = groups[parentIndex];//GetGroupInfo(groupParent);
533 if (groupInfo == null) throw new Exception("Cannot find group information!");
535 if (gSource.IsGroupFooter(startIndex))
537 // It doesn't make sence to adding footer by notify...
538 // if GroupFooterTemplate is added,
539 // need to implement on here.
543 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
545 // Wrong! Grid Layouter do not support MeasureAll!
549 int pureCount = groupInfo.Count - 1 - (colView.GroupFooterTemplate == null? 0: 1);
550 if (pureCount % spanSize == 0)
552 currentSize = StepCandidate;
553 groupInfo.GroupSize += currentSize;
562 if (parentIndex + 1 < groups.Count)
564 for(int i = parentIndex + 1; i < groups.Count; i++)
566 groups[i].GroupPosition += currentSize;
567 groups[i].StartIndex++;
573 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
575 // Wrong! Grid Layouter do not support MeasureAll!
577 int pureCount = colView.InternalItemSource.Count - (hasHeader? 1: 0) - (hasFooter? 1: 0);
579 // Count comes after updated in ungrouped case!
580 if (pureCount % spanSize == 1)
582 currentSize = StepCandidate;
586 // 3. Update Scroll Content Size
587 ScrollContentSize += currentSize;
589 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
590 else colView.ContentContainer.SizeHeight = ScrollContentSize;
592 // 4. Update Visible Items.
593 foreach (RecyclerViewItem item in VisibleItems)
595 if (item.Index >= startIndex)
601 float scrollPosition = PrevScrollPosition;
605 // Insertion above Top Visible!
606 if (startIndex <= topInScreenIndex)
608 scrollPosition = GetItemPosition(topInScreenIndex);
609 scrollPosition -= offset;
611 colView.ScrollTo(scrollPosition);
615 // Update Viewport in delay.
616 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
617 // but currently we do not have any accessor to pre-calculation so instead of this,
618 // using Timer temporarily.
619 DelayedRequestLayout(scrollPosition);
623 [EditorBrowsable(EditorBrowsableState.Never)]
624 public override void NotifyItemRangeInserted(IItemSource source, int startIndex, int count)
627 if (source == null) throw new ArgumentNullException(nameof(source));
629 float currentSize = 0;
630 // Will be null if not a group.
631 IGroupableItemSource gSource = source as IGroupableItemSource;
633 // Get the first Visible Position to adjust.
635 int topInScreenIndex = 0;
637 (topInScreenIndex, offset) = FindTopItemInScreen();
640 // 2. Handle Group Case
641 // Adding ranged items should all same new groups.
642 if (isGrouped && gSource != null)
644 GroupInfo groupInfo = null;
645 object groupParent = gSource.GetGroupParent(startIndex);
646 int parentIndex = gSource.GetPosition(groupParent);
647 if (gSource.HasHeader) parentIndex--;
648 int groupStartIndex = 0;
649 if (gSource.IsGroupHeader(startIndex))
651 groupStartIndex = startIndex;
656 throw new Exception("Inserted wrong groups!");
659 for (int current = startIndex; current - startIndex < count; current++)
661 // Check item is group parent or not
662 // if group parent, add new gorupinfo
663 if (groupStartIndex == current)
665 //create new groupInfo!
666 groupInfo = new GroupInfo()
668 GroupParent = groupParent,
669 StartIndex = current,
671 GroupSize = groupHeaderSize,
673 currentSize += groupHeaderSize;
678 //if not group parent, add item into the groupinfo.
679 //groupInfo = GetGroupInfo(groupStartIndex);
680 if (groupInfo == null) throw new Exception("Cannot find group information!");
683 if (gSource.IsGroupFooter(current))
685 groupInfo.GroupSize += groupFooterSize;
686 currentSize += groupFooterSize;
690 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
692 // Wrong! Grid Layouter do not support MeasureAll!
696 int index = current - groupInfo.StartIndex - 1; // groupHeader must always exist.
697 if ((index % spanSize) == 0)
699 groupInfo.GroupSize += StepCandidate;
700 currentSize += StepCandidate;
707 if (parentIndex >= groups.Count)
709 groupInfo.GroupPosition = ScrollContentSize;
710 groups.Add(groupInfo);
714 groupInfo.GroupPosition = groups[parentIndex].GroupPosition;
715 groups.Insert(parentIndex, groupInfo);
718 // Update other below group's position
719 if (parentIndex + 1 < groups.Count)
721 for(int i = parentIndex + 1; i < groups.Count; i++)
723 groups[i].GroupPosition += currentSize;
724 groups[i].StartIndex += count;
728 ScrollContentSize += currentSize;
732 throw new Exception("Cannot insert ungrouped range items!");
735 // 3. Update Scroll Content Size
736 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
737 else colView.ContentContainer.SizeHeight = ScrollContentSize;
739 // 4. Update Visible Items.
740 foreach (RecyclerViewItem item in VisibleItems)
742 if (item.Index >= startIndex)
749 float scrollPosition = PrevScrollPosition;
751 // Insertion above Top Visible!
752 if (startIndex + count <= topInScreenIndex)
754 scrollPosition = GetItemPosition(topInScreenIndex);
755 scrollPosition -= offset;
757 colView.ScrollTo(scrollPosition);
761 // Update Viewport in delay.
762 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
763 // but currently we do not have any accessor to pre-calculation so instead of this,
764 // using Timer temporarily.
765 DelayedRequestLayout(scrollPosition);
769 [EditorBrowsable(EditorBrowsableState.Never)]
770 public override void NotifyItemRemoved(IItemSource source, int startIndex)
773 if (source == null) throw new ArgumentNullException(nameof(source));
775 // Will be null if not a group.
776 float currentSize = 0;
777 IGroupableItemSource gSource = source as IGroupableItemSource;
779 // Get the first Visible Position to adjust.
781 int topInScreenIndex = 0;
783 (topInScreenIndex, offset) = FindTopItemInScreen();
786 // 2. Handle Group Case
787 if (isGrouped && gSource != null)
790 GroupInfo groupInfo = null;
791 foreach(GroupInfo cur in groups)
793 if ((cur.StartIndex <= startIndex) && (cur.StartIndex + cur.Count - 1 >= startIndex))
800 if (groupInfo == null) throw new Exception("Cannot find group information!");
801 // Check item is group parent or not
802 // if group parent, add new gorupinfo
803 if (groupInfo.StartIndex == startIndex)
805 // This is empty group!
806 // check group is empty.
807 if (groupInfo.Count != 1)
809 throw new Exception("Cannot remove group parent");
811 currentSize = groupInfo.GroupSize;
814 // groupInfo.Dispose();
815 groups.Remove(groupInfo);
821 // Skip footer case as footer cannot exist alone without header.
822 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
824 // Wrong! Grid Layouter do not support MeasureAll!
828 int pureCount = groupInfo.Count - 1 - (colView.GroupFooterTemplate == null? 0: 1);
829 if (pureCount % spanSize == 0)
831 currentSize = StepCandidate;
832 groupInfo.GroupSize -= currentSize;
837 for (int i = parentIndex + 1; i < groups.Count; i++)
839 groups[i].GroupPosition -= currentSize;
840 groups[i].StartIndex--;
845 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
847 // Wrong! Grid Layouter do not support MeasureAll!
849 int pureCount = colView.InternalItemSource.Count - (hasHeader? 1: 0) - (hasFooter? 1: 0);
851 // Count comes after updated in ungrouped case!
852 if (pureCount % spanSize == 0)
854 currentSize = StepCandidate;
859 ScrollContentSize -= currentSize;
861 // 3. Update Scroll Content Size
862 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
863 else colView.ContentContainer.SizeHeight = ScrollContentSize;
865 // 4. Update Visible Items.
866 RecyclerViewItem targetItem = null;
867 foreach (RecyclerViewItem item in VisibleItems)
869 if (item.Index == startIndex)
872 colView.UnrealizeItem(item);
874 else if (item.Index > startIndex)
879 VisibleItems.Remove(targetItem);
882 float scrollPosition = PrevScrollPosition;
884 // Insertion above Top Visible!
885 if (startIndex <= topInScreenIndex)
887 scrollPosition = GetItemPosition(topInScreenIndex);
888 scrollPosition -= offset;
890 colView.ScrollTo(scrollPosition);
894 // Update Viewport in delay.
895 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
896 // but currently we do not have any accessor to pre-calculation so instead of this,
897 // using Timer temporarily.
898 DelayedRequestLayout(scrollPosition);
902 [EditorBrowsable(EditorBrowsableState.Never)]
903 public override void NotifyItemRangeRemoved(IItemSource source, int startIndex, int count)
906 if (source == null) throw new ArgumentNullException(nameof(source));
908 // Will be null if not a group.
909 float currentSize = StepCandidate;
910 IGroupableItemSource gSource = source as IGroupableItemSource;
912 // Get the first Visible Position to adjust.
914 int topInScreenIndex = 0;
916 (topInScreenIndex, offset) = FindTopItemInScreen();
919 // 1. Handle Group Case
920 if (isGrouped && gSource != null)
923 GroupInfo groupInfo = null;
924 foreach(GroupInfo cur in groups)
926 if ((cur.StartIndex == startIndex) && (cur.Count == count))
933 if (groupInfo == null) throw new Exception("Cannot find group information!");
934 // Check item is group parent or not
935 // if group parent, add new gorupinfo
936 currentSize = groupInfo.GroupSize;
937 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
939 // Wrong! Grid Layouter do not support MeasureAll!
942 // groupInfo.Dispose();
943 groups.Remove(groupInfo);
945 for (int i = parentIndex; i < groups.Count; i++)
947 groups[i].GroupPosition -= currentSize;
948 groups[i].StartIndex -= count;
953 // It must group case! throw exception!
954 throw new Exception("Range remove must group remove!");
957 ScrollContentSize -= currentSize;
959 // 2. Update Scroll Content Size
960 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
961 else colView.ContentContainer.SizeHeight = ScrollContentSize;
963 // 3. Update Visible Items.
964 List<RecyclerViewItem> unrealizedItems = new List<RecyclerViewItem>();
965 foreach (RecyclerViewItem item in VisibleItems)
967 if ((item.Index >= startIndex)
968 && (item.Index < startIndex + count))
970 unrealizedItems.Add(item);
971 colView.UnrealizeItem(item);
973 else if (item.Index >= startIndex + count)
978 VisibleItems.RemoveAll(unrealizedItems.Contains);
979 unrealizedItems.Clear();
982 float scrollPosition = PrevScrollPosition;
984 // Insertion above Top Visible!
985 if (startIndex <= topInScreenIndex)
987 scrollPosition = GetItemPosition(topInScreenIndex);
988 scrollPosition -= offset;
990 colView.ScrollTo(scrollPosition);
994 // Update Viewport in delay.
995 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
996 // but currently we do not have any accessor to pre-calculation so instead of this,
997 // using Timer temporarily.
998 DelayedRequestLayout(scrollPosition);
1002 [EditorBrowsable(EditorBrowsableState.Never)]
1003 public override void NotifyItemMoved(IItemSource source, int fromPosition, int toPosition)
1006 if (source == null) throw new ArgumentNullException(nameof(source));
1008 // Will be null if not a group.
1009 float currentSize = StepCandidate;
1010 int diff = toPosition - fromPosition;
1012 // Get the first Visible Position to adjust.
1014 int topInScreenIndex = 0;
1016 (topInScreenIndex, offset) = FindTopItemInScreen();
1019 // Move can only happen in it's own groups.
1020 // so there will be no changes in position, startIndex in ohter groups.
1021 // check visible item and update indexs.
1022 int startIndex = ( diff > 0 ? fromPosition: toPosition);
1023 int endIndex = (diff > 0 ? toPosition: fromPosition);
1025 if ((endIndex >= FirstVisible) && (startIndex <= LastVisible))
1027 foreach (RecyclerViewItem item in VisibleItems)
1029 if ((item.Index >= startIndex)
1030 && (item.Index <= endIndex))
1032 if (item.Index == fromPosition) item.Index = toPosition;
1035 if (diff > 0) item.Index--;
1042 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
1043 else colView.ContentContainer.SizeHeight = ScrollContentSize;
1046 float scrollPosition = PrevScrollPosition;
1048 // Insertion above Top Visible!
1049 if (((fromPosition > topInScreenIndex) && (toPosition < topInScreenIndex) ||
1050 ((fromPosition < topInScreenIndex) && (toPosition > topInScreenIndex)))
1052 scrollPosition = GetItemPosition(topInScreenIndex);
1053 scrollPosition -= offset;
1055 colView.ScrollTo(scrollPosition);
1059 // Update Viewport in delay.
1060 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
1061 // but currently we do not have any accessor to pre-calculation so instead of this,
1062 // using Timer temporarily.
1063 DelayedRequestLayout(scrollPosition);
1067 [EditorBrowsable(EditorBrowsableState.Never)]
1068 public override void NotifyItemRangeMoved(IItemSource source, int fromPosition, int toPosition, int count)
1071 if (source == null) throw new ArgumentNullException(nameof(source));
1073 // Will be null if not a group.
1074 float currentSize = StepCandidate;
1075 int diff = toPosition - fromPosition;
1077 int startIndex = ( diff > 0 ? fromPosition: toPosition);
1078 int endIndex = (diff > 0 ? toPosition + count - 1: fromPosition + count - 1);
1080 // 2. Handle Group Case
1083 int fromParentIndex = 0;
1084 int toParentIndex = 0;
1085 bool findFrom = false;
1086 bool findTo = false;
1087 GroupInfo fromGroup = null;
1088 GroupInfo toGroup = null;
1090 foreach(GroupInfo cur in groups)
1092 if ((cur.StartIndex == fromPosition) && (cur.Count == count))
1096 if (findFrom && findTo) break;
1098 else if (cur.StartIndex == toPosition)
1102 if (findFrom && findTo) break;
1104 if (!findFrom) fromParentIndex++;
1105 if (!findTo) toParentIndex++;
1107 if (toGroup == null || fromGroup == null) throw new Exception("Cannot find group information!");
1109 fromGroup.StartIndex = toGroup.StartIndex;
1110 fromGroup.GroupPosition = toGroup.GroupPosition;
1112 endIndex = (diff > 0 ? toPosition + toGroup.Count - 1: fromPosition + count - 1);
1114 groups.Remove(fromGroup);
1115 groups.Insert(toParentIndex, fromGroup);
1117 int startGroup = (diff > 0? fromParentIndex: toParentIndex);
1118 int endGroup = (diff > 0? toParentIndex: fromParentIndex);
1120 for (int i = startGroup; i <= endGroup; i++)
1122 if (i == toParentIndex) continue;
1123 float prevPos = groups[i].GroupPosition;
1124 int prevIdx = groups[i].StartIndex;
1125 groups[i].GroupPosition = groups[i].GroupPosition + (diff > 0? -1 : 1) * fromGroup.GroupSize;
1126 groups[i].StartIndex = groups[i].StartIndex + (diff > 0? -1 : 1) * fromGroup.Count;
1131 //It must group case! throw exception!
1132 throw new Exception("Range remove must group remove!");
1135 // Move can only happen in it's own groups.
1136 // so there will be no changes in position, startIndex in ohter groups.
1137 // check visible item and update indexs.
1138 if ((endIndex >= FirstVisible) && (startIndex <= LastVisible))
1140 foreach (RecyclerViewItem item in VisibleItems)
1142 if ((item.Index >= startIndex)
1143 && (item.Index <= endIndex))
1145 if ((item.Index >= fromPosition) && (item.Index < fromPosition + count))
1147 item.Index = fromPosition - item.Index + toPosition;
1151 if (diff > 0) item.Index -= count;
1152 else item.Index += count;
1159 float scrollPosition = PrevScrollPosition;
1161 // Insertion above Top Visible!
1162 if (((fromPosition > topInScreenIndex) && (toPosition < topInScreenIndex) ||
1163 ((fromPosition < topInScreenIndex) && (toPosition > topInScreenIndex)))
1165 scrollPosition = GetItemPosition(topInScreenIndex);
1166 scrollPosition -= offset;
1168 colView.ScrollTo(scrollPosition);
1172 // Update Viewport in delay.
1173 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
1174 // but currently we do not have any accessor to pre-calculation so instead of this,
1175 // using Timer temporarily.
1176 DelayedRequestLayout(scrollPosition);
1180 [EditorBrowsable(EditorBrowsableState.Never)]
1181 public override float CalculateLayoutOrientationSize()
1183 //Console.WriteLine("[NUI] Calculate Layout ScrollContentSize {0}", ScrollContentSize);
1184 return ScrollContentSize;
1188 [EditorBrowsable(EditorBrowsableState.Never)]
1189 public override float CalculateCandidateScrollPosition(float scrollPosition)
1191 //Console.WriteLine("[NUI] Calculate Candidate ScrollContentSize {0}", ScrollContentSize);
1192 return scrollPosition;
1196 [EditorBrowsable(EditorBrowsableState.Never)]
1197 public override View RequestNextFocusableView(View currentFocusedView, View.FocusDirection direction, bool loopEnabled)
1199 if (currentFocusedView == null)
1200 throw new ArgumentNullException(nameof(currentFocusedView));
1202 View nextFocusedView = null;
1203 int targetSibling = -1;
1204 bool IsHorizontal = colView.ScrollingDirection == ScrollableBase.Direction.Horizontal;
1208 case View.FocusDirection.Left:
1210 targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder - 1 : targetSibling;
1213 case View.FocusDirection.Right:
1215 targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder + 1 : targetSibling;
1218 case View.FocusDirection.Up:
1220 targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder - 1;
1223 case View.FocusDirection.Down:
1225 targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder + 1;
1230 if (targetSibling > -1 && targetSibling < Container.Children.Count)
1232 RecyclerViewItem candidate = Container.Children[targetSibling] as RecyclerViewItem;
1233 if (candidate.Index >= 0 && candidate.Index < colView.InternalItemSource.Count)
1235 nextFocusedView = candidate;
1238 return nextFocusedView;
1242 [EditorBrowsable(EditorBrowsableState.Never)]
1243 protected override (int start, int end) FindVisibleItems((float X, float Y) visibleArea)
1245 int MaxIndex = colView.InternalItemSource.Count - 1 - (hasFooter ? 1 : 0);
1246 int adds = spanSize * 2;
1248 (int start, int end) found = (0, 0);
1250 // Header is Showing
1251 if (hasHeader && visibleArea.X < headerSize + (IsHorizontal? Padding.Start : Padding.Top))
1260 foreach (GroupInfo gInfo in groups)
1264 if (gInfo.GroupPosition <= visibleArea.X &&
1265 gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.X)
1267 if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.X)
1269 found.start = gInfo.StartIndex - adds;
1272 //can be step in spanSize...
1273 for (int i = 1; i < gInfo.Count; i++)
1276 // Reach last index of group.
1277 if (i == (gInfo.Count - 1))
1279 found.start = gInfo.StartIndex + i - adds;
1284 else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.X - gInfo.GroupPosition - groupHeaderSize)
1286 found.start = gInfo.StartIndex + i - adds;
1293 //footer only shows?
1296 found.start = MaxIndex;
1301 float visibleAreaX = visibleArea.X - (hasHeader ? headerSize : 0);
1302 found.start = (Convert.ToInt32(Math.Abs(visibleAreaX / StepCandidate)) - 1) * spanSize;
1303 if (hasHeader) found.start += 1;
1305 if (found.start < 0) found.start = 0;
1308 if (hasFooter && visibleArea.Y > ScrollContentSize - footerSize - (IsHorizontal? Padding.End : Padding.Bottom))
1310 found.end = MaxIndex + 1;
1317 // can it be start from founded group...?
1318 //foreach(GroupInfo gInfo in groups.Skip(skipGroup))
1319 foreach (GroupInfo gInfo in groups)
1322 if (gInfo.GroupPosition <= visibleArea.Y &&
1323 gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.Y)
1325 if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.Y)
1327 found.end = gInfo.StartIndex + adds;
1330 //can be step in spanSize...
1331 for (int i = 1; i < gInfo.Count; i++)
1334 // Reach last index of group.
1335 if (i == (gInfo.Count - 1))
1337 found.end = gInfo.StartIndex + i + adds;
1341 else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.Y - gInfo.GroupPosition - groupHeaderSize)
1343 found.end = gInfo.StartIndex + i + adds;
1350 //footer only shows?
1353 found.start = MaxIndex;
1358 float visibleAreaY = visibleArea.Y - (hasHeader ? headerSize : 0);
1359 //Need to Consider GroupHeight!!!!
1360 found.end = (Convert.ToInt32(Math.Abs(visibleAreaY / StepCandidate)) + 1) * spanSize + adds;
1361 if (hasHeader) found.end += 1;
1363 if (found.end > (MaxIndex)) found.end = MaxIndex;
1368 internal override (float X, float Y) GetItemPosition(int index)
1371 int spaceStartX = Padding.Start;
1372 int spaceStartY = Padding.Top;
1373 int emptyArea = IsHorizontal?
1374 (int)(colView.Size.Height - Padding.Top - Padding.Bottom - (sizeCandidate.Height * spanSize)) :
1375 (int)(colView.Size.Width - Padding.Start - Padding.End - (sizeCandidate.Width * spanSize));
1377 if (hasHeader && index == 0)
1379 return (spaceStartX + headerMargin.Start, spaceStartY + headerMargin.Top);
1381 if (hasFooter && index == colView.InternalItemSource.Count - 1)
1383 xPos = IsHorizontal?
1384 ScrollContentSize - Padding.End - footerSize + footerMargin.Start:
1386 yPos = IsHorizontal?
1388 ScrollContentSize - Padding.Bottom - footerSize + footerMargin.Top;
1389 return (xPos, yPos);
1393 GroupInfo myGroup = GetGroupInfo(index);
1394 if (colView.InternalItemSource.IsGroupHeader(index))
1396 spaceStartX+= groupHeaderMargin.Start;
1397 spaceStartY+= groupHeaderMargin.Top;
1398 xPos = IsHorizontal?
1399 myGroup.GroupPosition + groupHeaderMargin.Start:
1401 yPos = IsHorizontal?
1403 myGroup.GroupPosition + groupHeaderMargin.Top;
1405 else if (colView.InternalItemSource.IsGroupFooter(index))
1407 spaceStartX+= groupFooterMargin.Start;
1408 spaceStartY+= groupFooterMargin.Top;
1409 xPos = IsHorizontal?
1410 myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize + groupFooterMargin.Start:
1412 yPos = IsHorizontal?
1414 myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize + groupFooterMargin.Top;
1418 int pureIndex = index - myGroup.StartIndex - 1;
1419 int division = pureIndex / spanSize;
1420 int remainder = pureIndex % spanSize;
1421 if (division < 0) division = 0;
1422 if (remainder < 0) remainder = 0;
1423 spaceStartX+= CandidateMargin.Start;
1424 spaceStartY+= CandidateMargin.Top;
1426 xPos = IsHorizontal?
1427 (division * sizeCandidate.Width) + myGroup.GroupPosition + groupHeaderSize + CandidateMargin.Start:
1428 (emptyArea * align) + (remainder * sizeCandidate.Width) + spaceStartX;
1429 yPos = IsHorizontal?
1430 (emptyArea * align) + (remainder * sizeCandidate.Height) + spaceStartY:
1431 (division * sizeCandidate.Height) + myGroup.GroupPosition + groupHeaderSize + CandidateMargin.Top;
1436 int pureIndex = index - (colView.Header ? 1 : 0);
1437 // int convert must be truncate value.
1438 int division = pureIndex / spanSize;
1439 int remainder = pureIndex % spanSize;
1440 if (division < 0) division = 0;
1441 if (remainder < 0) remainder = 0;
1442 spaceStartX+= CandidateMargin.Start;
1443 spaceStartY+= CandidateMargin.Top;
1445 xPos = IsHorizontal?
1446 (division * sizeCandidate.Width) + (hasHeader? headerSize : 0) + spaceStartX:
1447 (emptyArea * align) + (remainder * sizeCandidate.Width) + spaceStartX;
1448 yPos = IsHorizontal?
1449 (emptyArea * align) + (remainder * sizeCandidate.Height) + spaceStartY:
1450 (division * sizeCandidate.Height) + (hasHeader? headerSize : 0) + spaceStartY;
1453 return (xPos, yPos);
1456 internal override (float Width, float Height) GetItemSize(int index)
1458 return (sizeCandidate.Width - CandidateMargin.Start - CandidateMargin.End,
1459 sizeCandidate.Height - CandidateMargin.Top - CandidateMargin.Bottom);
1461 private void DelayedRequestLayout(float scrollPosition , bool force = true)
1463 if (requestLayoutTimer != null)
1465 requestLayoutTimer.Dispose();
1468 requestLayoutTimer = new Timer(1);
1469 requestLayoutTimer.Interval = 1;
1470 requestLayoutTimer.Tick += ((object target, Timer.TickEventArgs args) =>
1472 RequestLayout(scrollPosition, force);
1477 private RecyclerViewItem GetVisibleItem(int index)
1479 foreach (RecyclerViewItem item in VisibleItems)
1481 if (item.Index == index) return item;
1487 private GroupInfo GetGroupInfo(int index)
1489 if (Visited != null)
1491 if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index)
1494 if (hasHeader && index == 0) return null;
1495 foreach (GroupInfo group in groups)
1497 if (group.StartIndex <= index && group.StartIndex + group.Count > index)
1508 private object GetGroupParent(int index)
1510 if (Visited != null)
1512 if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index)
1513 return Visited.GroupParent;
1515 if (hasHeader && index == 0) return null;
1516 foreach (GroupInfo group in groups)
1518 if (group.StartIndex <= index && group.StartIndex + group.Count > index)
1521 return group.GroupParent;
1531 public object GroupParent;
1532 public int StartIndex;
1534 public float GroupSize;
1535 public float GroupPosition;
1536 //Items relative position from the GroupPosition