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 /// This class implements a grid box layout.
26 [EditorBrowsable(EditorBrowsableState.Never)]
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"> ItemsView of layouter. </param>
52 [EditorBrowsable(EditorBrowsableState.Never)]
53 public override void Initialize(RecyclerView view)
55 colView = view as CollectionView;
58 throw new ArgumentException("GridLayouter only can be applied CollectionView.", nameof(view));
62 foreach (RecyclerViewItem item in VisibleItems)
64 colView.UnrealizeItem(item, false);
72 IsHorizontal = (colView.ScrollingDirection == ScrollableBase.Direction.Horizontal);
74 RecyclerViewItem header = colView?.Header;
75 RecyclerViewItem footer = colView?.Footer;
77 int count = colView.InternalItemSource.Count;
78 int pureCount = count - (header? 1 : 0) - (footer? 1 : 0);
80 // 2. Get the header / footer and size deligated item and measure the size.
83 MeasureChild(colView, header);
85 width = header.Layout != null? header.Layout.MeasuredWidth.Size.AsRoundedValue() : 0;
86 height = header.Layout != null? header.Layout.MeasuredHeight.Size.AsRoundedValue() : 0;
88 Extents itemMargin = header.Margin;
89 headerSize = IsHorizontal?
90 width + itemMargin.Start + itemMargin.End:
91 height + itemMargin.Top + itemMargin.Bottom;
92 headerMargin = new Extents(itemMargin);
95 colView.UnrealizeItem(header);
100 MeasureChild(colView, footer);
102 width = footer.Layout != null? footer.Layout.MeasuredWidth.Size.AsRoundedValue() : 0;
103 height = footer.Layout != null? footer.Layout.MeasuredHeight.Size.AsRoundedValue() : 0;
105 Extents itemMargin = footer.Margin;
106 footerSize = IsHorizontal?
107 width + itemMargin.Start + itemMargin.End:
108 height + itemMargin.Top + itemMargin.Bottom;
109 footerMargin = new Extents(itemMargin);
110 footer.Index = count - 1;
113 colView.UnrealizeItem(footer);
116 int firstIndex = header? 1 : 0;
118 if (colView.IsGrouped)
122 if (colView.GroupHeaderTemplate != null)
124 while (!colView.InternalItemSource.IsGroupHeader(firstIndex)) firstIndex++;
125 //must be always true
126 if (colView.InternalItemSource.IsGroupHeader(firstIndex))
128 RecyclerViewItem groupHeader = colView.RealizeItem(firstIndex);
131 if (groupHeader == null) throw new Exception("[" + firstIndex + "] Group Header failed to realize!");
133 // Need to Set proper height or width on scroll direction.
134 if (groupHeader.Layout == null)
136 width = groupHeader.WidthSpecification;
137 height = groupHeader.HeightSpecification;
141 MeasureChild(colView, groupHeader);
143 width = groupHeader.Layout.MeasuredWidth.Size.AsRoundedValue();
144 height = groupHeader.Layout.MeasuredHeight.Size.AsRoundedValue();
146 //Console.WriteLine("[NUI] GroupHeader Size {0} :{0}", width, height);
147 // pick the StepCandidate.
148 Extents itemMargin = groupHeader.Margin;
149 groupHeaderSize = IsHorizontal?
150 width + itemMargin.Start + itemMargin.End:
151 height + itemMargin.Top + itemMargin.Bottom;
152 groupHeaderMargin = new Extents(itemMargin);
153 colView.UnrealizeItem(groupHeader);
158 groupHeaderSize = 0F;
161 if (colView.GroupFooterTemplate != null)
163 int firstFooter = firstIndex;
164 while (!colView.InternalItemSource.IsGroupFooter(firstFooter)) firstFooter++;
165 //must be always true
166 if (colView.InternalItemSource.IsGroupFooter(firstFooter))
168 RecyclerViewItem groupFooter = colView.RealizeItem(firstFooter);
170 if (groupFooter == null) throw new Exception("[" + firstFooter + "] Group Footer failed to realize!");
171 // Need to Set proper height or width on scroll direction.
172 if (groupFooter.Layout == null)
174 width = groupFooter.WidthSpecification;
175 height = groupFooter.HeightSpecification;
179 MeasureChild(colView, groupFooter);
181 width = groupFooter.Layout.MeasuredWidth.Size.AsRoundedValue();
182 height = groupFooter.Layout.MeasuredHeight.Size.AsRoundedValue();
184 // pick the StepCandidate.
185 Extents itemMargin = groupFooter.Margin;
186 groupFooterSize = IsHorizontal?
187 width + itemMargin.Start + itemMargin.End:
188 height + itemMargin.Top + itemMargin.Bottom;
189 groupFooterMargin = new Extents(itemMargin);
191 colView.UnrealizeItem(groupFooter);
196 groupFooterSize = 0F;
199 else isGrouped = false;
202 //Final Check of FirstIndex
203 while (colView.InternalItemSource.IsHeader(firstIndex) ||
204 colView.InternalItemSource.IsGroupHeader(firstIndex) ||
205 colView.InternalItemSource.IsGroupFooter(firstIndex))
207 if (colView.InternalItemSource.IsFooter(firstIndex))
216 sizeCandidate = (0, 0);
219 // Get Size Deligate. FIXME if group exist index must be changed.
220 RecyclerViewItem sizeDeligate = colView.RealizeItem(firstIndex);
221 if (sizeDeligate == null)
223 throw new Exception("Cannot create content from DatTemplate.");
225 sizeDeligate.BindingContext = colView.InternalItemSource.GetItem(firstIndex);
227 // Need to Set proper height or width on scroll direction.
228 if (sizeDeligate.Layout == null)
230 width = sizeDeligate.WidthSpecification;
231 height = sizeDeligate.HeightSpecification;
235 MeasureChild(colView, sizeDeligate);
237 width = sizeDeligate.Layout.MeasuredWidth.Size.AsRoundedValue();
238 height = sizeDeligate.Layout.MeasuredHeight.Size.AsRoundedValue();
240 //Console.WriteLine("[NUI] item Size {0} :{1}", width, height);
242 // pick the StepCandidate.
243 Extents itemMargin = sizeDeligate.Margin;
244 width = width + itemMargin.Start + itemMargin.End;
245 height = height + itemMargin.Top + itemMargin.Bottom;
246 StepCandidate = IsHorizontal? width : height;
247 CandidateMargin = new Extents(itemMargin);
248 spanSize = IsHorizontal?
249 Convert.ToInt32(Math.Truncate((double)((colView.Size.Height - Padding.Top - Padding.Bottom) / height))) :
250 Convert.ToInt32(Math.Truncate((double)((colView.Size.Width - Padding.Start - Padding.End) / width)));
252 sizeCandidate = (width, height);
254 colView.UnrealizeItem(sizeDeligate);
257 if (StepCandidate < 1) StepCandidate = 1;
258 if (spanSize < 1) spanSize = 1;
262 float Current = 0.0F;
263 IGroupableItemSource source = colView.InternalItemSource;
264 GroupInfo currentGroup = null;
266 for (int i = 0; i < count; i++)
268 if (i == 0 && hasHeader)
270 Current += headerSize;
272 else if (i == count - 1 && hasFooter)
274 Current += footerSize;
278 //GroupHeader must always exist in group usage.
279 if (source.IsGroupHeader(i))
281 currentGroup = new GroupInfo()
283 GroupParent = source.GetGroupParent(i),
286 GroupSize = groupHeaderSize,
287 GroupPosition = Current
289 groups.Add(currentGroup);
290 Current += groupHeaderSize;
293 else if (source.IsGroupFooter(i))
295 //currentGroup.hasFooter = true;
296 currentGroup.Count++;
297 currentGroup.GroupSize += groupFooterSize;
298 Current += groupFooterSize;
302 currentGroup.Count++;
303 int index = i - currentGroup.StartIndex - 1; // groupHeader must always exist.
304 if ((index % spanSize) == 0)
306 currentGroup.GroupSize += StepCandidate;
307 Current += StepCandidate;
312 ScrollContentSize = Current;
316 // 3. Measure the scroller content size.
317 ScrollContentSize = StepCandidate * Convert.ToInt32(Math.Ceiling((double)pureCount / (double)spanSize));
318 if (hasHeader) ScrollContentSize += headerSize;
319 if (hasFooter) ScrollContentSize += footerSize;
322 ScrollContentSize = IsHorizontal?
323 ScrollContentSize + Padding.Start + Padding.End:
324 ScrollContentSize + Padding.Top + Padding.Bottom;
326 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
327 else colView.ContentContainer.SizeHeight = ScrollContentSize;
329 base.Initialize(colView);
330 //Console.WriteLine("Init Done, StepCnadidate{0}, spanSize{1}, Scroll{2}", StepCandidate, spanSize, ScrollContentSize);
334 /// This is called to find out where items are lain out according to current scroll position.
336 /// <param name="scrollPosition">Scroll position which is calculated by ScrollableBase</param>
337 /// <param name="force">boolean force flag to layouting forcely.</param>
338 public override void RequestLayout(float scrollPosition, bool force = false)
340 // Layouting is only possible after once it intialized.
341 if (!IsInitialized) return;
342 int LastIndex = colView.InternalItemSource.Count;
344 if (!force && PrevScrollPosition == Math.Abs(scrollPosition)) return;
345 PrevScrollPosition = Math.Abs(scrollPosition);
347 int prevFirstVisible = FirstVisible;
348 int prevLastVisible = LastVisible;
349 bool IsHorizontal = (colView.ScrollingDirection == ScrollableBase.Direction.Horizontal);
351 (float X, float Y) visibleArea = (PrevScrollPosition,
352 PrevScrollPosition + (IsHorizontal? colView.Size.Width : colView.Size.Height)
355 //Console.WriteLine("[NUI] itemsView [{0},{1}] [{2},{3}]", colView.Size.Width, colView.Size.Height, colView.ContentContainer.Size.Width, colView.ContentContainer.Size.Height);
357 // 1. Set First/Last Visible Item Index.
358 (int start, int end) = FindVisibleItems(visibleArea);
359 FirstVisible = start;
362 //Console.WriteLine("[NUI] {0} :visibleArea before [{1},{2}] after [{3},{4}]", scrollPosition, prevFirstVisible, prevLastVisible, FirstVisible, LastVisible);
364 // 2. Unrealize invisible items.
365 List<RecyclerViewItem> unrealizedItems = new List<RecyclerViewItem>();
366 foreach (RecyclerViewItem item in VisibleItems)
368 if (item.Index < FirstVisible || item.Index > LastVisible)
370 //Console.WriteLine("[NUI] Unrealize{0}!", item.Index);
371 unrealizedItems.Add(item);
372 colView.UnrealizeItem(item);
375 VisibleItems.RemoveAll(unrealizedItems.Contains);
377 //Console.WriteLine("Realize Begin [{0} to {1}]", FirstVisible, LastVisible);
378 // 3. Realize and placing visible items.
379 for (int i = FirstVisible; i <= LastVisible; i++)
381 //Console.WriteLine("[NUI] Realize!");
382 RecyclerViewItem item = null;
383 // 4. Get item if visible or realize new.
384 if (i >= prevFirstVisible && i <= prevLastVisible)
386 item = GetVisibleItem(i);
387 if (item != null && !force) continue;
391 item = colView.RealizeItem(i);
392 if (item != null) VisibleItems.Add(item);
393 else throw new Exception("Failed to create RecycerViewItem index of ["+ i + "]");
396 //item Position without Padding and Margin.
397 (float x, float y) = GetItemPosition(i);
398 // 5. Placing item with Padding and Margin.
399 item.Position = new Position(x, y);
401 //Linear Item need to be resized!
402 if (item.IsHeader || item.IsFooter || item.isGroupHeader || item.isGroupFooter)
404 if (IsHorizontal && item.HeightSpecification == LayoutParamPolicies.MatchParent)
406 item.Size = new Size(item.Size.Width, Container.Size.Height - Padding.Top - Padding.Bottom - item.Margin.Top - item.Margin.Bottom);
408 else if (!IsHorizontal && item.WidthSpecification == LayoutParamPolicies.MatchParent)
410 item.Size = new Size(Container.Size.Width - Padding.Start - Padding.End - item.Margin.Start - item.Margin.End, item.Size.Height);
413 //Console.WriteLine("[NUI] ["+item.Index+"] ["+item.Position.X+", "+item.Position.Y+" ==== \n");
415 //Console.WriteLine("Realize Done");
419 /// Clear the current screen and all properties.
421 [EditorBrowsable(EditorBrowsableState.Never)]
422 public override void Clear()
425 if (requestLayoutTimer != null)
427 requestLayoutTimer.Dispose();
432 foreach (GroupInfo group in groups)
434 //group.ItemPosition?.Clear();
441 if (headerMargin != null)
443 headerMargin.Dispose();
446 if (footerMargin != null)
448 footerMargin.Dispose();
451 if (groupHeaderMargin != null)
453 groupHeaderMargin.Dispose();
454 groupHeaderMargin = null;
456 if (groupFooterMargin != null)
458 groupFooterMargin.Dispose();
459 groupFooterMargin = null;
466 public override void NotifyItemSizeChanged(RecyclerViewItem item)
468 // All Item size need to be same in grid!
469 // if you want to change item size, change dataTemplate to re-initing.
474 [EditorBrowsable(EditorBrowsableState.Never)]
475 public override void NotifyItemInserted(IItemSource source, int startIndex)
477 // Insert Single item.
478 if (source == null) throw new ArgumentNullException(nameof(source));
480 // Will be null if not a group.
481 float currentSize = 0;
482 IGroupableItemSource gSource = source as IGroupableItemSource;
484 // Get the first Visible Position to adjust.
486 int topInScreenIndex = 0;
488 (topInScreenIndex, offset) = FindTopItemInScreen();
491 //2. Handle Group Case.
492 if (isGrouped && gSource != null)
494 GroupInfo groupInfo = null;
495 object groupParent = gSource.GetGroupParent(startIndex);
496 int parentIndex = gSource.GetPosition(groupParent);
497 if (gSource.HasHeader) parentIndex--;
499 // Check item is group parent or not
500 // if group parent, add new gorupinfo
501 if (gSource.IsHeader(startIndex))
503 // This is childless group.
504 // create new groupInfo!
505 groupInfo = new GroupInfo()
507 GroupParent = groupParent,
508 StartIndex = startIndex,
510 GroupSize = groupHeaderSize,
513 if (parentIndex >= groups.Count)
515 groupInfo.GroupPosition = ScrollContentSize;
516 groups.Add(groupInfo);
520 groupInfo.GroupPosition = groups[parentIndex].GroupPosition;
521 groups.Insert(parentIndex, groupInfo);
524 currentSize = groupHeaderSize;
528 // If not group parent, add item into the groupinfo.
529 if (parentIndex >= groups.Count) throw new Exception("group parent is bigger than group counts.");
530 groupInfo = groups[parentIndex];//GetGroupInfo(groupParent);
531 if (groupInfo == null) throw new Exception("Cannot find group information!");
533 if (gSource.IsGroupFooter(startIndex))
535 // It doesn't make sence to adding footer by notify...
536 // if GroupFooterTemplate is added,
537 // need to implement on here.
541 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
543 // Wrong! Grid Layouter do not support MeasureAll!
547 int pureCount = groupInfo.Count - 1 - (colView.GroupFooterTemplate == null? 0: 1);
548 if (pureCount % spanSize == 0)
550 currentSize = StepCandidate;
551 groupInfo.GroupSize += currentSize;
560 if (parentIndex + 1 < groups.Count)
562 for(int i = parentIndex + 1; i < groups.Count; i++)
564 groups[i].GroupPosition += currentSize;
565 groups[i].StartIndex++;
571 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
573 // Wrong! Grid Layouter do not support MeasureAll!
575 int pureCount = colView.InternalItemSource.Count - (hasHeader? 1: 0) - (hasFooter? 1: 0);
577 // Count comes after updated in ungrouped case!
578 if (pureCount % spanSize == 1)
580 currentSize = StepCandidate;
584 // 3. Update Scroll Content Size
585 ScrollContentSize += currentSize;
587 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
588 else colView.ContentContainer.SizeHeight = ScrollContentSize;
590 // 4. Update Visible Items.
591 foreach (RecyclerViewItem item in VisibleItems)
593 if (item.Index >= startIndex)
599 float scrollPosition = PrevScrollPosition;
603 // Insertion above Top Visible!
604 if (startIndex <= topInScreenIndex)
606 scrollPosition = GetItemPosition(topInScreenIndex);
607 scrollPosition -= offset;
609 colView.ScrollTo(scrollPosition);
613 // Update Viewport in delay.
614 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
615 // but currently we do not have any accessor to pre-calculation so instead of this,
616 // using Timer temporarily.
617 DelayedRequestLayout(scrollPosition);
621 [EditorBrowsable(EditorBrowsableState.Never)]
622 public override void NotifyItemRangeInserted(IItemSource source, int startIndex, int count)
625 if (source == null) throw new ArgumentNullException(nameof(source));
627 float currentSize = 0;
628 // Will be null if not a group.
629 IGroupableItemSource gSource = source as IGroupableItemSource;
631 // Get the first Visible Position to adjust.
633 int topInScreenIndex = 0;
635 (topInScreenIndex, offset) = FindTopItemInScreen();
638 // 2. Handle Group Case
639 // Adding ranged items should all same new groups.
640 if (isGrouped && gSource != null)
642 GroupInfo groupInfo = null;
643 object groupParent = gSource.GetGroupParent(startIndex);
644 int parentIndex = gSource.GetPosition(groupParent);
645 if (gSource.HasHeader) parentIndex--;
646 int groupStartIndex = 0;
647 if (gSource.IsGroupHeader(startIndex))
649 groupStartIndex = startIndex;
654 throw new Exception("Inserted wrong groups!");
657 for (int current = startIndex; current - startIndex < count; current++)
659 // Check item is group parent or not
660 // if group parent, add new gorupinfo
661 if (groupStartIndex == current)
663 //create new groupInfo!
664 groupInfo = new GroupInfo()
666 GroupParent = groupParent,
667 StartIndex = current,
669 GroupSize = groupHeaderSize,
671 currentSize += groupHeaderSize;
676 //if not group parent, add item into the groupinfo.
677 //groupInfo = GetGroupInfo(groupStartIndex);
678 if (groupInfo == null) throw new Exception("Cannot find group information!");
681 if (gSource.IsGroupFooter(current))
683 groupInfo.GroupSize += groupFooterSize;
684 currentSize += groupFooterSize;
688 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
690 // Wrong! Grid Layouter do not support MeasureAll!
694 int index = current - groupInfo.StartIndex - 1; // groupHeader must always exist.
695 if ((index % spanSize) == 0)
697 groupInfo.GroupSize += StepCandidate;
698 currentSize += StepCandidate;
705 if (parentIndex >= groups.Count)
707 groupInfo.GroupPosition = ScrollContentSize;
708 groups.Add(groupInfo);
712 groupInfo.GroupPosition = groups[parentIndex].GroupPosition;
713 groups.Insert(parentIndex, groupInfo);
716 // Update other below group's position
717 if (parentIndex + 1 < groups.Count)
719 for(int i = parentIndex + 1; i < groups.Count; i++)
721 groups[i].GroupPosition += currentSize;
722 groups[i].StartIndex += count;
726 ScrollContentSize += currentSize;
730 throw new Exception("Cannot insert ungrouped range items!");
733 // 3. Update Scroll Content Size
734 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
735 else colView.ContentContainer.SizeHeight = ScrollContentSize;
737 // 4. Update Visible Items.
738 foreach (RecyclerViewItem item in VisibleItems)
740 if (item.Index >= startIndex)
747 float scrollPosition = PrevScrollPosition;
749 // Insertion above Top Visible!
750 if (startIndex + count <= topInScreenIndex)
752 scrollPosition = GetItemPosition(topInScreenIndex);
753 scrollPosition -= offset;
755 colView.ScrollTo(scrollPosition);
759 // Update Viewport in delay.
760 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
761 // but currently we do not have any accessor to pre-calculation so instead of this,
762 // using Timer temporarily.
763 DelayedRequestLayout(scrollPosition);
767 [EditorBrowsable(EditorBrowsableState.Never)]
768 public override void NotifyItemRemoved(IItemSource source, int startIndex)
771 if (source == null) throw new ArgumentNullException(nameof(source));
773 // Will be null if not a group.
774 float currentSize = 0;
775 IGroupableItemSource gSource = source as IGroupableItemSource;
777 // Get the first Visible Position to adjust.
779 int topInScreenIndex = 0;
781 (topInScreenIndex, offset) = FindTopItemInScreen();
784 // 2. Handle Group Case
785 if (isGrouped && gSource != null)
788 GroupInfo groupInfo = null;
789 foreach(GroupInfo cur in groups)
791 if ((cur.StartIndex <= startIndex) && (cur.StartIndex + cur.Count - 1 >= startIndex))
798 if (groupInfo == null) throw new Exception("Cannot find group information!");
799 // Check item is group parent or not
800 // if group parent, add new gorupinfo
801 if (groupInfo.StartIndex == startIndex)
803 // This is empty group!
804 // check group is empty.
805 if (groupInfo.Count != 1)
807 throw new Exception("Cannot remove group parent");
809 currentSize = groupInfo.GroupSize;
812 // groupInfo.Dispose();
813 groups.Remove(groupInfo);
819 // Skip footer case as footer cannot exist alone without header.
820 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
822 // Wrong! Grid Layouter do not support MeasureAll!
826 int pureCount = groupInfo.Count - 1 - (colView.GroupFooterTemplate == null? 0: 1);
827 if (pureCount % spanSize == 0)
829 currentSize = StepCandidate;
830 groupInfo.GroupSize -= currentSize;
835 for (int i = parentIndex + 1; i < groups.Count; i++)
837 groups[i].GroupPosition -= currentSize;
838 groups[i].StartIndex--;
843 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
845 // Wrong! Grid Layouter do not support MeasureAll!
847 int pureCount = colView.InternalItemSource.Count - (hasHeader? 1: 0) - (hasFooter? 1: 0);
849 // Count comes after updated in ungrouped case!
850 if (pureCount % spanSize == 0)
852 currentSize = StepCandidate;
857 ScrollContentSize -= currentSize;
859 // 3. Update Scroll Content Size
860 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
861 else colView.ContentContainer.SizeHeight = ScrollContentSize;
863 // 4. Update Visible Items.
864 RecyclerViewItem targetItem = null;
865 foreach (RecyclerViewItem item in VisibleItems)
867 if (item.Index == startIndex)
870 colView.UnrealizeItem(item);
872 else if (item.Index > startIndex)
877 VisibleItems.Remove(targetItem);
880 float scrollPosition = PrevScrollPosition;
882 // Insertion above Top Visible!
883 if (startIndex <= topInScreenIndex)
885 scrollPosition = GetItemPosition(topInScreenIndex);
886 scrollPosition -= offset;
888 colView.ScrollTo(scrollPosition);
892 // Update Viewport in delay.
893 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
894 // but currently we do not have any accessor to pre-calculation so instead of this,
895 // using Timer temporarily.
896 DelayedRequestLayout(scrollPosition);
900 [EditorBrowsable(EditorBrowsableState.Never)]
901 public override void NotifyItemRangeRemoved(IItemSource source, int startIndex, int count)
904 if (source == null) throw new ArgumentNullException(nameof(source));
906 // Will be null if not a group.
907 float currentSize = StepCandidate;
908 IGroupableItemSource gSource = source as IGroupableItemSource;
910 // Get the first Visible Position to adjust.
912 int topInScreenIndex = 0;
914 (topInScreenIndex, offset) = FindTopItemInScreen();
917 // 1. Handle Group Case
918 if (isGrouped && gSource != null)
921 GroupInfo groupInfo = null;
922 foreach(GroupInfo cur in groups)
924 if ((cur.StartIndex == startIndex) && (cur.Count == count))
931 if (groupInfo == null) throw new Exception("Cannot find group information!");
932 // Check item is group parent or not
933 // if group parent, add new gorupinfo
934 currentSize = groupInfo.GroupSize;
935 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
937 // Wrong! Grid Layouter do not support MeasureAll!
940 // groupInfo.Dispose();
941 groups.Remove(groupInfo);
943 for (int i = parentIndex; i < groups.Count; i++)
945 groups[i].GroupPosition -= currentSize;
946 groups[i].StartIndex -= count;
951 // It must group case! throw exception!
952 throw new Exception("Range remove must group remove!");
955 ScrollContentSize -= currentSize;
957 // 2. Update Scroll Content Size
958 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
959 else colView.ContentContainer.SizeHeight = ScrollContentSize;
961 // 3. Update Visible Items.
962 List<RecyclerViewItem> unrealizedItems = new List<RecyclerViewItem>();
963 foreach (RecyclerViewItem item in VisibleItems)
965 if ((item.Index >= startIndex)
966 && (item.Index < startIndex + count))
968 unrealizedItems.Add(item);
969 colView.UnrealizeItem(item);
971 else if (item.Index >= startIndex + count)
976 VisibleItems.RemoveAll(unrealizedItems.Contains);
977 unrealizedItems.Clear();
980 float scrollPosition = PrevScrollPosition;
982 // Insertion above Top Visible!
983 if (startIndex <= topInScreenIndex)
985 scrollPosition = GetItemPosition(topInScreenIndex);
986 scrollPosition -= offset;
988 colView.ScrollTo(scrollPosition);
992 // Update Viewport in delay.
993 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
994 // but currently we do not have any accessor to pre-calculation so instead of this,
995 // using Timer temporarily.
996 DelayedRequestLayout(scrollPosition);
1000 [EditorBrowsable(EditorBrowsableState.Never)]
1001 public override void NotifyItemMoved(IItemSource source, int fromPosition, int toPosition)
1004 if (source == null) throw new ArgumentNullException(nameof(source));
1006 // Will be null if not a group.
1007 float currentSize = StepCandidate;
1008 int diff = toPosition - fromPosition;
1010 // Get the first Visible Position to adjust.
1012 int topInScreenIndex = 0;
1014 (topInScreenIndex, offset) = FindTopItemInScreen();
1017 // Move can only happen in it's own groups.
1018 // so there will be no changes in position, startIndex in ohter groups.
1019 // check visible item and update indexs.
1020 int startIndex = ( diff > 0 ? fromPosition: toPosition);
1021 int endIndex = (diff > 0 ? toPosition: fromPosition);
1023 if ((endIndex >= FirstVisible) && (startIndex <= LastVisible))
1025 foreach (RecyclerViewItem item in VisibleItems)
1027 if ((item.Index >= startIndex)
1028 && (item.Index <= endIndex))
1030 if (item.Index == fromPosition) item.Index = toPosition;
1033 if (diff > 0) item.Index--;
1040 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
1041 else colView.ContentContainer.SizeHeight = ScrollContentSize;
1044 float scrollPosition = PrevScrollPosition;
1046 // Insertion above Top Visible!
1047 if (((fromPosition > topInScreenIndex) && (toPosition < topInScreenIndex) ||
1048 ((fromPosition < topInScreenIndex) && (toPosition > topInScreenIndex)))
1050 scrollPosition = GetItemPosition(topInScreenIndex);
1051 scrollPosition -= offset;
1053 colView.ScrollTo(scrollPosition);
1057 // Update Viewport in delay.
1058 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
1059 // but currently we do not have any accessor to pre-calculation so instead of this,
1060 // using Timer temporarily.
1061 DelayedRequestLayout(scrollPosition);
1065 [EditorBrowsable(EditorBrowsableState.Never)]
1066 public override void NotifyItemRangeMoved(IItemSource source, int fromPosition, int toPosition, int count)
1069 if (source == null) throw new ArgumentNullException(nameof(source));
1071 // Will be null if not a group.
1072 float currentSize = StepCandidate;
1073 int diff = toPosition - fromPosition;
1075 int startIndex = ( diff > 0 ? fromPosition: toPosition);
1076 int endIndex = (diff > 0 ? toPosition + count - 1: fromPosition + count - 1);
1078 // 2. Handle Group Case
1081 int fromParentIndex = 0;
1082 int toParentIndex = 0;
1083 bool findFrom = false;
1084 bool findTo = false;
1085 GroupInfo fromGroup = null;
1086 GroupInfo toGroup = null;
1088 foreach(GroupInfo cur in groups)
1090 if ((cur.StartIndex == fromPosition) && (cur.Count == count))
1094 if (findFrom && findTo) break;
1096 else if (cur.StartIndex == toPosition)
1100 if (findFrom && findTo) break;
1102 if (!findFrom) fromParentIndex++;
1103 if (!findTo) toParentIndex++;
1105 if (toGroup == null || fromGroup == null) throw new Exception("Cannot find group information!");
1107 fromGroup.StartIndex = toGroup.StartIndex;
1108 fromGroup.GroupPosition = toGroup.GroupPosition;
1110 endIndex = (diff > 0 ? toPosition + toGroup.Count - 1: fromPosition + count - 1);
1112 groups.Remove(fromGroup);
1113 groups.Insert(toParentIndex, fromGroup);
1115 int startGroup = (diff > 0? fromParentIndex: toParentIndex);
1116 int endGroup = (diff > 0? toParentIndex: fromParentIndex);
1118 for (int i = startGroup; i <= endGroup; i++)
1120 if (i == toParentIndex) continue;
1121 float prevPos = groups[i].GroupPosition;
1122 int prevIdx = groups[i].StartIndex;
1123 groups[i].GroupPosition = groups[i].GroupPosition + (diff > 0? -1 : 1) * fromGroup.GroupSize;
1124 groups[i].StartIndex = groups[i].StartIndex + (diff > 0? -1 : 1) * fromGroup.Count;
1129 //It must group case! throw exception!
1130 throw new Exception("Range remove must group remove!");
1133 // Move can only happen in it's own groups.
1134 // so there will be no changes in position, startIndex in ohter groups.
1135 // check visible item and update indexs.
1136 if ((endIndex >= FirstVisible) && (startIndex <= LastVisible))
1138 foreach (RecyclerViewItem item in VisibleItems)
1140 if ((item.Index >= startIndex)
1141 && (item.Index <= endIndex))
1143 if ((item.Index >= fromPosition) && (item.Index < fromPosition + count))
1145 item.Index = fromPosition - item.Index + toPosition;
1149 if (diff > 0) item.Index -= count;
1150 else item.Index += count;
1157 float scrollPosition = PrevScrollPosition;
1159 // Insertion above Top Visible!
1160 if (((fromPosition > topInScreenIndex) && (toPosition < topInScreenIndex) ||
1161 ((fromPosition < topInScreenIndex) && (toPosition > topInScreenIndex)))
1163 scrollPosition = GetItemPosition(topInScreenIndex);
1164 scrollPosition -= offset;
1166 colView.ScrollTo(scrollPosition);
1170 // Update Viewport in delay.
1171 // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop.
1172 // but currently we do not have any accessor to pre-calculation so instead of this,
1173 // using Timer temporarily.
1174 DelayedRequestLayout(scrollPosition);
1178 [EditorBrowsable(EditorBrowsableState.Never)]
1179 public override float CalculateLayoutOrientationSize()
1181 //Console.WriteLine("[NUI] Calculate Layout ScrollContentSize {0}", ScrollContentSize);
1182 return ScrollContentSize;
1186 [EditorBrowsable(EditorBrowsableState.Never)]
1187 public override float CalculateCandidateScrollPosition(float scrollPosition)
1189 //Console.WriteLine("[NUI] Calculate Candidate ScrollContentSize {0}", ScrollContentSize);
1190 return scrollPosition;
1194 [EditorBrowsable(EditorBrowsableState.Never)]
1195 public override View RequestNextFocusableView(View currentFocusedView, View.FocusDirection direction, bool loopEnabled)
1197 if (currentFocusedView == null)
1198 throw new ArgumentNullException(nameof(currentFocusedView));
1200 View nextFocusedView = null;
1201 int targetSibling = -1;
1202 bool IsHorizontal = colView.ScrollingDirection == ScrollableBase.Direction.Horizontal;
1206 case View.FocusDirection.Left:
1208 targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder - 1 : targetSibling;
1211 case View.FocusDirection.Right:
1213 targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder + 1 : targetSibling;
1216 case View.FocusDirection.Up:
1218 targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder - 1;
1221 case View.FocusDirection.Down:
1223 targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder + 1;
1228 if (targetSibling > -1 && targetSibling < Container.Children.Count)
1230 RecyclerViewItem candidate = Container.Children[targetSibling] as RecyclerViewItem;
1231 if (candidate.Index >= 0 && candidate.Index < colView.InternalItemSource.Count)
1233 nextFocusedView = candidate;
1236 return nextFocusedView;
1240 [EditorBrowsable(EditorBrowsableState.Never)]
1241 protected override (int start, int end) FindVisibleItems((float X, float Y) visibleArea)
1243 int MaxIndex = colView.InternalItemSource.Count - 1 - (hasFooter ? 1 : 0);
1244 int adds = spanSize * 2;
1246 (int start, int end) found = (0, 0);
1248 // Header is Showing
1249 if (hasHeader && visibleArea.X < headerSize + (IsHorizontal? Padding.Start : Padding.Top))
1258 foreach (GroupInfo gInfo in groups)
1262 if (gInfo.GroupPosition <= visibleArea.X &&
1263 gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.X)
1265 if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.X)
1267 found.start = gInfo.StartIndex - adds;
1270 //can be step in spanSize...
1271 for (int i = 1; i < gInfo.Count; i++)
1274 // Reach last index of group.
1275 if (i == (gInfo.Count - 1))
1277 found.start = gInfo.StartIndex + i - adds;
1282 else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.X - gInfo.GroupPosition - groupHeaderSize)
1284 found.start = gInfo.StartIndex + i - adds;
1291 //footer only shows?
1294 found.start = MaxIndex;
1299 float visibleAreaX = visibleArea.X - (hasHeader ? headerSize : 0);
1300 found.start = (Convert.ToInt32(Math.Abs(visibleAreaX / StepCandidate)) - 1) * spanSize;
1301 if (hasHeader) found.start += 1;
1303 if (found.start < 0) found.start = 0;
1306 if (hasFooter && visibleArea.Y > ScrollContentSize - footerSize - (IsHorizontal? Padding.End : Padding.Bottom))
1308 found.end = MaxIndex + 1;
1315 // can it be start from founded group...?
1316 //foreach(GroupInfo gInfo in groups.Skip(skipGroup))
1317 foreach (GroupInfo gInfo in groups)
1320 if (gInfo.GroupPosition <= visibleArea.Y &&
1321 gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.Y)
1323 if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.Y)
1325 found.end = gInfo.StartIndex + adds;
1328 //can be step in spanSize...
1329 for (int i = 1; i < gInfo.Count; i++)
1332 // Reach last index of group.
1333 if (i == (gInfo.Count - 1))
1335 found.end = gInfo.StartIndex + i + adds;
1339 else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.Y - gInfo.GroupPosition - groupHeaderSize)
1341 found.end = gInfo.StartIndex + i + adds;
1348 //footer only shows?
1351 found.start = MaxIndex;
1356 float visibleAreaY = visibleArea.Y - (hasHeader ? headerSize : 0);
1357 //Need to Consider GroupHeight!!!!
1358 found.end = (Convert.ToInt32(Math.Abs(visibleAreaY / StepCandidate)) + 1) * spanSize + adds;
1359 if (hasHeader) found.end += 1;
1361 if (found.end > (MaxIndex)) found.end = MaxIndex;
1366 internal override (float X, float Y) GetItemPosition(int index)
1369 int spaceStartX = Padding.Start;
1370 int spaceStartY = Padding.Top;
1371 int emptyArea = IsHorizontal?
1372 (int)(colView.Size.Height - Padding.Top - Padding.Bottom - (sizeCandidate.Height * spanSize)) :
1373 (int)(colView.Size.Width - Padding.Start - Padding.End - (sizeCandidate.Width * spanSize));
1375 if (hasHeader && index == 0)
1377 return (spaceStartX + headerMargin.Start, spaceStartY + headerMargin.Top);
1379 if (hasFooter && index == colView.InternalItemSource.Count - 1)
1381 xPos = IsHorizontal?
1382 ScrollContentSize - Padding.End - footerSize + footerMargin.Start:
1384 yPos = IsHorizontal?
1386 ScrollContentSize - Padding.Bottom - footerSize + footerMargin.Top;
1387 return (xPos, yPos);
1391 GroupInfo myGroup = GetGroupInfo(index);
1392 if (colView.InternalItemSource.IsGroupHeader(index))
1394 spaceStartX+= groupHeaderMargin.Start;
1395 spaceStartY+= groupHeaderMargin.Top;
1396 xPos = IsHorizontal?
1397 myGroup.GroupPosition + groupHeaderMargin.Start:
1399 yPos = IsHorizontal?
1401 myGroup.GroupPosition + groupHeaderMargin.Top;
1403 else if (colView.InternalItemSource.IsGroupFooter(index))
1405 spaceStartX+= groupFooterMargin.Start;
1406 spaceStartY+= groupFooterMargin.Top;
1407 xPos = IsHorizontal?
1408 myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize + groupFooterMargin.Start:
1410 yPos = IsHorizontal?
1412 myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize + groupFooterMargin.Top;
1416 int pureIndex = index - myGroup.StartIndex - 1;
1417 int division = pureIndex / spanSize;
1418 int remainder = pureIndex % spanSize;
1419 if (division < 0) division = 0;
1420 if (remainder < 0) remainder = 0;
1421 spaceStartX+= CandidateMargin.Start;
1422 spaceStartY+= CandidateMargin.Top;
1424 xPos = IsHorizontal?
1425 (division * sizeCandidate.Width) + myGroup.GroupPosition + groupHeaderSize + CandidateMargin.Start:
1426 (emptyArea * align) + (remainder * sizeCandidate.Width) + spaceStartX;
1427 yPos = IsHorizontal?
1428 (emptyArea * align) + (remainder * sizeCandidate.Height) + spaceStartY:
1429 (division * sizeCandidate.Height) + myGroup.GroupPosition + groupHeaderSize + CandidateMargin.Top;
1434 int pureIndex = index - (colView.Header ? 1 : 0);
1435 // int convert must be truncate value.
1436 int division = pureIndex / spanSize;
1437 int remainder = pureIndex % spanSize;
1438 if (division < 0) division = 0;
1439 if (remainder < 0) remainder = 0;
1440 spaceStartX+= CandidateMargin.Start;
1441 spaceStartY+= CandidateMargin.Top;
1443 xPos = IsHorizontal?
1444 (division * sizeCandidate.Width) + (hasHeader? headerSize : 0) + spaceStartX:
1445 (emptyArea * align) + (remainder * sizeCandidate.Width) + spaceStartX;
1446 yPos = IsHorizontal?
1447 (emptyArea * align) + (remainder * sizeCandidate.Height) + spaceStartY:
1448 (division * sizeCandidate.Height) + (hasHeader? headerSize : 0) + spaceStartY;
1451 return (xPos, yPos);
1454 internal override (float Width, float Height) GetItemSize(int index)
1456 return (sizeCandidate.Width - CandidateMargin.Start - CandidateMargin.End,
1457 sizeCandidate.Height - CandidateMargin.Top - CandidateMargin.Bottom);
1459 private void DelayedRequestLayout(float scrollPosition , bool force = true)
1461 if (requestLayoutTimer != null)
1463 requestLayoutTimer.Dispose();
1466 requestLayoutTimer = new Timer(1);
1467 requestLayoutTimer.Interval = 1;
1468 requestLayoutTimer.Tick += ((object target, Timer.TickEventArgs args) =>
1470 RequestLayout(scrollPosition, force);
1475 private RecyclerViewItem GetVisibleItem(int index)
1477 foreach (RecyclerViewItem item in VisibleItems)
1479 if (item.Index == index) return item;
1485 private GroupInfo GetGroupInfo(int index)
1487 if (Visited != null)
1489 if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index)
1492 if (hasHeader && index == 0) return null;
1493 foreach (GroupInfo group in groups)
1495 if (group.StartIndex <= index && group.StartIndex + group.Count > index)
1506 private object GetGroupParent(int index)
1508 if (Visited != null)
1510 if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index)
1511 return Visited.GroupParent;
1513 if (hasHeader && index == 0) return null;
1514 foreach (GroupInfo group in groups)
1516 if (group.StartIndex <= index && group.StartIndex + group.Count > index)
1519 return group.GroupParent;
1529 public object GroupParent;
1530 public int StartIndex;
1532 public float GroupSize;
1533 public float GroupPosition;
1534 //Items relative position from the GroupPosition