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 collectionView;
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;
47 private bool isSourceEmpty;
50 [EditorBrowsable(EditorBrowsableState.Never)]
51 protected new IGroupableItemSource Source => collectionView?.InternalSource;
56 [EditorBrowsable(EditorBrowsableState.Never)]
57 protected int SpanSize => spanSize;
62 [EditorBrowsable(EditorBrowsableState.Never)]
63 protected (float Width, float Height) SizeCandidate => sizeCandidate;
68 [EditorBrowsable(EditorBrowsableState.Never)]
69 protected override List<GroupInfo> GroupItems => groups;
72 /// Clean up ItemsLayouter.
74 /// <param name="view"> CollectionView of layouter. </param>
75 /// <remarks>please note that, view must be type of CollectionView</remarks>
76 /// <since_tizen> 9 </since_tizen>
77 public override void Initialize(RecyclerView view)
79 collectionView = view as CollectionView;
80 if (collectionView == null)
82 throw new ArgumentException("GridLayouter only can be applied CollectionView.", nameof(view));
86 foreach (RecyclerViewItem item in VisibleItems)
88 collectionView.UnrealizeItem(item, false);
95 IsHorizontal = (collectionView.ScrollingDirection == ScrollableBase.Direction.Horizontal);
97 RecyclerViewItem header = collectionView?.Header;
98 RecyclerViewItem footer = collectionView?.Footer;
100 int count = Source.Count;
101 int pureCount = count - (header? 1 : 0) - (footer? 1 : 0);
103 // 2. Get the header / footer and size deligated item and measure the size.
106 MeasureChild(collectionView, header);
108 width = header.Layout != null? header.Layout.MeasuredWidth.Size.AsRoundedValue() : 0;
109 height = header.Layout != null? header.Layout.MeasuredHeight.Size.AsRoundedValue() : 0;
111 Extents itemMargin = header.Margin;
112 headerSize = IsHorizontal?
113 width + itemMargin.Start + itemMargin.End:
114 height + itemMargin.Top + itemMargin.Bottom;
115 headerMargin = new Extents(itemMargin);
118 collectionView.UnrealizeItem(header);
123 MeasureChild(collectionView, footer);
125 width = footer.Layout != null? footer.Layout.MeasuredWidth.Size.AsRoundedValue() : 0;
126 height = footer.Layout != null? footer.Layout.MeasuredHeight.Size.AsRoundedValue() : 0;
128 Extents itemMargin = footer.Margin;
129 footerSize = IsHorizontal?
130 width + itemMargin.Start + itemMargin.End:
131 height + itemMargin.Top + itemMargin.Bottom;
132 footerMargin = new Extents(itemMargin);
133 footer.Index = count - 1;
136 collectionView.UnrealizeItem(footer);
141 isSourceEmpty = true;
142 base.Initialize(collectionView);
145 isSourceEmpty = false;
147 int firstIndex = header? 1 : 0;
149 if (collectionView.IsGrouped)
153 if (collectionView.GroupHeaderTemplate != null)
155 while (!Source.IsGroupHeader(firstIndex)) firstIndex++;
156 //must be always true
157 if (Source.IsGroupHeader(firstIndex))
159 RecyclerViewItem groupHeader = collectionView.RealizeItem(firstIndex);
162 if (groupHeader == null) throw new Exception("[" + firstIndex + "] Group Header failed to realize!");
164 // Need to Set proper height or width on scroll direction.
165 if (groupHeader.Layout == null)
167 width = groupHeader.WidthSpecification;
168 height = groupHeader.HeightSpecification;
172 MeasureChild(collectionView, groupHeader);
174 width = groupHeader.Layout.MeasuredWidth.Size.AsRoundedValue();
175 height = groupHeader.Layout.MeasuredHeight.Size.AsRoundedValue();
177 //Console.WriteLine("[NUI] GroupHeader Size {0} :{0}", width, height);
178 // pick the StepCandidate.
179 Extents itemMargin = groupHeader.Margin;
180 groupHeaderSize = IsHorizontal?
181 width + itemMargin.Start + itemMargin.End:
182 height + itemMargin.Top + itemMargin.Bottom;
183 groupHeaderMargin = new Extents(itemMargin);
184 collectionView.UnrealizeItem(groupHeader);
189 groupHeaderSize = 0F;
192 if (collectionView.GroupFooterTemplate != null)
194 int firstFooter = firstIndex;
195 while (!Source.IsGroupFooter(firstFooter)) firstFooter++;
196 //must be always true
197 if (Source.IsGroupFooter(firstFooter))
199 RecyclerViewItem groupFooter = collectionView.RealizeItem(firstFooter);
201 if (groupFooter == null) throw new Exception("[" + firstFooter + "] Group Footer failed to realize!");
202 // Need to Set proper height or width on scroll direction.
203 if (groupFooter.Layout == null)
205 width = groupFooter.WidthSpecification;
206 height = groupFooter.HeightSpecification;
210 MeasureChild(collectionView, groupFooter);
212 width = groupFooter.Layout.MeasuredWidth.Size.AsRoundedValue();
213 height = groupFooter.Layout.MeasuredHeight.Size.AsRoundedValue();
215 // pick the StepCandidate.
216 Extents itemMargin = groupFooter.Margin;
217 groupFooterSize = IsHorizontal?
218 width + itemMargin.Start + itemMargin.End:
219 height + itemMargin.Top + itemMargin.Bottom;
220 groupFooterMargin = new Extents(itemMargin);
222 collectionView.UnrealizeItem(groupFooter);
227 groupFooterSize = 0F;
230 else isGrouped = false;
233 //Final Check of FirstIndex
234 if ((Source.Count - 1 < firstIndex) ||
235 (Source.IsFooter(firstIndex) && (Source.Count - 1) == firstIndex))
242 Source.IsHeader(firstIndex) ||
243 Source.IsGroupHeader(firstIndex) ||
244 Source.IsGroupFooter(firstIndex))
246 if (Source.IsFooter(firstIndex)
247 || ((Source.Count - 1) <= firstIndex))
256 sizeCandidate = (0, 0);
259 // Get Size Deligate. FIXME if group exist index must be changed.
260 RecyclerViewItem sizeDeligate = collectionView.RealizeItem(firstIndex);
261 if (sizeDeligate == null)
263 throw new Exception("Cannot create content from DatTemplate.");
265 sizeDeligate.BindingContext = Source.GetItem(firstIndex);
267 // Need to Set proper height or width on scroll direction.
268 if (sizeDeligate.Layout == null)
270 width = sizeDeligate.WidthSpecification;
271 height = sizeDeligate.HeightSpecification;
275 MeasureChild(collectionView, sizeDeligate);
277 width = sizeDeligate.Layout.MeasuredWidth.Size.AsRoundedValue();
278 height = sizeDeligate.Layout.MeasuredHeight.Size.AsRoundedValue();
280 //Console.WriteLine("[NUI] item Size {0} :{1}", width, height);
282 // pick the StepCandidate.
283 Extents itemMargin = sizeDeligate.Margin;
284 width = width + itemMargin.Start + itemMargin.End;
285 height = height + itemMargin.Top + itemMargin.Bottom;
286 StepCandidate = IsHorizontal? width : height;
287 CandidateMargin = new Extents(itemMargin);
289 // Prevent zero division.
290 if (width == 0) width = 1;
291 if (height == 0) height = 1;
292 spanSize = IsHorizontal?
293 Convert.ToInt32(Math.Truncate((double)((collectionView.Size.Height - Padding.Top - Padding.Bottom) / height))) :
294 Convert.ToInt32(Math.Truncate((double)((collectionView.Size.Width - Padding.Start - Padding.End) / width)));
296 sizeCandidate = (width, height);
298 collectionView.UnrealizeItem(sizeDeligate);
301 if (StepCandidate < 1) StepCandidate = 1;
302 if (spanSize < 1) spanSize = 1;
306 float Current = 0.0F;
307 IGroupableItemSource source = Source;
308 GroupInfo currentGroup = null;
309 object currentParent = null;
311 for (int i = 0; i < count; i++)
313 if (i == 0 && hasHeader)
315 Current += headerSize;
317 else if (i == count - 1 && hasFooter)
319 Current += footerSize;
323 if (source.GetGroupParent(i) != currentParent)
325 currentParent = source.GetGroupParent(i);
326 float currentSize = (source.IsGroupHeader(i)? groupHeaderSize :
327 (source.IsGroupFooter(i)? groupFooterSize: StepCandidate));
328 currentGroup = new GroupInfo()
330 GroupParent = source.GetGroupParent(i),
333 GroupSize = currentSize,
334 GroupPosition = Current
336 groups.Add(currentGroup);
337 Current += currentSize;
340 else if (source.IsGroupFooter(i))
342 //currentGroup.hasFooter = true;
343 if (currentGroup != null)
345 currentGroup.Count++;
346 currentGroup.GroupSize += groupFooterSize;
347 Current += groupFooterSize;
352 if (currentGroup != null)
354 currentGroup.Count++;
355 int index = i - currentGroup.StartIndex - ((collectionView.GroupHeaderTemplate != null) ? 1 : 0);
356 if ((index % spanSize) == 0)
358 currentGroup.GroupSize += StepCandidate;
359 Current += StepCandidate;
365 ScrollContentSize = Current;
369 // 3. Measure the scroller content size.
370 ScrollContentSize = StepCandidate * Convert.ToInt32(Math.Ceiling((double)pureCount / (double)spanSize));
371 if (hasHeader) ScrollContentSize += headerSize;
372 if (hasFooter) ScrollContentSize += footerSize;
375 ScrollContentSize = IsHorizontal?
376 ScrollContentSize + Padding.Start + Padding.End:
377 ScrollContentSize + Padding.Top + Padding.Bottom;
379 if (IsHorizontal) collectionView.ContentContainer.SizeWidth = ScrollContentSize;
380 else collectionView.ContentContainer.SizeHeight = ScrollContentSize;
382 base.Initialize(collectionView);
383 //Console.WriteLine("Init Done, StepCnadidate{0}, spanSize{1}, Scroll{2}", StepCandidate, spanSize, ScrollContentSize);
387 /// This is called to find out where items are lain out according to current scroll position.
389 /// <param name="scrollPosition">Scroll position which is calculated by ScrollableBase</param>
390 /// <param name="force">boolean force flag to layouting forcely.</param>
391 /// <since_tizen> 9 </since_tizen>
392 public override void RequestLayout(float scrollPosition, bool force = false)
394 // Layouting is only possible after once it intialized.
395 if (!IsInitialized) return;
396 int LastIndex = Source.Count;
398 if (!force && PrevScrollPosition == Math.Abs(scrollPosition)) return;
399 PrevScrollPosition = Math.Abs(scrollPosition);
401 int prevFirstVisible = FirstVisible;
402 int prevLastVisible = LastVisible;
403 bool IsHorizontal = (collectionView.ScrollingDirection == ScrollableBase.Direction.Horizontal);
405 (float X, float Y) visibleArea = (PrevScrollPosition,
406 PrevScrollPosition + (IsHorizontal? collectionView.Size.Width : collectionView.Size.Height)
409 //Console.WriteLine("[NUI] itemsView [{0},{1}] [{2},{3}]", collectionView.Size.Width, collectionView.Size.Height, collectionView.ContentContainer.Size.Width, collectionView.ContentContainer.Size.Height);
411 // 1. Set First/Last Visible Item Index.
412 (int start, int end) = FindVisibleItems(visibleArea);
413 FirstVisible = start;
416 //Console.WriteLine("[NUI] {0} :visibleArea before [{1},{2}] after [{3},{4}]", scrollPosition, prevFirstVisible, prevLastVisible, FirstVisible, LastVisible);
418 // 2. Unrealize invisible items.
419 List<RecyclerViewItem> unrealizedItems = new List<RecyclerViewItem>();
420 foreach (RecyclerViewItem item in VisibleItems)
422 if (item.Index < FirstVisible || item.Index > LastVisible)
424 //Console.WriteLine("[NUI] Unrealize{0}!", item.Index);
425 unrealizedItems.Add(item);
426 collectionView.UnrealizeItem(item);
429 VisibleItems.RemoveAll(unrealizedItems.Contains);
431 //Console.WriteLine("Realize Begin [{0} to {1}]", FirstVisible, LastVisible);
432 // 3. Realize and placing visible items.
433 for (int i = FirstVisible; i <= LastVisible; i++)
435 //Console.WriteLine("[NUI] Realize!");
436 RecyclerViewItem item = null;
437 // 4. Get item if visible or realize new.
438 if (i >= prevFirstVisible && i <= prevLastVisible)
440 item = GetVisibleItem(i);
441 if (item != null && !force) continue;
445 item = collectionView.RealizeItem(i);
446 if (item != null) VisibleItems.Add(item);
447 else throw new Exception("Failed to create RecycerViewItem index of ["+ i + "]");
450 //item Position without Padding and Margin.
451 (float x, float y) = GetItemPosition(i);
452 // 5. Placing item with Padding and Margin.
453 item.Position = new Position(x, y);
455 //Linear Item need to be resized!
456 if (item.IsHeader || item.IsFooter || item.IsGroupHeader || item.IsGroupFooter)
458 var size = (IsHorizontal? item.SizeWidth: item.SizeHeight);
459 if (collectionView.SizingStrategy == ItemSizingStrategy.MeasureFirst)
461 if (item.IsHeader) size = headerSize;
462 else if (item.IsFooter) size = footerSize;
463 else if (item.IsGroupHeader) size = groupHeaderSize;
464 else if (item.IsGroupFooter) size = groupFooterSize;
466 if (IsHorizontal && item.HeightSpecification == LayoutParamPolicies.MatchParent)
468 item.Size = new Size(size, Container.Size.Height - Padding.Top - Padding.Bottom - item.Margin.Top - item.Margin.Bottom);
470 else if (!IsHorizontal && item.WidthSpecification == LayoutParamPolicies.MatchParent)
472 item.Size = new Size(Container.Size.Width - Padding.Start - Padding.End - item.Margin.Start - item.Margin.End, size);
475 //Console.WriteLine("[NUI] ["+item.Index+"] ["+item.Position.X+", "+item.Position.Y+" ==== \n");
477 //Console.WriteLine("Realize Done");
481 /// Clear the current screen and all properties.
483 [EditorBrowsable(EditorBrowsableState.Never)]
484 public override void Clear()
487 if (requestLayoutTimer != null)
489 requestLayoutTimer.Dispose();
494 foreach (GroupInfo group in groups)
496 //group.ItemPosition?.Clear();
503 if (headerMargin != null)
505 headerMargin.Dispose();
508 if (footerMargin != null)
510 footerMargin.Dispose();
513 if (groupHeaderMargin != null)
515 groupHeaderMargin.Dispose();
516 groupHeaderMargin = null;
518 if (groupFooterMargin != null)
520 groupFooterMargin.Dispose();
521 groupFooterMargin = null;
528 public override void NotifyItemSizeChanged(RecyclerViewItem item)
530 // All Item size need to be same in grid!
531 // if you want to change item size, change dataTemplate to re-initing.
536 [EditorBrowsable(EditorBrowsableState.Never)]
537 public override void NotifyItemInserted(IItemSource source, int startIndex)
539 // Insert Single item.
540 if (source == null) throw new ArgumentNullException(nameof(source));
541 if (collectionView == null) return;
542 if (isSourceEmpty || StepCandidate <= 1)
544 Initialize(collectionView);
547 // Will be null if not a group.
548 float currentSize = 0;
549 IGroupableItemSource gSource = source as IGroupableItemSource;
551 // Get the first Visible Position to adjust.
553 int topInScreenIndex = 0;
555 (topInScreenIndex, offset) = FindTopItemInScreen();
558 //2. Handle Group Case.
559 if (isGrouped && gSource != null)
561 GroupInfo groupInfo = null;
562 object groupParent = gSource.GetGroupParent(startIndex);
563 int parentIndex = gSource.GetPosition(groupParent);
564 if (gSource.HasHeader) parentIndex--;
566 // Check item is group parent or not
567 // if group parent, add new gorupinfo
568 if (gSource.IsHeader(startIndex))
570 // This is childless group.
571 // create new groupInfo!
572 groupInfo = new GroupInfo()
574 GroupParent = groupParent,
575 StartIndex = startIndex,
577 GroupSize = groupHeaderSize,
580 if (parentIndex >= groups.Count)
582 groupInfo.GroupPosition = ScrollContentSize;
583 groups.Add(groupInfo);
587 groupInfo.GroupPosition = groups[parentIndex].GroupPosition;
588 groups.Insert(parentIndex, groupInfo);
591 currentSize = groupHeaderSize;
595 // If not group parent, add item into the groupinfo.
596 if (parentIndex >= groups.Count) throw new Exception("group parent is bigger than group counts.");
597 groupInfo = groups[parentIndex];//GetGroupInfo(groupParent);
598 if (groupInfo == null) throw new Exception("Cannot find group information!");
600 if (gSource.IsGroupFooter(startIndex))
602 // It doesn't make sence to adding footer by notify...
603 // if GroupFooterTemplate is added,
604 // need to implement on here.
608 if (collectionView.SizingStrategy == ItemSizingStrategy.MeasureAll)
610 // Wrong! Grid Layouter do not support MeasureAll!
614 int pureCount = groupInfo.Count - 1 - (collectionView.GroupFooterTemplate == null? 0: 1);
615 if (pureCount % spanSize == 0)
617 currentSize = StepCandidate;
618 groupInfo.GroupSize += currentSize;
627 if (parentIndex + 1 < groups.Count)
629 for(int i = parentIndex + 1; i < groups.Count; i++)
631 groups[i].GroupPosition += currentSize;
632 groups[i].StartIndex++;
638 if (collectionView.SizingStrategy == ItemSizingStrategy.MeasureAll)
640 // Wrong! Grid Layouter do not support MeasureAll!
642 int pureCount = Source.Count - (hasHeader? 1: 0) - (hasFooter? 1: 0);
644 // Count comes after updated in ungrouped case!
645 if (pureCount % spanSize == 1)
647 currentSize = StepCandidate;
651 // 3. Update Scroll Content Size
652 ScrollContentSize += currentSize;
654 if (IsHorizontal) collectionView.ContentContainer.SizeWidth = ScrollContentSize;
655 else collectionView.ContentContainer.SizeHeight = ScrollContentSize;
657 // 4. Update Visible Items.
658 foreach (RecyclerViewItem item in VisibleItems)
660 if (item.Index >= startIndex)
666 float scrollPosition = PrevScrollPosition;
670 // Insertion above Top Visible!
671 if (startIndex <= topInScreenIndex)
673 scrollPosition = GetItemPosition(topInScreenIndex);
674 scrollPosition -= offset;
676 collectionView.ScrollTo(scrollPosition);
680 // Update Viewport in delay.
681 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
682 // but currently we do not have any accessor to pre-calculation so instead of this,
683 // using Timer temporarily.
684 DelayedRequestLayout(scrollPosition);
688 [EditorBrowsable(EditorBrowsableState.Never)]
689 public override void NotifyItemRangeInserted(IItemSource source, int startIndex, int count)
692 if (source == null) throw new ArgumentNullException(nameof(source));
693 if (collectionView == null) return;
694 if (isSourceEmpty || StepCandidate <= 1)
696 Initialize(collectionView);
699 float currentSize = StepCandidate;
700 // Will be null if not a group.
701 IGroupableItemSource gSource = source as IGroupableItemSource;
703 // Get the first Visible Position to adjust.
705 int topInScreenIndex = 0;
707 (topInScreenIndex, offset) = FindTopItemInScreen();
710 // 2. Handle Group Case
711 // Adding ranged items should all same new groups.
712 if (isGrouped && gSource != null)
714 GroupInfo groupInfo = null;
715 object groupParent = gSource.GetGroupParent(startIndex);
716 int parentIndex = gSource.GetPosition(groupParent);
717 if (gSource.HasHeader) parentIndex--;
719 // We guess here that range inserted from GroupStartIndex.
720 int groupStartIndex = startIndex;
722 for (int current = startIndex; current - startIndex < count; current++)
724 // Check item is group parent or not
725 // if group parent, add new gorupinfo
726 if (groupStartIndex == current)
728 //create new groupInfo!
729 currentSize = (gSource.IsGroupHeader(current)? groupHeaderSize :
730 (gSource.IsGroupFooter(current)? groupFooterSize: currentSize));
731 groupInfo = new GroupInfo()
733 GroupParent = groupParent,
734 StartIndex = current,
736 GroupSize = StepCandidate,
738 currentSize += StepCandidate;
743 //if not group parent, add item into the groupinfo.
744 //groupInfo = GetGroupInfo(groupStartIndex);
745 if (groupInfo == null) throw new Exception("Cannot find group information!");
748 if (gSource.IsGroupFooter(current))
750 groupInfo.GroupSize += groupFooterSize;
751 currentSize += groupFooterSize;
755 if (collectionView.SizingStrategy == ItemSizingStrategy.MeasureAll)
757 // Wrong! Grid Layouter do not support MeasureAll!
761 int index = current - groupStartIndex - ((collectionView.GroupHeaderTemplate != null)? 1: 0);
762 if ((index % spanSize) == 0)
764 groupInfo.GroupSize += StepCandidate;
765 currentSize += StepCandidate;
772 if (parentIndex >= groups.Count)
774 groupInfo.GroupPosition = ScrollContentSize;
775 groups.Add(groupInfo);
779 groupInfo.GroupPosition = groups[parentIndex].GroupPosition;
780 groups.Insert(parentIndex, groupInfo);
783 // Update other below group's position
784 if (parentIndex + 1 < groups.Count)
786 for(int i = parentIndex + 1; i < groups.Count; i++)
788 groups[i].GroupPosition += currentSize;
789 groups[i].StartIndex += count;
793 ScrollContentSize += currentSize;
797 throw new Exception("Cannot insert ungrouped range items!");
800 // 3. Update Scroll Content Size
801 if (IsHorizontal) collectionView.ContentContainer.SizeWidth = ScrollContentSize;
802 else collectionView.ContentContainer.SizeHeight = ScrollContentSize;
804 // 4. Update Visible Items.
805 foreach (RecyclerViewItem item in VisibleItems)
807 if (item.Index >= startIndex)
814 float scrollPosition = PrevScrollPosition;
816 // Insertion above Top Visible!
817 if (startIndex + count <= topInScreenIndex)
819 scrollPosition = GetItemPosition(topInScreenIndex);
820 scrollPosition -= offset;
822 collectionView.ScrollTo(scrollPosition);
826 // Update Viewport in delay.
827 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
828 // but currently we do not have any accessor to pre-calculation so instead of this,
829 // using Timer temporarily.
830 DelayedRequestLayout(scrollPosition);
834 [EditorBrowsable(EditorBrowsableState.Never)]
835 public override void NotifyItemRemoved(IItemSource source, int startIndex)
838 if (source == null) throw new ArgumentNullException(nameof(source));
839 if (collectionView == null) return;
841 // Will be null if not a group.
842 float currentSize = 0;
843 IGroupableItemSource gSource = source as IGroupableItemSource;
845 // Get the first Visible Position to adjust.
847 int topInScreenIndex = 0;
849 (topInScreenIndex, offset) = FindTopItemInScreen();
852 // 2. Handle Group Case
853 if (isGrouped && gSource != null)
856 GroupInfo groupInfo = null;
857 foreach(GroupInfo cur in groups)
859 if ((cur.StartIndex <= startIndex) && (cur.StartIndex + cur.Count - 1 >= startIndex))
866 if (groupInfo == null) throw new Exception("Cannot find group information!");
867 // Check item is group parent or not
868 // if group parent, add new gorupinfo
869 if (groupInfo.StartIndex == startIndex)
871 // This is empty group!
872 // check group is empty.
873 if (groupInfo.Count != 1)
875 throw new Exception("Cannot remove group parent");
877 currentSize = groupInfo.GroupSize;
880 // groupInfo.Dispose();
881 groups.Remove(groupInfo);
888 // Skip footer case as footer cannot exist alone without header.
889 if (collectionView.SizingStrategy == ItemSizingStrategy.MeasureAll)
891 // Wrong! Grid Layouter do not support MeasureAll!
895 int pureCount = groupInfo.Count - 1 - (collectionView.GroupFooterTemplate == null? 0: 1);
896 if (pureCount % spanSize == 0)
898 currentSize = StepCandidate;
899 groupInfo.GroupSize -= currentSize;
904 for (int i = parentIndex + 1; i < groups.Count; i++)
906 groups[i].GroupPosition -= currentSize;
907 groups[i].StartIndex--;
912 if (collectionView.SizingStrategy == ItemSizingStrategy.MeasureAll)
914 // Wrong! Grid Layouter do not support MeasureAll!
916 int pureCount = Source.Count - (hasHeader? 1: 0) - (hasFooter? 1: 0);
918 // Count comes after updated in ungrouped case!
919 if (pureCount % spanSize == 0)
921 currentSize = StepCandidate;
926 ScrollContentSize -= currentSize;
928 // 3. Update Scroll Content Size
929 if (IsHorizontal) collectionView.ContentContainer.SizeWidth = ScrollContentSize;
930 else collectionView.ContentContainer.SizeHeight = ScrollContentSize;
932 // 4. Update Visible Items.
933 RecyclerViewItem targetItem = null;
934 foreach (RecyclerViewItem item in VisibleItems)
936 if (item.Index == startIndex)
939 collectionView.UnrealizeItem(item);
941 else if (item.Index > startIndex)
946 VisibleItems.Remove(targetItem);
949 float scrollPosition = PrevScrollPosition;
951 // Insertion above Top Visible!
952 if (startIndex <= topInScreenIndex)
954 scrollPosition = GetItemPosition(topInScreenIndex);
955 scrollPosition -= offset;
957 collectionView.ScrollTo(scrollPosition);
961 // Update Viewport in delay.
962 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
963 // but currently we do not have any accessor to pre-calculation so instead of this,
964 // using Timer temporarily.
965 DelayedRequestLayout(scrollPosition);
969 [EditorBrowsable(EditorBrowsableState.Never)]
970 public override void NotifyItemRangeRemoved(IItemSource source, int startIndex, int count)
973 if (source == null) throw new ArgumentNullException(nameof(source));
974 if (collectionView == null) return;
976 // Will be null if not a group.
977 float currentSize = StepCandidate;
978 IGroupableItemSource gSource = source as IGroupableItemSource;
980 // Get the first Visible Position to adjust.
982 int topInScreenIndex = 0;
984 (topInScreenIndex, offset) = FindTopItemInScreen();
987 // 1. Handle Group Case
988 if (isGrouped && gSource != null)
991 GroupInfo groupInfo = null;
992 foreach(GroupInfo cur in groups)
994 if ((cur.StartIndex == startIndex) && (cur.Count == count))
1001 if (groupInfo == null) throw new Exception("Cannot find group information!");
1002 // Check item is group parent or not
1003 // if group parent, add new gorupinfo
1004 currentSize = groupInfo.GroupSize;
1005 if (collectionView.SizingStrategy == ItemSizingStrategy.MeasureAll)
1007 // Wrong! Grid Layouter do not support MeasureAll!
1010 // groupInfo.Dispose();
1011 groups.Remove(groupInfo);
1013 for (int i = parentIndex; i < groups.Count; i++)
1015 groups[i].GroupPosition -= currentSize;
1016 groups[i].StartIndex -= count;
1021 // It must group case! throw exception!
1022 throw new Exception("Range remove must group remove!");
1025 ScrollContentSize -= currentSize;
1027 // 2. Update Scroll Content Size
1028 if (IsHorizontal) collectionView.ContentContainer.SizeWidth = ScrollContentSize;
1029 else collectionView.ContentContainer.SizeHeight = ScrollContentSize;
1031 // 3. Update Visible Items.
1032 List<RecyclerViewItem> unrealizedItems = new List<RecyclerViewItem>();
1033 foreach (RecyclerViewItem item in VisibleItems)
1035 if ((item.Index >= startIndex)
1036 && (item.Index < startIndex + count))
1038 unrealizedItems.Add(item);
1039 collectionView.UnrealizeItem(item);
1041 else if (item.Index >= startIndex + count)
1043 item.Index -= count;
1046 VisibleItems.RemoveAll(unrealizedItems.Contains);
1047 unrealizedItems.Clear();
1050 float scrollPosition = PrevScrollPosition;
1052 // Insertion above Top Visible!
1053 if (startIndex <= topInScreenIndex)
1055 scrollPosition = GetItemPosition(topInScreenIndex);
1056 scrollPosition -= offset;
1058 collectionView.ScrollTo(scrollPosition);
1062 // Update Viewport in delay.
1063 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
1064 // but currently we do not have any accessor to pre-calculation so instead of this,
1065 // using Timer temporarily.
1066 DelayedRequestLayout(scrollPosition);
1070 [EditorBrowsable(EditorBrowsableState.Never)]
1071 public override void NotifyItemMoved(IItemSource source, int fromPosition, int toPosition)
1074 if (source == null) throw new ArgumentNullException(nameof(source));
1075 if (collectionView == null) return;
1077 // Will be null if not a group.
1078 float currentSize = StepCandidate;
1079 int diff = toPosition - fromPosition;
1081 // Get the first Visible Position to adjust.
1083 int topInScreenIndex = 0;
1085 (topInScreenIndex, offset) = FindTopItemInScreen();
1088 // Move can only happen in it's own groups.
1089 // so there will be no changes in position, startIndex in ohter groups.
1090 // check visible item and update indexs.
1091 int startIndex = ( diff > 0 ? fromPosition: toPosition);
1092 int endIndex = (diff > 0 ? toPosition: fromPosition);
1094 if ((endIndex >= FirstVisible) && (startIndex <= LastVisible))
1096 foreach (RecyclerViewItem item in VisibleItems)
1098 if ((item.Index >= startIndex)
1099 && (item.Index <= endIndex))
1101 if (item.Index == fromPosition) item.Index = toPosition;
1104 if (diff > 0) item.Index--;
1111 if (IsHorizontal) collectionView.ContentContainer.SizeWidth = ScrollContentSize;
1112 else collectionView.ContentContainer.SizeHeight = ScrollContentSize;
1115 float scrollPosition = PrevScrollPosition;
1117 // Insertion above Top Visible!
1118 if (((fromPosition > topInScreenIndex) && (toPosition < topInScreenIndex) ||
1119 ((fromPosition < topInScreenIndex) && (toPosition > topInScreenIndex)))
1121 scrollPosition = GetItemPosition(topInScreenIndex);
1122 scrollPosition -= offset;
1124 collectionView.ScrollTo(scrollPosition);
1128 // Update Viewport in delay.
1129 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
1130 // but currently we do not have any accessor to pre-calculation so instead of this,
1131 // using Timer temporarily.
1132 DelayedRequestLayout(scrollPosition);
1136 [EditorBrowsable(EditorBrowsableState.Never)]
1137 public override void NotifyItemRangeMoved(IItemSource source, int fromPosition, int toPosition, int count)
1140 if (source == null) throw new ArgumentNullException(nameof(source));
1141 if (collectionView == null) return;
1143 // Will be null if not a group.
1144 float currentSize = StepCandidate;
1145 int diff = toPosition - fromPosition;
1147 int startIndex = ( diff > 0 ? fromPosition: toPosition);
1148 int endIndex = (diff > 0 ? toPosition + count - 1: fromPosition + count - 1);
1150 // 2. Handle Group Case
1153 int fromParentIndex = 0;
1154 int toParentIndex = 0;
1155 bool findFrom = false;
1156 bool findTo = false;
1157 GroupInfo fromGroup = null;
1158 GroupInfo toGroup = null;
1160 foreach(GroupInfo cur in groups)
1162 if ((cur.StartIndex == fromPosition) && (cur.Count == count))
1166 if (findFrom && findTo) break;
1168 else if (cur.StartIndex == toPosition)
1172 if (findFrom && findTo) break;
1174 if (!findFrom) fromParentIndex++;
1175 if (!findTo) toParentIndex++;
1177 if (toGroup == null || fromGroup == null) throw new Exception("Cannot find group information!");
1179 fromGroup.StartIndex = toGroup.StartIndex;
1180 fromGroup.GroupPosition = toGroup.GroupPosition;
1182 endIndex = (diff > 0 ? toPosition + toGroup.Count - 1: fromPosition + count - 1);
1184 groups.Remove(fromGroup);
1185 groups.Insert(toParentIndex, fromGroup);
1187 int startGroup = (diff > 0? fromParentIndex: toParentIndex);
1188 int endGroup = (diff > 0? toParentIndex: fromParentIndex);
1190 for (int i = startGroup; i <= endGroup; i++)
1192 if (i == toParentIndex) continue;
1193 float prevPos = groups[i].GroupPosition;
1194 int prevIdx = groups[i].StartIndex;
1195 groups[i].GroupPosition = groups[i].GroupPosition + (diff > 0? -1 : 1) * fromGroup.GroupSize;
1196 groups[i].StartIndex = groups[i].StartIndex + (diff > 0? -1 : 1) * fromGroup.Count;
1201 //It must group case! throw exception!
1202 throw new Exception("Range remove must group remove!");
1205 // Move can only happen in it's own groups.
1206 // so there will be no changes in position, startIndex in ohter groups.
1207 // check visible item and update indexs.
1208 if ((endIndex >= FirstVisible) && (startIndex <= LastVisible))
1210 foreach (RecyclerViewItem item in VisibleItems)
1212 if ((item.Index >= startIndex)
1213 && (item.Index <= endIndex))
1215 if ((item.Index >= fromPosition) && (item.Index < fromPosition + count))
1217 item.Index = fromPosition - item.Index + toPosition;
1221 if (diff > 0) item.Index -= count;
1222 else item.Index += count;
1229 float scrollPosition = PrevScrollPosition;
1231 // Insertion above Top Visible!
1232 if (((fromPosition > topInScreenIndex) && (toPosition < topInScreenIndex) ||
1233 ((fromPosition < topInScreenIndex) && (toPosition > topInScreenIndex)))
1235 scrollPosition = GetItemPosition(topInScreenIndex);
1236 scrollPosition -= offset;
1238 collectionView.ScrollTo(scrollPosition);
1242 // Update Viewport in delay.
1243 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
1244 // but currently we do not have any accessor to pre-calculation so instead of this,
1245 // using Timer temporarily.
1246 DelayedRequestLayout(scrollPosition);
1250 [EditorBrowsable(EditorBrowsableState.Never)]
1251 public override float CalculateLayoutOrientationSize()
1253 //Console.WriteLine("[NUI] Calculate Layout ScrollContentSize {0}", ScrollContentSize);
1254 return ScrollContentSize;
1258 [EditorBrowsable(EditorBrowsableState.Never)]
1259 public override float CalculateCandidateScrollPosition(float scrollPosition)
1261 //Console.WriteLine("[NUI] Calculate Candidate ScrollContentSize {0}", ScrollContentSize);
1262 return scrollPosition;
1266 [EditorBrowsable(EditorBrowsableState.Never)]
1267 public override View RequestNextFocusableView(View currentFocusedView, View.FocusDirection direction, bool loopEnabled)
1269 if (currentFocusedView == null)
1270 throw new ArgumentNullException(nameof(currentFocusedView));
1272 View nextFocusedView = null;
1273 int targetSibling = -1;
1274 bool IsHorizontal = collectionView.ScrollingDirection == ScrollableBase.Direction.Horizontal;
1278 case View.FocusDirection.Left:
1280 targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder - 1 : targetSibling;
1283 case View.FocusDirection.Right:
1285 targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder + 1 : targetSibling;
1288 case View.FocusDirection.Up:
1290 targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder - 1;
1293 case View.FocusDirection.Down:
1295 targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder + 1;
1300 if (targetSibling > -1 && targetSibling < Container.Children.Count)
1302 RecyclerViewItem candidate = Container.Children[targetSibling] as RecyclerViewItem;
1303 if (candidate != null && candidate.Index >= 0 && candidate.Index < Source.Count)
1305 nextFocusedView = candidate;
1308 return nextFocusedView;
1312 [EditorBrowsable(EditorBrowsableState.Never)]
1313 protected override (int start, int end) FindVisibleItems((float X, float Y) visibleArea)
1315 int MaxIndex = Source.Count - 1 - (hasFooter ? 1 : 0);
1316 int adds = spanSize * 2;
1318 (int start, int end) found = (0, 0);
1320 // Header is Showing
1321 if (hasHeader && visibleArea.X < headerSize + (IsHorizontal? Padding.Start : Padding.Top))
1330 foreach (GroupInfo gInfo in groups)
1334 if (gInfo.GroupPosition <= visibleArea.X &&
1335 gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.X)
1337 if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.X)
1339 found.start = gInfo.StartIndex - adds;
1343 //can be step in spanSize...
1344 for (int i = 1; i < gInfo.Count; i++)
1347 // Reach last index of group.
1348 if (i == (gInfo.Count - 1))
1350 found.start = gInfo.StartIndex + i - adds;
1355 else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.X - gInfo.GroupPosition - groupHeaderSize)
1357 found.start = gInfo.StartIndex + i - adds;
1365 //footer only shows?
1368 found.start = MaxIndex;
1373 float visibleAreaX = visibleArea.X - (hasHeader ? headerSize : 0);
1374 // Prevent zero division.
1375 var itemSize = (StepCandidate != 0)? StepCandidate: 1f;
1376 found.start = (Convert.ToInt32(Math.Abs(visibleAreaX / itemSize)) - 1) * spanSize;
1377 if (hasHeader) found.start += 1;
1379 if (found.start < 0) found.start = 0;
1382 if (hasFooter && visibleArea.Y > ScrollContentSize - footerSize - (IsHorizontal? Padding.End : Padding.Bottom))
1384 found.end = MaxIndex + 1;
1391 // can it be start from founded group...?
1392 //foreach(GroupInfo gInfo in groups.Skip(skipGroup))
1393 foreach (GroupInfo gInfo in groups)
1396 if (gInfo.GroupPosition <= visibleArea.Y &&
1397 gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.Y)
1399 if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.Y)
1401 found.end = gInfo.StartIndex + adds;
1405 //can be step in spanSize...
1406 for (int i = 1; i < gInfo.Count; i++)
1409 // Reach last index of group.
1410 if (i == (gInfo.Count - 1))
1412 found.end = gInfo.StartIndex + i + adds;
1416 else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.Y - gInfo.GroupPosition - groupHeaderSize)
1418 found.end = gInfo.StartIndex + i + adds;
1426 //footer only shows?
1429 found.end = MaxIndex;
1434 float visibleAreaY = visibleArea.Y - (hasHeader ? headerSize : 0);
1435 //Need to Consider GroupHeight!!!!
1436 // Prevent zero division.
1437 var itemSize = (StepCandidate != 0)? StepCandidate: 1f;
1438 found.end = (Convert.ToInt32(Math.Abs(visibleAreaY / itemSize)) + 1) * spanSize + adds;
1439 if (hasHeader) found.end += 1;
1441 if (found.end > (MaxIndex)) found.end = MaxIndex;
1447 [EditorBrowsable(EditorBrowsableState.Never)]
1448 protected internal override (float X, float Y) GetItemPosition(int index)
1451 int spaceStartX = Padding.Start;
1452 int spaceStartY = Padding.Top;
1453 int emptyArea = IsHorizontal?
1454 (int)(collectionView.Size.Height - Padding.Top - Padding.Bottom - (sizeCandidate.Height * spanSize)) :
1455 (int)(collectionView.Size.Width - Padding.Start - Padding.End - (sizeCandidate.Width * spanSize));
1457 if (hasHeader && index == 0)
1459 return (spaceStartX + headerMargin.Start, spaceStartY + headerMargin.Top);
1461 if (hasFooter && index == Source.Count - 1)
1463 xPos = IsHorizontal?
1464 ScrollContentSize - Padding.End - footerSize + footerMargin.Start:
1466 yPos = IsHorizontal?
1468 ScrollContentSize - Padding.Bottom - footerSize + footerMargin.Top;
1469 return (xPos, yPos);
1472 GroupInfo myGroup = GetGroupInfo(index);
1473 if (isGrouped && null != myGroup)
1475 if (Source.IsGroupHeader(index))
1477 spaceStartX+= groupHeaderMargin.Start;
1478 spaceStartY+= groupHeaderMargin.Top;
1479 xPos = IsHorizontal?
1480 myGroup.GroupPosition + groupHeaderMargin.Start:
1482 yPos = IsHorizontal?
1484 myGroup.GroupPosition + groupHeaderMargin.Top;
1486 else if (Source.IsGroupFooter(index))
1488 spaceStartX+= groupFooterMargin.Start;
1489 spaceStartY+= groupFooterMargin.Top;
1490 xPos = IsHorizontal?
1491 myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize + groupFooterMargin.Start:
1493 yPos = IsHorizontal?
1495 myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize + groupFooterMargin.Top;
1499 int pureIndex = index - myGroup.StartIndex - ((collectionView.GroupHeaderTemplate != null)? 1: 0);
1500 int division = pureIndex / spanSize;
1501 int remainder = pureIndex % spanSize;
1502 if (division < 0) division = 0;
1503 if (remainder < 0) remainder = 0;
1504 spaceStartX+= CandidateMargin.Start;
1505 spaceStartY+= CandidateMargin.Top;
1507 xPos = IsHorizontal?
1508 (division * sizeCandidate.Width) + myGroup.GroupPosition + groupHeaderSize + CandidateMargin.Start:
1509 (emptyArea * align) + (remainder * sizeCandidate.Width) + spaceStartX;
1510 yPos = IsHorizontal?
1511 (emptyArea * align) + (remainder * sizeCandidate.Height) + spaceStartY:
1512 (division * sizeCandidate.Height) + myGroup.GroupPosition + groupHeaderSize + CandidateMargin.Top;
1517 int pureIndex = index - (collectionView.Header ? 1 : 0);
1518 // int convert must be truncate value.
1519 int division = pureIndex / spanSize;
1520 int remainder = pureIndex % spanSize;
1521 if (division < 0) division = 0;
1522 if (remainder < 0) remainder = 0;
1523 spaceStartX+= CandidateMargin.Start;
1524 spaceStartY+= CandidateMargin.Top;
1526 xPos = IsHorizontal?
1527 (division * sizeCandidate.Width) + (hasHeader? headerSize : 0) + spaceStartX:
1528 (emptyArea * align) + (remainder * sizeCandidate.Width) + spaceStartX;
1529 yPos = IsHorizontal?
1530 (emptyArea * align) + (remainder * sizeCandidate.Height) + spaceStartY:
1531 (division * sizeCandidate.Height) + (hasHeader? headerSize : 0) + spaceStartY;
1534 return (xPos, yPos);
1538 [EditorBrowsable(EditorBrowsableState.Never)]
1539 protected internal override (float Width, float Height) GetItemSize(int index)
1541 return (sizeCandidate.Width - CandidateMargin.Start - CandidateMargin.End,
1542 sizeCandidate.Height - CandidateMargin.Top - CandidateMargin.Bottom);
1545 private void DelayedRequestLayout(float scrollPosition , bool force = true)
1547 if (requestLayoutTimer != null)
1549 requestLayoutTimer.Dispose();
1552 requestLayoutTimer = new Timer(1);
1553 requestLayoutTimer.Interval = 1;
1554 requestLayoutTimer.Tick += ((object target, Timer.TickEventArgs args) =>
1556 RequestLayout(scrollPosition, force);
1559 requestLayoutTimer.Start();
1562 private GroupInfo GetGroupInfo(int index)
1564 if (Visited != null)
1566 if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index)
1569 if (hasHeader && index == 0) return null;
1570 foreach (GroupInfo group in groups)
1572 if (group.StartIndex <= index && group.StartIndex + group.Count > index)
1583 private object GetGroupParent(int index)
1585 if (Visited != null)
1587 if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index)
1588 return Visited.GroupParent;
1590 if (hasHeader && index == 0) return null;
1591 foreach (GroupInfo group in groups)
1593 if (group.StartIndex <= index && group.StartIndex + group.Count > index)
1596 return group.GroupParent;