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;
47 private bool isSourceEmpty;
50 /// Clean up ItemsLayouter.
52 /// <param name="view"> CollectionView of layouter. </param>
53 /// <remarks>please note that, view must be type of CollectionView</remarks>
54 /// <since_tizen> 9 </since_tizen>
55 public override void Initialize(RecyclerView view)
57 colView = view as CollectionView;
60 throw new ArgumentException("GridLayouter only can be applied CollectionView.", nameof(view));
64 foreach (RecyclerViewItem item in VisibleItems)
66 colView.UnrealizeItem(item, false);
74 IsHorizontal = (colView.ScrollingDirection == ScrollableBase.Direction.Horizontal);
76 RecyclerViewItem header = colView?.Header;
77 RecyclerViewItem footer = colView?.Footer;
79 int count = colView.InternalItemSource.Count;
80 int pureCount = count - (header? 1 : 0) - (footer? 1 : 0);
82 // 2. Get the header / footer and size deligated item and measure the size.
85 MeasureChild(colView, header);
87 width = header.Layout != null? header.Layout.MeasuredWidth.Size.AsRoundedValue() : 0;
88 height = header.Layout != null? header.Layout.MeasuredHeight.Size.AsRoundedValue() : 0;
90 Extents itemMargin = header.Margin;
91 headerSize = IsHorizontal?
92 width + itemMargin.Start + itemMargin.End:
93 height + itemMargin.Top + itemMargin.Bottom;
94 headerMargin = new Extents(itemMargin);
97 colView.UnrealizeItem(header);
102 MeasureChild(colView, footer);
104 width = footer.Layout != null? footer.Layout.MeasuredWidth.Size.AsRoundedValue() : 0;
105 height = footer.Layout != null? footer.Layout.MeasuredHeight.Size.AsRoundedValue() : 0;
107 Extents itemMargin = footer.Margin;
108 footerSize = IsHorizontal?
109 width + itemMargin.Start + itemMargin.End:
110 height + itemMargin.Top + itemMargin.Bottom;
111 footerMargin = new Extents(itemMargin);
112 footer.Index = count - 1;
115 colView.UnrealizeItem(footer);
120 isSourceEmpty = true;
121 base.Initialize(colView);
124 isSourceEmpty = false;
126 int firstIndex = header? 1 : 0;
128 if (colView.IsGrouped)
132 if (colView.GroupHeaderTemplate != null)
134 while (!colView.InternalItemSource.IsGroupHeader(firstIndex)) firstIndex++;
135 //must be always true
136 if (colView.InternalItemSource.IsGroupHeader(firstIndex))
138 RecyclerViewItem groupHeader = colView.RealizeItem(firstIndex);
141 if (groupHeader == null) throw new Exception("[" + firstIndex + "] Group Header failed to realize!");
143 // Need to Set proper height or width on scroll direction.
144 if (groupHeader.Layout == null)
146 width = groupHeader.WidthSpecification;
147 height = groupHeader.HeightSpecification;
151 MeasureChild(colView, groupHeader);
153 width = groupHeader.Layout.MeasuredWidth.Size.AsRoundedValue();
154 height = groupHeader.Layout.MeasuredHeight.Size.AsRoundedValue();
156 //Console.WriteLine("[NUI] GroupHeader Size {0} :{0}", width, height);
157 // pick the StepCandidate.
158 Extents itemMargin = groupHeader.Margin;
159 groupHeaderSize = IsHorizontal?
160 width + itemMargin.Start + itemMargin.End:
161 height + itemMargin.Top + itemMargin.Bottom;
162 groupHeaderMargin = new Extents(itemMargin);
163 colView.UnrealizeItem(groupHeader);
168 groupHeaderSize = 0F;
171 if (colView.GroupFooterTemplate != null)
173 int firstFooter = firstIndex;
174 while (!colView.InternalItemSource.IsGroupFooter(firstFooter)) firstFooter++;
175 //must be always true
176 if (colView.InternalItemSource.IsGroupFooter(firstFooter))
178 RecyclerViewItem groupFooter = colView.RealizeItem(firstFooter);
180 if (groupFooter == null) throw new Exception("[" + firstFooter + "] Group Footer failed to realize!");
181 // Need to Set proper height or width on scroll direction.
182 if (groupFooter.Layout == null)
184 width = groupFooter.WidthSpecification;
185 height = groupFooter.HeightSpecification;
189 MeasureChild(colView, groupFooter);
191 width = groupFooter.Layout.MeasuredWidth.Size.AsRoundedValue();
192 height = groupFooter.Layout.MeasuredHeight.Size.AsRoundedValue();
194 // pick the StepCandidate.
195 Extents itemMargin = groupFooter.Margin;
196 groupFooterSize = IsHorizontal?
197 width + itemMargin.Start + itemMargin.End:
198 height + itemMargin.Top + itemMargin.Bottom;
199 groupFooterMargin = new Extents(itemMargin);
201 colView.UnrealizeItem(groupFooter);
206 groupFooterSize = 0F;
209 else isGrouped = false;
212 //Final Check of FirstIndex
213 while (colView.InternalItemSource.IsHeader(firstIndex) ||
214 colView.InternalItemSource.IsGroupHeader(firstIndex) ||
215 colView.InternalItemSource.IsGroupFooter(firstIndex))
217 if (colView.InternalItemSource.IsFooter(firstIndex))
226 sizeCandidate = (0, 0);
229 // Get Size Deligate. FIXME if group exist index must be changed.
230 RecyclerViewItem sizeDeligate = colView.RealizeItem(firstIndex);
231 if (sizeDeligate == null)
233 throw new Exception("Cannot create content from DatTemplate.");
235 sizeDeligate.BindingContext = colView.InternalItemSource.GetItem(firstIndex);
237 // Need to Set proper height or width on scroll direction.
238 if (sizeDeligate.Layout == null)
240 width = sizeDeligate.WidthSpecification;
241 height = sizeDeligate.HeightSpecification;
245 MeasureChild(colView, sizeDeligate);
247 width = sizeDeligate.Layout.MeasuredWidth.Size.AsRoundedValue();
248 height = sizeDeligate.Layout.MeasuredHeight.Size.AsRoundedValue();
250 //Console.WriteLine("[NUI] item Size {0} :{1}", width, height);
252 // pick the StepCandidate.
253 Extents itemMargin = sizeDeligate.Margin;
254 width = width + itemMargin.Start + itemMargin.End;
255 height = height + itemMargin.Top + itemMargin.Bottom;
256 StepCandidate = IsHorizontal? width : height;
257 CandidateMargin = new Extents(itemMargin);
259 // Prevent zero division.
260 if (width == 0) width = 1;
261 if (height == 0) height = 1;
262 spanSize = IsHorizontal?
263 Convert.ToInt32(Math.Truncate((double)((colView.Size.Height - Padding.Top - Padding.Bottom) / height))) :
264 Convert.ToInt32(Math.Truncate((double)((colView.Size.Width - Padding.Start - Padding.End) / width)));
266 sizeCandidate = (width, height);
268 colView.UnrealizeItem(sizeDeligate);
271 if (StepCandidate < 1) StepCandidate = 1;
272 if (spanSize < 1) spanSize = 1;
276 float Current = 0.0F;
277 IGroupableItemSource source = colView.InternalItemSource;
278 GroupInfo currentGroup = null;
279 object currentParent = null;
281 for (int i = 0; i < count; i++)
283 if (i == 0 && hasHeader)
285 Current += headerSize;
287 else if (i == count - 1 && hasFooter)
289 Current += footerSize;
293 if (source.GetGroupParent(i) != currentParent)
295 currentParent = source.GetGroupParent(i);
296 float currentSize = (source.IsGroupHeader(i)? groupHeaderSize :
297 (source.IsGroupFooter(i)? groupFooterSize: StepCandidate));
298 currentGroup = new GroupInfo()
300 GroupParent = source.GetGroupParent(i),
303 GroupSize = currentSize,
304 GroupPosition = Current
306 groups.Add(currentGroup);
307 Current += currentSize;
310 else if (source.IsGroupFooter(i))
312 //currentGroup.hasFooter = true;
313 if (currentGroup != null)
315 currentGroup.Count++;
316 currentGroup.GroupSize += groupFooterSize;
317 Current += groupFooterSize;
322 if (currentGroup != null)
324 currentGroup.Count++;
325 int index = i - currentGroup.StartIndex - ((colView.GroupHeaderTemplate != null) ? 1 : 0);
326 if ((index % spanSize) == 0)
328 currentGroup.GroupSize += StepCandidate;
329 Current += StepCandidate;
335 ScrollContentSize = Current;
339 // 3. Measure the scroller content size.
340 ScrollContentSize = StepCandidate * Convert.ToInt32(Math.Ceiling((double)pureCount / (double)spanSize));
341 if (hasHeader) ScrollContentSize += headerSize;
342 if (hasFooter) ScrollContentSize += footerSize;
345 ScrollContentSize = IsHorizontal?
346 ScrollContentSize + Padding.Start + Padding.End:
347 ScrollContentSize + Padding.Top + Padding.Bottom;
349 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
350 else colView.ContentContainer.SizeHeight = ScrollContentSize;
352 base.Initialize(colView);
353 //Console.WriteLine("Init Done, StepCnadidate{0}, spanSize{1}, Scroll{2}", StepCandidate, spanSize, ScrollContentSize);
357 /// This is called to find out where items are lain out according to current scroll position.
359 /// <param name="scrollPosition">Scroll position which is calculated by ScrollableBase</param>
360 /// <param name="force">boolean force flag to layouting forcely.</param>
361 /// <since_tizen> 9 </since_tizen>
362 public override void RequestLayout(float scrollPosition, bool force = false)
364 // Layouting is only possible after once it intialized.
365 if (!IsInitialized) return;
366 int LastIndex = colView.InternalItemSource.Count;
368 if (!force && PrevScrollPosition == Math.Abs(scrollPosition)) return;
369 PrevScrollPosition = Math.Abs(scrollPosition);
371 int prevFirstVisible = FirstVisible;
372 int prevLastVisible = LastVisible;
373 bool IsHorizontal = (colView.ScrollingDirection == ScrollableBase.Direction.Horizontal);
375 (float X, float Y) visibleArea = (PrevScrollPosition,
376 PrevScrollPosition + (IsHorizontal? colView.Size.Width : colView.Size.Height)
379 //Console.WriteLine("[NUI] itemsView [{0},{1}] [{2},{3}]", colView.Size.Width, colView.Size.Height, colView.ContentContainer.Size.Width, colView.ContentContainer.Size.Height);
381 // 1. Set First/Last Visible Item Index.
382 (int start, int end) = FindVisibleItems(visibleArea);
383 FirstVisible = start;
386 //Console.WriteLine("[NUI] {0} :visibleArea before [{1},{2}] after [{3},{4}]", scrollPosition, prevFirstVisible, prevLastVisible, FirstVisible, LastVisible);
388 // 2. Unrealize invisible items.
389 List<RecyclerViewItem> unrealizedItems = new List<RecyclerViewItem>();
390 foreach (RecyclerViewItem item in VisibleItems)
392 if (item.Index < FirstVisible || item.Index > LastVisible)
394 //Console.WriteLine("[NUI] Unrealize{0}!", item.Index);
395 unrealizedItems.Add(item);
396 colView.UnrealizeItem(item);
399 VisibleItems.RemoveAll(unrealizedItems.Contains);
401 //Console.WriteLine("Realize Begin [{0} to {1}]", FirstVisible, LastVisible);
402 // 3. Realize and placing visible items.
403 for (int i = FirstVisible; i <= LastVisible; i++)
405 //Console.WriteLine("[NUI] Realize!");
406 RecyclerViewItem item = null;
407 // 4. Get item if visible or realize new.
408 if (i >= prevFirstVisible && i <= prevLastVisible)
410 item = GetVisibleItem(i);
411 if (item != null && !force) continue;
415 item = colView.RealizeItem(i);
416 if (item != null) VisibleItems.Add(item);
417 else throw new Exception("Failed to create RecycerViewItem index of ["+ i + "]");
420 //item Position without Padding and Margin.
421 (float x, float y) = GetItemPosition(i);
422 // 5. Placing item with Padding and Margin.
423 item.Position = new Position(x, y);
425 //Linear Item need to be resized!
426 if (item.IsHeader || item.IsFooter || item.isGroupHeader || item.isGroupFooter)
428 var size = (IsHorizontal? item.SizeWidth: item.SizeHeight);
429 if (colView.SizingStrategy == ItemSizingStrategy.MeasureFirst)
431 if (item.IsHeader) size = headerSize;
432 else if (item.IsFooter) size = footerSize;
433 else if (item.isGroupHeader) size = groupHeaderSize;
434 else if (item.isGroupFooter) size = groupFooterSize;
436 if (IsHorizontal && item.HeightSpecification == LayoutParamPolicies.MatchParent)
438 item.Size = new Size(size, Container.Size.Height - Padding.Top - Padding.Bottom - item.Margin.Top - item.Margin.Bottom);
440 else if (!IsHorizontal && item.WidthSpecification == LayoutParamPolicies.MatchParent)
442 item.Size = new Size(Container.Size.Width - Padding.Start - Padding.End - item.Margin.Start - item.Margin.End, size);
445 //Console.WriteLine("[NUI] ["+item.Index+"] ["+item.Position.X+", "+item.Position.Y+" ==== \n");
447 //Console.WriteLine("Realize Done");
451 /// Clear the current screen and all properties.
453 [EditorBrowsable(EditorBrowsableState.Never)]
454 public override void Clear()
457 if (requestLayoutTimer != null)
459 requestLayoutTimer.Dispose();
464 foreach (GroupInfo group in groups)
466 //group.ItemPosition?.Clear();
473 if (headerMargin != null)
475 headerMargin.Dispose();
478 if (footerMargin != null)
480 footerMargin.Dispose();
483 if (groupHeaderMargin != null)
485 groupHeaderMargin.Dispose();
486 groupHeaderMargin = null;
488 if (groupFooterMargin != null)
490 groupFooterMargin.Dispose();
491 groupFooterMargin = null;
498 public override void NotifyItemSizeChanged(RecyclerViewItem item)
500 // All Item size need to be same in grid!
501 // if you want to change item size, change dataTemplate to re-initing.
506 [EditorBrowsable(EditorBrowsableState.Never)]
507 public override void NotifyItemInserted(IItemSource source, int startIndex)
509 // Insert Single item.
510 if (source == null) throw new ArgumentNullException(nameof(source));
511 if (colView == null) return;
517 // Will be null if not a group.
518 float currentSize = 0;
519 IGroupableItemSource gSource = source as IGroupableItemSource;
521 // Get the first Visible Position to adjust.
523 int topInScreenIndex = 0;
525 (topInScreenIndex, offset) = FindTopItemInScreen();
528 //2. Handle Group Case.
529 if (isGrouped && gSource != null)
531 GroupInfo groupInfo = null;
532 object groupParent = gSource.GetGroupParent(startIndex);
533 int parentIndex = gSource.GetPosition(groupParent);
534 if (gSource.HasHeader) parentIndex--;
536 // Check item is group parent or not
537 // if group parent, add new gorupinfo
538 if (gSource.IsHeader(startIndex))
540 // This is childless group.
541 // create new groupInfo!
542 groupInfo = new GroupInfo()
544 GroupParent = groupParent,
545 StartIndex = startIndex,
547 GroupSize = groupHeaderSize,
550 if (parentIndex >= groups.Count)
552 groupInfo.GroupPosition = ScrollContentSize;
553 groups.Add(groupInfo);
557 groupInfo.GroupPosition = groups[parentIndex].GroupPosition;
558 groups.Insert(parentIndex, groupInfo);
561 currentSize = groupHeaderSize;
565 // If not group parent, add item into the groupinfo.
566 if (parentIndex >= groups.Count) throw new Exception("group parent is bigger than group counts.");
567 groupInfo = groups[parentIndex];//GetGroupInfo(groupParent);
568 if (groupInfo == null) throw new Exception("Cannot find group information!");
570 if (gSource.IsGroupFooter(startIndex))
572 // It doesn't make sence to adding footer by notify...
573 // if GroupFooterTemplate is added,
574 // need to implement on here.
578 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
580 // Wrong! Grid Layouter do not support MeasureAll!
584 int pureCount = groupInfo.Count - 1 - (colView.GroupFooterTemplate == null? 0: 1);
585 if (pureCount % spanSize == 0)
587 currentSize = StepCandidate;
588 groupInfo.GroupSize += currentSize;
597 if (parentIndex + 1 < groups.Count)
599 for(int i = parentIndex + 1; i < groups.Count; i++)
601 groups[i].GroupPosition += currentSize;
602 groups[i].StartIndex++;
608 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
610 // Wrong! Grid Layouter do not support MeasureAll!
612 int pureCount = colView.InternalItemSource.Count - (hasHeader? 1: 0) - (hasFooter? 1: 0);
614 // Count comes after updated in ungrouped case!
615 if (pureCount % spanSize == 1)
617 currentSize = StepCandidate;
621 // 3. Update Scroll Content Size
622 ScrollContentSize += currentSize;
624 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
625 else colView.ContentContainer.SizeHeight = ScrollContentSize;
627 // 4. Update Visible Items.
628 foreach (RecyclerViewItem item in VisibleItems)
630 if (item.Index >= startIndex)
636 float scrollPosition = PrevScrollPosition;
640 // Insertion above Top Visible!
641 if (startIndex <= topInScreenIndex)
643 scrollPosition = GetItemPosition(topInScreenIndex);
644 scrollPosition -= offset;
646 colView.ScrollTo(scrollPosition);
650 // Update Viewport in delay.
651 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
652 // but currently we do not have any accessor to pre-calculation so instead of this,
653 // using Timer temporarily.
654 DelayedRequestLayout(scrollPosition);
658 [EditorBrowsable(EditorBrowsableState.Never)]
659 public override void NotifyItemRangeInserted(IItemSource source, int startIndex, int count)
662 if (source == null) throw new ArgumentNullException(nameof(source));
663 if (colView == null) return;
669 float currentSize = StepCandidate;
670 // Will be null if not a group.
671 IGroupableItemSource gSource = source as IGroupableItemSource;
673 // Get the first Visible Position to adjust.
675 int topInScreenIndex = 0;
677 (topInScreenIndex, offset) = FindTopItemInScreen();
680 // 2. Handle Group Case
681 // Adding ranged items should all same new groups.
682 if (isGrouped && gSource != null)
684 GroupInfo groupInfo = null;
685 object groupParent = gSource.GetGroupParent(startIndex);
686 int parentIndex = gSource.GetPosition(groupParent);
687 if (gSource.HasHeader) parentIndex--;
689 // We guess here that range inserted from GroupStartIndex.
690 int groupStartIndex = startIndex;
692 for (int current = startIndex; current - startIndex < count; current++)
694 // Check item is group parent or not
695 // if group parent, add new gorupinfo
696 if (groupStartIndex == current)
698 //create new groupInfo!
699 currentSize = (gSource.IsGroupHeader(current)? groupHeaderSize :
700 (gSource.IsGroupFooter(current)? groupFooterSize: currentSize));
701 groupInfo = new GroupInfo()
703 GroupParent = groupParent,
704 StartIndex = current,
706 GroupSize = StepCandidate,
708 currentSize += StepCandidate;
713 //if not group parent, add item into the groupinfo.
714 //groupInfo = GetGroupInfo(groupStartIndex);
715 if (groupInfo == null) throw new Exception("Cannot find group information!");
718 if (gSource.IsGroupFooter(current))
720 groupInfo.GroupSize += groupFooterSize;
721 currentSize += groupFooterSize;
725 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
727 // Wrong! Grid Layouter do not support MeasureAll!
731 int index = current - groupStartIndex - ((colView.GroupHeaderTemplate != null)? 1: 0);
732 if ((index % spanSize) == 0)
734 groupInfo.GroupSize += StepCandidate;
735 currentSize += StepCandidate;
742 if (parentIndex >= groups.Count)
744 groupInfo.GroupPosition = ScrollContentSize;
745 groups.Add(groupInfo);
749 groupInfo.GroupPosition = groups[parentIndex].GroupPosition;
750 groups.Insert(parentIndex, groupInfo);
753 // Update other below group's position
754 if (parentIndex + 1 < groups.Count)
756 for(int i = parentIndex + 1; i < groups.Count; i++)
758 groups[i].GroupPosition += currentSize;
759 groups[i].StartIndex += count;
763 ScrollContentSize += currentSize;
767 throw new Exception("Cannot insert ungrouped range items!");
770 // 3. Update Scroll Content Size
771 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
772 else colView.ContentContainer.SizeHeight = ScrollContentSize;
774 // 4. Update Visible Items.
775 foreach (RecyclerViewItem item in VisibleItems)
777 if (item.Index >= startIndex)
784 float scrollPosition = PrevScrollPosition;
786 // Insertion above Top Visible!
787 if (startIndex + count <= topInScreenIndex)
789 scrollPosition = GetItemPosition(topInScreenIndex);
790 scrollPosition -= offset;
792 colView.ScrollTo(scrollPosition);
796 // Update Viewport in delay.
797 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
798 // but currently we do not have any accessor to pre-calculation so instead of this,
799 // using Timer temporarily.
800 DelayedRequestLayout(scrollPosition);
804 [EditorBrowsable(EditorBrowsableState.Never)]
805 public override void NotifyItemRemoved(IItemSource source, int startIndex)
808 if (source == null) throw new ArgumentNullException(nameof(source));
809 if (colView == null) return;
811 // Will be null if not a group.
812 float currentSize = 0;
813 IGroupableItemSource gSource = source as IGroupableItemSource;
815 // Get the first Visible Position to adjust.
817 int topInScreenIndex = 0;
819 (topInScreenIndex, offset) = FindTopItemInScreen();
822 // 2. Handle Group Case
823 if (isGrouped && gSource != null)
826 GroupInfo groupInfo = null;
827 foreach(GroupInfo cur in groups)
829 if ((cur.StartIndex <= startIndex) && (cur.StartIndex + cur.Count - 1 >= startIndex))
836 if (groupInfo == null) throw new Exception("Cannot find group information!");
837 // Check item is group parent or not
838 // if group parent, add new gorupinfo
839 if (groupInfo.StartIndex == startIndex)
841 // This is empty group!
842 // check group is empty.
843 if (groupInfo.Count != 1)
845 throw new Exception("Cannot remove group parent");
847 currentSize = groupInfo.GroupSize;
850 // groupInfo.Dispose();
851 groups.Remove(groupInfo);
857 // Skip footer case as footer cannot exist alone without header.
858 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
860 // Wrong! Grid Layouter do not support MeasureAll!
864 int pureCount = groupInfo.Count - 1 - (colView.GroupFooterTemplate == null? 0: 1);
865 if (pureCount % spanSize == 0)
867 currentSize = StepCandidate;
868 groupInfo.GroupSize -= currentSize;
873 for (int i = parentIndex + 1; i < groups.Count; i++)
875 groups[i].GroupPosition -= currentSize;
876 groups[i].StartIndex--;
881 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
883 // Wrong! Grid Layouter do not support MeasureAll!
885 int pureCount = colView.InternalItemSource.Count - (hasHeader? 1: 0) - (hasFooter? 1: 0);
887 // Count comes after updated in ungrouped case!
888 if (pureCount % spanSize == 0)
890 currentSize = StepCandidate;
895 ScrollContentSize -= currentSize;
897 // 3. Update Scroll Content Size
898 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
899 else colView.ContentContainer.SizeHeight = ScrollContentSize;
901 // 4. Update Visible Items.
902 RecyclerViewItem targetItem = null;
903 foreach (RecyclerViewItem item in VisibleItems)
905 if (item.Index == startIndex)
908 colView.UnrealizeItem(item);
910 else if (item.Index > startIndex)
915 VisibleItems.Remove(targetItem);
918 float scrollPosition = PrevScrollPosition;
920 // Insertion above Top Visible!
921 if (startIndex <= topInScreenIndex)
923 scrollPosition = GetItemPosition(topInScreenIndex);
924 scrollPosition -= offset;
926 colView.ScrollTo(scrollPosition);
930 // Update Viewport in delay.
931 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
932 // but currently we do not have any accessor to pre-calculation so instead of this,
933 // using Timer temporarily.
934 DelayedRequestLayout(scrollPosition);
938 [EditorBrowsable(EditorBrowsableState.Never)]
939 public override void NotifyItemRangeRemoved(IItemSource source, int startIndex, int count)
942 if (source == null) throw new ArgumentNullException(nameof(source));
943 if (colView == null) return;
945 // Will be null if not a group.
946 float currentSize = StepCandidate;
947 IGroupableItemSource gSource = source as IGroupableItemSource;
949 // Get the first Visible Position to adjust.
951 int topInScreenIndex = 0;
953 (topInScreenIndex, offset) = FindTopItemInScreen();
956 // 1. Handle Group Case
957 if (isGrouped && gSource != null)
960 GroupInfo groupInfo = null;
961 foreach(GroupInfo cur in groups)
963 if ((cur.StartIndex == startIndex) && (cur.Count == count))
970 if (groupInfo == null) throw new Exception("Cannot find group information!");
971 // Check item is group parent or not
972 // if group parent, add new gorupinfo
973 currentSize = groupInfo.GroupSize;
974 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
976 // Wrong! Grid Layouter do not support MeasureAll!
979 // groupInfo.Dispose();
980 groups.Remove(groupInfo);
982 for (int i = parentIndex; i < groups.Count; i++)
984 groups[i].GroupPosition -= currentSize;
985 groups[i].StartIndex -= count;
990 // It must group case! throw exception!
991 throw new Exception("Range remove must group remove!");
994 ScrollContentSize -= currentSize;
996 // 2. Update Scroll Content Size
997 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
998 else colView.ContentContainer.SizeHeight = ScrollContentSize;
1000 // 3. Update Visible Items.
1001 List<RecyclerViewItem> unrealizedItems = new List<RecyclerViewItem>();
1002 foreach (RecyclerViewItem item in VisibleItems)
1004 if ((item.Index >= startIndex)
1005 && (item.Index < startIndex + count))
1007 unrealizedItems.Add(item);
1008 colView.UnrealizeItem(item);
1010 else if (item.Index >= startIndex + count)
1012 item.Index -= count;
1015 VisibleItems.RemoveAll(unrealizedItems.Contains);
1016 unrealizedItems.Clear();
1019 float scrollPosition = PrevScrollPosition;
1021 // Insertion above Top Visible!
1022 if (startIndex <= topInScreenIndex)
1024 scrollPosition = GetItemPosition(topInScreenIndex);
1025 scrollPosition -= offset;
1027 colView.ScrollTo(scrollPosition);
1031 // Update Viewport in delay.
1032 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
1033 // but currently we do not have any accessor to pre-calculation so instead of this,
1034 // using Timer temporarily.
1035 DelayedRequestLayout(scrollPosition);
1039 [EditorBrowsable(EditorBrowsableState.Never)]
1040 public override void NotifyItemMoved(IItemSource source, int fromPosition, int toPosition)
1043 if (source == null) throw new ArgumentNullException(nameof(source));
1044 if (colView == null) return;
1046 // Will be null if not a group.
1047 float currentSize = StepCandidate;
1048 int diff = toPosition - fromPosition;
1050 // Get the first Visible Position to adjust.
1052 int topInScreenIndex = 0;
1054 (topInScreenIndex, offset) = FindTopItemInScreen();
1057 // Move can only happen in it's own groups.
1058 // so there will be no changes in position, startIndex in ohter groups.
1059 // check visible item and update indexs.
1060 int startIndex = ( diff > 0 ? fromPosition: toPosition);
1061 int endIndex = (diff > 0 ? toPosition: fromPosition);
1063 if ((endIndex >= FirstVisible) && (startIndex <= LastVisible))
1065 foreach (RecyclerViewItem item in VisibleItems)
1067 if ((item.Index >= startIndex)
1068 && (item.Index <= endIndex))
1070 if (item.Index == fromPosition) item.Index = toPosition;
1073 if (diff > 0) item.Index--;
1080 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
1081 else colView.ContentContainer.SizeHeight = ScrollContentSize;
1084 float scrollPosition = PrevScrollPosition;
1086 // Insertion above Top Visible!
1087 if (((fromPosition > topInScreenIndex) && (toPosition < topInScreenIndex) ||
1088 ((fromPosition < topInScreenIndex) && (toPosition > topInScreenIndex)))
1090 scrollPosition = GetItemPosition(topInScreenIndex);
1091 scrollPosition -= offset;
1093 colView.ScrollTo(scrollPosition);
1097 // Update Viewport in delay.
1098 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
1099 // but currently we do not have any accessor to pre-calculation so instead of this,
1100 // using Timer temporarily.
1101 DelayedRequestLayout(scrollPosition);
1105 [EditorBrowsable(EditorBrowsableState.Never)]
1106 public override void NotifyItemRangeMoved(IItemSource source, int fromPosition, int toPosition, int count)
1109 if (source == null) throw new ArgumentNullException(nameof(source));
1110 if (colView == null) return;
1112 // Will be null if not a group.
1113 float currentSize = StepCandidate;
1114 int diff = toPosition - fromPosition;
1116 int startIndex = ( diff > 0 ? fromPosition: toPosition);
1117 int endIndex = (diff > 0 ? toPosition + count - 1: fromPosition + count - 1);
1119 // 2. Handle Group Case
1122 int fromParentIndex = 0;
1123 int toParentIndex = 0;
1124 bool findFrom = false;
1125 bool findTo = false;
1126 GroupInfo fromGroup = null;
1127 GroupInfo toGroup = null;
1129 foreach(GroupInfo cur in groups)
1131 if ((cur.StartIndex == fromPosition) && (cur.Count == count))
1135 if (findFrom && findTo) break;
1137 else if (cur.StartIndex == toPosition)
1141 if (findFrom && findTo) break;
1143 if (!findFrom) fromParentIndex++;
1144 if (!findTo) toParentIndex++;
1146 if (toGroup == null || fromGroup == null) throw new Exception("Cannot find group information!");
1148 fromGroup.StartIndex = toGroup.StartIndex;
1149 fromGroup.GroupPosition = toGroup.GroupPosition;
1151 endIndex = (diff > 0 ? toPosition + toGroup.Count - 1: fromPosition + count - 1);
1153 groups.Remove(fromGroup);
1154 groups.Insert(toParentIndex, fromGroup);
1156 int startGroup = (diff > 0? fromParentIndex: toParentIndex);
1157 int endGroup = (diff > 0? toParentIndex: fromParentIndex);
1159 for (int i = startGroup; i <= endGroup; i++)
1161 if (i == toParentIndex) continue;
1162 float prevPos = groups[i].GroupPosition;
1163 int prevIdx = groups[i].StartIndex;
1164 groups[i].GroupPosition = groups[i].GroupPosition + (diff > 0? -1 : 1) * fromGroup.GroupSize;
1165 groups[i].StartIndex = groups[i].StartIndex + (diff > 0? -1 : 1) * fromGroup.Count;
1170 //It must group case! throw exception!
1171 throw new Exception("Range remove must group remove!");
1174 // Move can only happen in it's own groups.
1175 // so there will be no changes in position, startIndex in ohter groups.
1176 // check visible item and update indexs.
1177 if ((endIndex >= FirstVisible) && (startIndex <= LastVisible))
1179 foreach (RecyclerViewItem item in VisibleItems)
1181 if ((item.Index >= startIndex)
1182 && (item.Index <= endIndex))
1184 if ((item.Index >= fromPosition) && (item.Index < fromPosition + count))
1186 item.Index = fromPosition - item.Index + toPosition;
1190 if (diff > 0) item.Index -= count;
1191 else item.Index += count;
1198 float scrollPosition = PrevScrollPosition;
1200 // Insertion above Top Visible!
1201 if (((fromPosition > topInScreenIndex) && (toPosition < topInScreenIndex) ||
1202 ((fromPosition < topInScreenIndex) && (toPosition > topInScreenIndex)))
1204 scrollPosition = GetItemPosition(topInScreenIndex);
1205 scrollPosition -= offset;
1207 colView.ScrollTo(scrollPosition);
1211 // Update Viewport in delay.
1212 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
1213 // but currently we do not have any accessor to pre-calculation so instead of this,
1214 // using Timer temporarily.
1215 DelayedRequestLayout(scrollPosition);
1219 [EditorBrowsable(EditorBrowsableState.Never)]
1220 public override float CalculateLayoutOrientationSize()
1222 //Console.WriteLine("[NUI] Calculate Layout ScrollContentSize {0}", ScrollContentSize);
1223 return ScrollContentSize;
1227 [EditorBrowsable(EditorBrowsableState.Never)]
1228 public override float CalculateCandidateScrollPosition(float scrollPosition)
1230 //Console.WriteLine("[NUI] Calculate Candidate ScrollContentSize {0}", ScrollContentSize);
1231 return scrollPosition;
1235 [EditorBrowsable(EditorBrowsableState.Never)]
1236 public override View RequestNextFocusableView(View currentFocusedView, View.FocusDirection direction, bool loopEnabled)
1238 if (currentFocusedView == null)
1239 throw new ArgumentNullException(nameof(currentFocusedView));
1241 View nextFocusedView = null;
1242 int targetSibling = -1;
1243 bool IsHorizontal = colView.ScrollingDirection == ScrollableBase.Direction.Horizontal;
1247 case View.FocusDirection.Left:
1249 targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder - 1 : targetSibling;
1252 case View.FocusDirection.Right:
1254 targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder + 1 : targetSibling;
1257 case View.FocusDirection.Up:
1259 targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder - 1;
1262 case View.FocusDirection.Down:
1264 targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder + 1;
1269 if (targetSibling > -1 && targetSibling < Container.Children.Count)
1271 RecyclerViewItem candidate = Container.Children[targetSibling] as RecyclerViewItem;
1272 if (candidate != null && candidate.Index >= 0 && candidate.Index < colView.InternalItemSource.Count)
1274 nextFocusedView = candidate;
1277 return nextFocusedView;
1281 [EditorBrowsable(EditorBrowsableState.Never)]
1282 protected override (int start, int end) FindVisibleItems((float X, float Y) visibleArea)
1284 int MaxIndex = colView.InternalItemSource.Count - 1 - (hasFooter ? 1 : 0);
1285 int adds = spanSize * 2;
1287 (int start, int end) found = (0, 0);
1289 // Header is Showing
1290 if (hasHeader && visibleArea.X < headerSize + (IsHorizontal? Padding.Start : Padding.Top))
1299 foreach (GroupInfo gInfo in groups)
1303 if (gInfo.GroupPosition <= visibleArea.X &&
1304 gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.X)
1306 if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.X)
1308 found.start = gInfo.StartIndex - adds;
1312 //can be step in spanSize...
1313 for (int i = 1; i < gInfo.Count; i++)
1316 // Reach last index of group.
1317 if (i == (gInfo.Count - 1))
1319 found.start = gInfo.StartIndex + i - adds;
1324 else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.X - gInfo.GroupPosition - groupHeaderSize)
1326 found.start = gInfo.StartIndex + i - adds;
1334 //footer only shows?
1337 found.start = MaxIndex;
1342 float visibleAreaX = visibleArea.X - (hasHeader ? headerSize : 0);
1343 // Prevent zero division.
1344 var itemSize = (StepCandidate != 0)? StepCandidate: 1f;
1345 found.start = (Convert.ToInt32(Math.Abs(visibleAreaX / itemSize)) - 1) * spanSize;
1346 if (hasHeader) found.start += 1;
1348 if (found.start < 0) found.start = 0;
1351 if (hasFooter && visibleArea.Y > ScrollContentSize - footerSize - (IsHorizontal? Padding.End : Padding.Bottom))
1353 found.end = MaxIndex + 1;
1360 // can it be start from founded group...?
1361 //foreach(GroupInfo gInfo in groups.Skip(skipGroup))
1362 foreach (GroupInfo gInfo in groups)
1365 if (gInfo.GroupPosition <= visibleArea.Y &&
1366 gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.Y)
1368 if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.Y)
1370 found.end = gInfo.StartIndex + adds;
1374 //can be step in spanSize...
1375 for (int i = 1; i < gInfo.Count; i++)
1378 // Reach last index of group.
1379 if (i == (gInfo.Count - 1))
1381 found.end = gInfo.StartIndex + i + adds;
1385 else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.Y - gInfo.GroupPosition - groupHeaderSize)
1387 found.end = gInfo.StartIndex + i + adds;
1395 //footer only shows?
1398 found.end = MaxIndex;
1403 float visibleAreaY = visibleArea.Y - (hasHeader ? headerSize : 0);
1404 //Need to Consider GroupHeight!!!!
1405 // Prevent zero division.
1406 var itemSize = (StepCandidate != 0)? StepCandidate: 1f;
1407 found.end = (Convert.ToInt32(Math.Abs(visibleAreaY / itemSize)) + 1) * spanSize + adds;
1408 if (hasHeader) found.end += 1;
1410 if (found.end > (MaxIndex)) found.end = MaxIndex;
1415 internal override (float X, float Y) GetItemPosition(int index)
1418 int spaceStartX = Padding.Start;
1419 int spaceStartY = Padding.Top;
1420 int emptyArea = IsHorizontal?
1421 (int)(colView.Size.Height - Padding.Top - Padding.Bottom - (sizeCandidate.Height * spanSize)) :
1422 (int)(colView.Size.Width - Padding.Start - Padding.End - (sizeCandidate.Width * spanSize));
1424 if (hasHeader && index == 0)
1426 return (spaceStartX + headerMargin.Start, spaceStartY + headerMargin.Top);
1428 if (hasFooter && index == colView.InternalItemSource.Count - 1)
1430 xPos = IsHorizontal?
1431 ScrollContentSize - Padding.End - footerSize + footerMargin.Start:
1433 yPos = IsHorizontal?
1435 ScrollContentSize - Padding.Bottom - footerSize + footerMargin.Top;
1436 return (xPos, yPos);
1440 GroupInfo myGroup = GetGroupInfo(index);
1441 if (colView.InternalItemSource.IsGroupHeader(index))
1443 spaceStartX+= groupHeaderMargin.Start;
1444 spaceStartY+= groupHeaderMargin.Top;
1445 xPos = IsHorizontal?
1446 myGroup.GroupPosition + groupHeaderMargin.Start:
1448 yPos = IsHorizontal?
1450 myGroup.GroupPosition + groupHeaderMargin.Top;
1452 else if (colView.InternalItemSource.IsGroupFooter(index))
1454 spaceStartX+= groupFooterMargin.Start;
1455 spaceStartY+= groupFooterMargin.Top;
1456 xPos = IsHorizontal?
1457 myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize + groupFooterMargin.Start:
1459 yPos = IsHorizontal?
1461 myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize + groupFooterMargin.Top;
1465 int pureIndex = index - myGroup.StartIndex - ((colView.GroupHeaderTemplate != null)? 1: 0);
1466 int division = pureIndex / spanSize;
1467 int remainder = pureIndex % spanSize;
1468 if (division < 0) division = 0;
1469 if (remainder < 0) remainder = 0;
1470 spaceStartX+= CandidateMargin.Start;
1471 spaceStartY+= CandidateMargin.Top;
1473 xPos = IsHorizontal?
1474 (division * sizeCandidate.Width) + myGroup.GroupPosition + groupHeaderSize + CandidateMargin.Start:
1475 (emptyArea * align) + (remainder * sizeCandidate.Width) + spaceStartX;
1476 yPos = IsHorizontal?
1477 (emptyArea * align) + (remainder * sizeCandidate.Height) + spaceStartY:
1478 (division * sizeCandidate.Height) + myGroup.GroupPosition + groupHeaderSize + CandidateMargin.Top;
1483 int pureIndex = index - (colView.Header ? 1 : 0);
1484 // int convert must be truncate value.
1485 int division = pureIndex / spanSize;
1486 int remainder = pureIndex % spanSize;
1487 if (division < 0) division = 0;
1488 if (remainder < 0) remainder = 0;
1489 spaceStartX+= CandidateMargin.Start;
1490 spaceStartY+= CandidateMargin.Top;
1492 xPos = IsHorizontal?
1493 (division * sizeCandidate.Width) + (hasHeader? headerSize : 0) + spaceStartX:
1494 (emptyArea * align) + (remainder * sizeCandidate.Width) + spaceStartX;
1495 yPos = IsHorizontal?
1496 (emptyArea * align) + (remainder * sizeCandidate.Height) + spaceStartY:
1497 (division * sizeCandidate.Height) + (hasHeader? headerSize : 0) + spaceStartY;
1500 return (xPos, yPos);
1503 internal override (float Width, float Height) GetItemSize(int index)
1505 return (sizeCandidate.Width - CandidateMargin.Start - CandidateMargin.End,
1506 sizeCandidate.Height - CandidateMargin.Top - CandidateMargin.Bottom);
1508 private void DelayedRequestLayout(float scrollPosition , bool force = true)
1510 if (requestLayoutTimer != null)
1512 requestLayoutTimer.Dispose();
1515 requestLayoutTimer = new Timer(1);
1516 requestLayoutTimer.Interval = 1;
1517 requestLayoutTimer.Tick += ((object target, Timer.TickEventArgs args) =>
1519 RequestLayout(scrollPosition, force);
1524 private RecyclerViewItem GetVisibleItem(int index)
1526 foreach (RecyclerViewItem item in VisibleItems)
1528 if (item.Index == index) return item;
1534 private GroupInfo GetGroupInfo(int index)
1536 if (Visited != null)
1538 if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index)
1541 if (hasHeader && index == 0) return null;
1542 foreach (GroupInfo group in groups)
1544 if (group.StartIndex <= index && group.StartIndex + group.Count > index)
1555 private object GetGroupParent(int index)
1557 if (Visited != null)
1559 if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index)
1560 return Visited.GroupParent;
1562 if (hasHeader && index == 0) return null;
1563 foreach (GroupInfo group in groups)
1565 if (group.StartIndex <= index && group.StartIndex + group.Count > index)
1568 return group.GroupParent;
1578 public object GroupParent;
1579 public int StartIndex;
1581 public float GroupSize;
1582 public float GroupPosition;
1583 //Items relative position from the GroupPosition