f3aba65bbd50812ee925fef4efe262d2b07905e1
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI.Components / Controls / RecyclerView / Layouter / GridLayouter.cs
1 /* Copyright (c) 2021 Samsung Electronics Co., Ltd.
2  *
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
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
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.
14  *
15  */
16 using System;
17 using System.Collections.Generic;
18 using System.ComponentModel;
19 using Tizen.NUI.BaseComponents;
20
21 namespace Tizen.NUI.Components
22 {
23     /// <summary>
24     /// This class implements a grid box layout.
25     /// </summary>
26     [EditorBrowsable(EditorBrowsableState.Never)]
27     public class GridLayouter : ItemsLayouter
28     {
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
48         /// <summary>
49         /// Clean up ItemsLayouter.
50         /// </summary>
51         /// <param name="view"> ItemsView of layouter. </param>
52         [EditorBrowsable(EditorBrowsableState.Never)]
53         public override void Initialize(RecyclerView view)
54         {
55             colView = view as CollectionView;
56             if (colView == null)
57             {
58                 throw new ArgumentException("GridLayouter only can be applied CollectionView.", nameof(view));
59             }
60
61             // 1. Clean Up
62             foreach (RecyclerViewItem item in VisibleItems)
63             {
64                 colView.UnrealizeItem(item, false);
65             }
66             VisibleItems.Clear();
67             groups.Clear();
68
69             FirstVisible = 0;
70             LastVisible = 0;
71
72             IsHorizontal = (colView.ScrollingDirection == ScrollableBase.Direction.Horizontal);
73
74             RecyclerViewItem header = colView?.Header;
75             RecyclerViewItem footer = colView?.Footer;
76             float width, height;
77             int count = colView.InternalItemSource.Count;
78             int pureCount = count - (header? 1 : 0) - (footer? 1 : 0);
79
80             // 2. Get the header / footer and size deligated item and measure the size.
81             if (header != null)
82             {
83                 MeasureChild(colView, header);
84
85                 width = header.Layout != null? header.Layout.MeasuredWidth.Size.AsRoundedValue() : 0;
86                 height = header.Layout != null? header.Layout.MeasuredHeight.Size.AsRoundedValue() : 0;
87
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);
93                 hasHeader = true;
94
95                 colView.UnrealizeItem(header);
96             }
97
98             if (footer != null)
99             {
100                 MeasureChild(colView, footer);
101
102                 width = footer.Layout != null? footer.Layout.MeasuredWidth.Size.AsRoundedValue() : 0;
103                 height = footer.Layout != null? footer.Layout.MeasuredHeight.Size.AsRoundedValue() : 0;
104
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;
111                 hasFooter = true;
112
113                 colView.UnrealizeItem(footer);
114             }
115
116             int firstIndex = header? 1 : 0;
117
118             if (colView.IsGrouped)
119             {
120                 isGrouped = true;
121
122                 if (colView.GroupHeaderTemplate != null)
123                 {
124                     while (!colView.InternalItemSource.IsGroupHeader(firstIndex)) firstIndex++;
125                     //must be always true
126                     if (colView.InternalItemSource.IsGroupHeader(firstIndex))
127                     {
128                         RecyclerViewItem groupHeader = colView.RealizeItem(firstIndex);
129                         firstIndex++;
130
131                         if (groupHeader == null) throw new Exception("[" + firstIndex + "] Group Header failed to realize!");
132
133                         // Need to Set proper height or width on scroll direction.
134                         if (groupHeader.Layout == null)
135                         {
136                             width = groupHeader.WidthSpecification;
137                             height = groupHeader.HeightSpecification;
138                         }
139                         else
140                         {
141                             MeasureChild(colView, groupHeader);
142
143                             width = groupHeader.Layout.MeasuredWidth.Size.AsRoundedValue();
144                             height = groupHeader.Layout.MeasuredHeight.Size.AsRoundedValue();
145                         }
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);
154                     }
155                 }
156                 else
157                 {
158                     groupHeaderSize = 0F;
159                 }
160
161                 if (colView.GroupFooterTemplate != null)
162                 {
163                     int firstFooter = firstIndex;
164                     while (!colView.InternalItemSource.IsGroupFooter(firstFooter)) firstFooter++;
165                     //must be always true
166                     if (colView.InternalItemSource.IsGroupFooter(firstFooter))
167                     {
168                         RecyclerViewItem groupFooter = colView.RealizeItem(firstFooter);
169
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)
173                         {
174                             width = groupFooter.WidthSpecification;
175                             height = groupFooter.HeightSpecification;
176                         }
177                         else
178                         {
179                             MeasureChild(colView, groupFooter);
180
181                             width = groupFooter.Layout.MeasuredWidth.Size.AsRoundedValue();
182                             height = groupFooter.Layout.MeasuredHeight.Size.AsRoundedValue();
183                         }
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);
190                     
191                         colView.UnrealizeItem(groupFooter);
192                     }
193                 }
194                 else
195                 {
196                     groupFooterSize = 0F;
197                 }
198             }
199             else isGrouped = false;
200
201             bool failed = false;
202             //Final Check of FirstIndex
203             while (colView.InternalItemSource.IsHeader(firstIndex) ||
204                     colView.InternalItemSource.IsGroupHeader(firstIndex) ||
205                     colView.InternalItemSource.IsGroupFooter(firstIndex))
206             {
207                 if (colView.InternalItemSource.IsFooter(firstIndex))
208                 {
209                     StepCandidate = 0F;
210                     failed = true;
211                     break;
212                 }
213                 firstIndex++;
214             }
215
216             sizeCandidate = (0, 0);
217             if (!failed)
218             {
219                 // Get Size Deligate. FIXME if group exist index must be changed.
220                 RecyclerViewItem sizeDeligate = colView.RealizeItem(firstIndex);
221                 if (sizeDeligate == null)
222                 {
223                     throw new Exception("Cannot create content from DatTemplate.");
224                 }
225                 sizeDeligate.BindingContext = colView.InternalItemSource.GetItem(firstIndex);
226
227                 // Need to Set proper height or width on scroll direction.
228                 if (sizeDeligate.Layout == null)
229                 {
230                     width = sizeDeligate.WidthSpecification;
231                     height = sizeDeligate.HeightSpecification;
232                 }
233                 else
234                 {
235                     MeasureChild(colView, sizeDeligate);
236
237                     width = sizeDeligate.Layout.MeasuredWidth.Size.AsRoundedValue();
238                     height = sizeDeligate.Layout.MeasuredHeight.Size.AsRoundedValue();
239                 }
240                 //Console.WriteLine("[NUI] item Size {0} :{1}", width, height);
241
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)));
251
252                 sizeCandidate = (width, height);
253
254                 colView.UnrealizeItem(sizeDeligate);
255             }
256
257             if (StepCandidate < 1) StepCandidate = 1;
258             if (spanSize < 1) spanSize = 1;
259
260             if (isGrouped)
261             {
262                 float Current = 0.0F;
263                 IGroupableItemSource source = colView.InternalItemSource;
264                 GroupInfo currentGroup = null;
265
266                 for (int i = 0; i < count; i++)
267                 {
268                     if (i == 0 && hasHeader)
269                     {
270                         Current += headerSize;
271                     }
272                     else if (i == count - 1 && hasFooter)
273                     {
274                         Current += footerSize;
275                     }
276                     else
277                     {
278                         //GroupHeader must always exist in group usage.
279                         if (source.IsGroupHeader(i))
280                         {
281                             currentGroup = new GroupInfo()
282                             {
283                                 GroupParent = source.GetGroupParent(i),
284                                 StartIndex = i,
285                                 Count = 1,
286                                 GroupSize = groupHeaderSize,
287                                 GroupPosition = Current
288                             };
289                             groups.Add(currentGroup);
290                             Current += groupHeaderSize;
291                         }
292                         //optional
293                         else if (source.IsGroupFooter(i))
294                         {
295                             //currentGroup.hasFooter = true;
296                             currentGroup.Count++;
297                             currentGroup.GroupSize += groupFooterSize;
298                             Current += groupFooterSize;
299                         }
300                         else
301                         {
302                             currentGroup.Count++;
303                             int index = i - currentGroup.StartIndex - 1; // groupHeader must always exist.
304                             if ((index % spanSize) == 0)
305                             {
306                                 currentGroup.GroupSize += StepCandidate;
307                                 Current += StepCandidate;
308                             }
309                         }
310                     }
311                 }
312                 ScrollContentSize = Current;
313             }
314             else
315             {
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;
320             }
321
322             ScrollContentSize = IsHorizontal?
323                                 ScrollContentSize + Padding.Start + Padding.End:
324                                 ScrollContentSize + Padding.Top + Padding.Bottom;
325
326             if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
327             else colView.ContentContainer.SizeHeight = ScrollContentSize;
328
329             base.Initialize(colView);
330             //Console.WriteLine("Init Done, StepCnadidate{0}, spanSize{1}, Scroll{2}", StepCandidate, spanSize, ScrollContentSize);
331         }
332
333         /// <summary>
334         /// This is called to find out where items are lain out according to current scroll position.
335         /// </summary>
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)
339         {
340             // Layouting is only possible after once it intialized.
341             if (!IsInitialized) return;
342             int LastIndex = colView.InternalItemSource.Count;
343
344             if (!force && PrevScrollPosition == Math.Abs(scrollPosition)) return;
345             PrevScrollPosition = Math.Abs(scrollPosition);
346
347             int prevFirstVisible = FirstVisible;
348             int prevLastVisible = LastVisible;
349             bool IsHorizontal = (colView.ScrollingDirection == ScrollableBase.Direction.Horizontal);
350
351             (float X, float Y) visibleArea = (PrevScrollPosition,
352                 PrevScrollPosition + (IsHorizontal? colView.Size.Width : colView.Size.Height)
353             );
354
355             //Console.WriteLine("[NUI] itemsView [{0},{1}] [{2},{3}]", colView.Size.Width, colView.Size.Height, colView.ContentContainer.Size.Width, colView.ContentContainer.Size.Height);
356
357             // 1. Set First/Last Visible Item Index. 
358             (int start, int end) = FindVisibleItems(visibleArea);
359             FirstVisible = start;
360             LastVisible = end;
361
362             //Console.WriteLine("[NUI] {0} :visibleArea before [{1},{2}] after [{3},{4}]", scrollPosition, prevFirstVisible, prevLastVisible, FirstVisible, LastVisible);
363
364             // 2. Unrealize invisible items.
365             List<RecyclerViewItem> unrealizedItems = new List<RecyclerViewItem>();
366             foreach (RecyclerViewItem item in VisibleItems)
367             {
368                 if (item.Index < FirstVisible || item.Index > LastVisible)
369                 {
370                     //Console.WriteLine("[NUI] Unrealize{0}!", item.Index);
371                     unrealizedItems.Add(item);
372                     colView.UnrealizeItem(item);
373                 }
374             }
375             VisibleItems.RemoveAll(unrealizedItems.Contains);
376
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++)
380             {
381                 //Console.WriteLine("[NUI] Realize!");
382                 RecyclerViewItem item = null;
383                 // 4. Get item if visible or realize new.
384                 if (i >= prevFirstVisible && i <= prevLastVisible)
385                 {
386                     item = GetVisibleItem(i);
387                     if (item != null && !force) continue;
388                 }
389                 if (item == null)
390                 {
391                     item = colView.RealizeItem(i);
392                     if (item != null) VisibleItems.Add(item);
393                     else throw new Exception("Failed to create RecycerViewItem index of ["+ i + "]");
394                 }
395
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);
400                 
401                 //Linear Item need to be resized!
402                 if (item.IsHeader || item.IsFooter || item.isGroupHeader || item.isGroupFooter)
403                 {
404                     if (IsHorizontal && item.HeightSpecification == LayoutParamPolicies.MatchParent)
405                     {
406                         item.Size = new Size(item.Size.Width, Container.Size.Height - Padding.Top - Padding.Bottom - item.Margin.Top - item.Margin.Bottom);
407                     }
408                     else if (!IsHorizontal && item.WidthSpecification == LayoutParamPolicies.MatchParent)
409                     {
410                         item.Size = new Size(Container.Size.Width - Padding.Start - Padding.End - item.Margin.Start - item.Margin.End, item.Size.Height);
411                     }
412                 }
413                 //Console.WriteLine("[NUI] ["+item.Index+"] ["+item.Position.X+", "+item.Position.Y+" ==== \n");
414             }
415             //Console.WriteLine("Realize Done");
416         }
417
418         /// <summary>
419         /// Clear the current screen and all properties.
420         /// </summary>
421         [EditorBrowsable(EditorBrowsableState.Never)]
422         public override void Clear()
423         {
424             // Clean Up
425             if (requestLayoutTimer != null)
426             {
427                 requestLayoutTimer.Dispose();
428             }
429             if (groups != null)
430             {
431                  /*
432                 foreach (GroupInfo group in groups)
433                 {
434                     //group.ItemPosition?.Clear();
435                     // if Disposable?
436                     //group.Dispose();
437                 }
438                 */
439                 groups.Clear();
440             }
441             if (headerMargin != null)
442             {
443                 headerMargin.Dispose();
444                 headerMargin = null;
445             }
446             if (footerMargin != null)
447             {
448                 footerMargin.Dispose();
449                 footerMargin = null;
450             }
451             if (groupHeaderMargin != null)
452             {
453                 groupHeaderMargin.Dispose();
454                 groupHeaderMargin = null;
455             }
456             if (groupFooterMargin != null)
457             {
458                 groupFooterMargin.Dispose();
459                 groupFooterMargin = null;
460             }
461
462             base.Clear();
463         }
464
465         /// <inheritdoc/>
466         public override void NotifyItemSizeChanged(RecyclerViewItem item)
467         {
468             // All Item size need to be same in grid!
469             // if you want to change item size, change dataTemplate to re-initing.
470             return;
471         }
472
473         /// <Inheritdoc/>
474         [EditorBrowsable(EditorBrowsableState.Never)]
475         public override void NotifyItemInserted(IItemSource source, int startIndex)
476         {
477             // Insert Single item.
478             if (source == null) throw new ArgumentNullException(nameof(source));
479
480             // Will be null if not a group.
481             float currentSize = 0;
482             IGroupableItemSource gSource = source as IGroupableItemSource;
483
484             // Get the first Visible Position to adjust.
485             /*
486             int topInScreenIndex = 0;
487             float offset = 0F;
488             (topInScreenIndex, offset) = FindTopItemInScreen();
489             */
490
491             //2. Handle Group Case.
492             if (isGrouped && gSource != null)
493             {
494                 GroupInfo groupInfo = null;
495                 object groupParent = gSource.GetGroupParent(startIndex);
496                 int parentIndex = gSource.GetPosition(groupParent);
497                 if (gSource.HasHeader) parentIndex--;
498
499                 // Check item is group parent or not
500                 // if group parent, add new gorupinfo
501                 if (gSource.IsHeader(startIndex))
502                 {
503                     // This is childless group.
504                     // create new groupInfo!
505                     groupInfo = new GroupInfo()
506                     {
507                         GroupParent = groupParent,
508                         StartIndex = startIndex,
509                         Count = 1,
510                         GroupSize = groupHeaderSize,
511                     };
512
513                     if (parentIndex >= groups.Count)
514                     {
515                         groupInfo.GroupPosition = ScrollContentSize;
516                         groups.Add(groupInfo);
517                     }
518                     else
519                     {
520                         groupInfo.GroupPosition = groups[parentIndex].GroupPosition;
521                         groups.Insert(parentIndex, groupInfo);
522                     }
523
524                     currentSize = groupHeaderSize;
525                 }
526                 else
527                 {
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!");
532
533                     if (gSource.IsGroupFooter(startIndex))
534                     {
535                         // It doesn't make sence to adding footer by notify...
536                         // if GroupFooterTemplate is added,
537                         // need to implement on here.
538                     }
539                     else
540                     {
541                         if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
542                         {
543                             // Wrong! Grid Layouter do not support MeasureAll!
544                         }
545                         else
546                         {
547                             int pureCount = groupInfo.Count - 1 - (colView.GroupFooterTemplate == null? 0: 1);
548                             if (pureCount % spanSize == 0)
549                             {
550                                 currentSize = StepCandidate;
551                                 groupInfo.GroupSize += currentSize;
552                             }
553
554                         }
555                     }
556                     groupInfo.Count++;
557
558                 }
559
560                 if (parentIndex + 1 < groups.Count)
561                 {
562                     for(int i = parentIndex + 1; i < groups.Count; i++)
563                     {
564                         groups[i].GroupPosition += currentSize;
565                         groups[i].StartIndex++;
566                     }
567                 }
568             }
569             else
570             {
571                 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
572                 {
573                     // Wrong! Grid Layouter do not support MeasureAll!
574                 }
575                 int pureCount = colView.InternalItemSource.Count - (hasHeader? 1: 0) - (hasFooter? 1: 0);
576
577                 // Count comes after updated in ungrouped case!
578                 if (pureCount % spanSize == 1)
579                 {
580                     currentSize = StepCandidate;
581                 }
582             }
583
584             // 3. Update Scroll Content Size
585             ScrollContentSize += currentSize;
586
587             if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
588             else colView.ContentContainer.SizeHeight = ScrollContentSize;
589
590             // 4. Update Visible Items.
591             foreach (RecyclerViewItem item in VisibleItems)
592             {
593                 if (item.Index >= startIndex)
594                 {
595                     item.Index++;
596                 }
597             }
598
599             float scrollPosition = PrevScrollPosition;
600
601             /*
602             // Position Adjust
603             // Insertion above Top Visible!
604             if (startIndex <= topInScreenIndex)
605             {
606                 scrollPosition = GetItemPosition(topInScreenIndex);
607                 scrollPosition -= offset;
608
609                 colView.ScrollTo(scrollPosition);
610             }
611             */
612
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);
618         }
619
620         /// <Inheritdoc/>
621         [EditorBrowsable(EditorBrowsableState.Never)]
622         public override void NotifyItemRangeInserted(IItemSource source, int startIndex, int count)
623         {
624              // Insert Group
625             if (source == null) throw new ArgumentNullException(nameof(source));
626
627             float currentSize = 0;
628             // Will be null if not a group.
629             IGroupableItemSource gSource = source as IGroupableItemSource;
630
631             // Get the first Visible Position to adjust.
632             /*
633             int topInScreenIndex = 0;
634             float offset = 0F;
635             (topInScreenIndex, offset) = FindTopItemInScreen();
636             */
637
638             // 2. Handle Group Case
639             // Adding ranged items should all same new groups.
640             if (isGrouped && gSource != null)
641             {
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))
648                 {
649                     groupStartIndex = startIndex;
650                 }
651                 else
652                 {
653                     //exception case!
654                     throw new Exception("Inserted wrong groups!");
655                 }
656
657                 for (int current = startIndex; current - startIndex < count; current++)
658                 {
659                     // Check item is group parent or not
660                     // if group parent, add new gorupinfo
661                     if (groupStartIndex == current)
662                     {
663                         //create new groupInfo!
664                         groupInfo = new GroupInfo()
665                         {
666                             GroupParent = groupParent,
667                             StartIndex = current,
668                             Count = 1,
669                             GroupSize = groupHeaderSize,
670                         };
671                         currentSize += groupHeaderSize;
672
673                     }
674                     else
675                     {
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!");
679                         groupInfo.Count++;
680
681                         if (gSource.IsGroupFooter(current))
682                         {
683                                 groupInfo.GroupSize += groupFooterSize;
684                                 currentSize += groupFooterSize;
685                         }
686                         else
687                         {
688                             if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
689                             {
690                                 // Wrong! Grid Layouter do not support MeasureAll!
691                             }
692                             else
693                             {
694                                 int index = current - groupInfo.StartIndex - 1; // groupHeader must always exist.
695                                 if ((index % spanSize) == 0)
696                                 {
697                                     groupInfo.GroupSize += StepCandidate;
698                                     currentSize += StepCandidate;
699                                 }
700                             }
701                         }
702                     }
703                 }
704
705                 if (parentIndex >= groups.Count)
706                 {
707                     groupInfo.GroupPosition = ScrollContentSize;
708                     groups.Add(groupInfo);
709                 }
710                 else
711                 {
712                     groupInfo.GroupPosition = groups[parentIndex].GroupPosition;
713                     groups.Insert(parentIndex, groupInfo);
714                 }
715
716                 // Update other below group's position
717                 if (parentIndex + 1 < groups.Count)
718                 {
719                     for(int i = parentIndex + 1; i < groups.Count; i++)
720                     {
721                         groups[i].GroupPosition += currentSize;
722                         groups[i].StartIndex += count;
723                     }
724                 }
725
726                 ScrollContentSize += currentSize;
727             }
728             else
729             {
730                 throw new Exception("Cannot insert ungrouped range items!");
731             }
732
733             // 3. Update Scroll Content Size
734             if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
735             else colView.ContentContainer.SizeHeight = ScrollContentSize;
736
737             // 4. Update Visible Items.
738             foreach (RecyclerViewItem item in VisibleItems)
739             {
740                 if (item.Index >= startIndex)
741                 {
742                     item.Index += count;
743                 }
744             }
745
746             // Position Adjust
747             float scrollPosition = PrevScrollPosition;
748             /*
749             // Insertion above Top Visible!
750             if (startIndex + count <= topInScreenIndex)
751             {
752                 scrollPosition = GetItemPosition(topInScreenIndex);
753                 scrollPosition -= offset;
754
755                 colView.ScrollTo(scrollPosition);
756             }
757             */
758
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);
764         }
765
766         /// <Inheritdoc/>
767         [EditorBrowsable(EditorBrowsableState.Never)]
768         public override void NotifyItemRemoved(IItemSource source, int startIndex)
769         {
770             // Remove Single
771             if (source == null) throw new ArgumentNullException(nameof(source));
772
773             // Will be null if not a group.
774             float currentSize = 0;
775             IGroupableItemSource gSource = source as IGroupableItemSource;
776
777             // Get the first Visible Position to adjust.
778             /*
779             int topInScreenIndex = 0;
780             float offset = 0F;
781             (topInScreenIndex, offset) = FindTopItemInScreen();
782             */
783
784             // 2. Handle Group Case
785             if (isGrouped && gSource != null)
786             {
787                 int parentIndex = 0;
788                 GroupInfo groupInfo = null;
789                 foreach(GroupInfo cur in groups)
790                 {
791                     if ((cur.StartIndex <= startIndex) && (cur.StartIndex + cur.Count - 1 >= startIndex))
792                     {
793                         groupInfo = cur;
794                         break;
795                     }
796                     parentIndex++;
797                 }
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)
802                 {
803                     // This is empty group!
804                     // check group is empty.
805                     if (groupInfo.Count != 1)
806                     {
807                         throw new Exception("Cannot remove group parent");
808                     }
809                     currentSize = groupInfo.GroupSize;
810
811                     // Remove Group
812                     // groupInfo.Dispose();
813                     groups.Remove(groupInfo);
814                 }
815                 else
816                 {
817                     groupInfo.Count--;
818
819                     // Skip footer case as footer cannot exist alone without header.
820                     if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
821                     {
822                         // Wrong! Grid Layouter do not support MeasureAll!
823                     }
824                     else
825                     {
826                         int pureCount = groupInfo.Count - 1 - (colView.GroupFooterTemplate == null? 0: 1);
827                         if (pureCount % spanSize == 0)
828                         {
829                                 currentSize = StepCandidate;
830                                 groupInfo.GroupSize -= currentSize;
831                         }
832                     }
833                 }
834
835                 for (int i = parentIndex + 1; i < groups.Count; i++)
836                 {
837                     groups[i].GroupPosition -= currentSize;
838                     groups[i].StartIndex--;
839                 }
840             }
841             else
842             {
843                 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
844                 {
845                     // Wrong! Grid Layouter do not support MeasureAll!
846                 }
847                 int pureCount = colView.InternalItemSource.Count - (hasHeader? 1: 0) - (hasFooter? 1: 0);
848
849                 // Count comes after updated in ungrouped case!
850                 if (pureCount % spanSize == 0)
851                 {
852                     currentSize = StepCandidate;
853                 }
854                 
855             }
856
857             ScrollContentSize -= currentSize;
858
859             // 3. Update Scroll Content Size
860             if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
861             else colView.ContentContainer.SizeHeight = ScrollContentSize;
862
863             // 4. Update Visible Items.
864             RecyclerViewItem targetItem = null;
865             foreach (RecyclerViewItem item in VisibleItems)
866             {
867                 if (item.Index == startIndex)
868                 {
869                     targetItem = item;
870                     colView.UnrealizeItem(item);
871                 }
872                 else if (item.Index > startIndex)
873                 {
874                     item.Index--;
875                 }
876             }
877             VisibleItems.Remove(targetItem);
878
879             // Position Adjust
880             float scrollPosition = PrevScrollPosition;
881             /*
882             // Insertion above Top Visible!
883             if (startIndex <= topInScreenIndex)
884             {
885                 scrollPosition = GetItemPosition(topInScreenIndex);
886                 scrollPosition -= offset;
887
888                 colView.ScrollTo(scrollPosition);
889             }
890             */
891
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);
897         }
898
899         /// <Inheritdoc/>
900         [EditorBrowsable(EditorBrowsableState.Never)]
901         public override void NotifyItemRangeRemoved(IItemSource source, int startIndex, int count)
902         {
903             // Remove Group
904             if (source == null) throw new ArgumentNullException(nameof(source));
905
906             // Will be null if not a group.
907             float currentSize = StepCandidate;
908             IGroupableItemSource gSource = source as IGroupableItemSource;
909
910             // Get the first Visible Position to adjust.
911             /*
912             int topInScreenIndex = 0;
913             float offset = 0F;
914             (topInScreenIndex, offset) = FindTopItemInScreen();
915             */
916
917             // 1. Handle Group Case
918             if (isGrouped && gSource != null)
919             {
920                 int parentIndex = 0;
921                 GroupInfo groupInfo = null;
922                 foreach(GroupInfo cur in groups)
923                 {
924                     if ((cur.StartIndex == startIndex) && (cur.Count == count))
925                     {
926                         groupInfo = cur;
927                         break;
928                     }
929                     parentIndex++;
930                 }
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)
936                 {
937                     // Wrong! Grid Layouter do not support MeasureAll!
938                 }
939                 // Remove Group
940                 // groupInfo.Dispose();
941                 groups.Remove(groupInfo);
942
943                 for (int i = parentIndex; i < groups.Count; i++)
944                 {
945                     groups[i].GroupPosition -= currentSize;
946                     groups[i].StartIndex -= count;
947                 }
948             }
949             else
950             {
951                 // It must group case! throw exception!
952                 throw new Exception("Range remove must group remove!");
953             }
954
955             ScrollContentSize -= currentSize;
956
957             // 2. Update Scroll Content Size
958             if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
959             else colView.ContentContainer.SizeHeight = ScrollContentSize;
960
961             // 3. Update Visible Items.
962             List<RecyclerViewItem> unrealizedItems = new List<RecyclerViewItem>();
963             foreach (RecyclerViewItem item in VisibleItems)
964             {
965                 if ((item.Index >= startIndex)
966                     && (item.Index < startIndex + count))
967                 {
968                     unrealizedItems.Add(item);
969                     colView.UnrealizeItem(item);
970                 }
971                 else if (item.Index >= startIndex + count)
972                 {
973                     item.Index -= count;
974                 }
975             }
976             VisibleItems.RemoveAll(unrealizedItems.Contains);
977             unrealizedItems.Clear();
978
979             // Position Adjust
980             float scrollPosition = PrevScrollPosition;
981             /*
982             // Insertion above Top Visible!
983             if (startIndex <= topInScreenIndex)
984             {
985                 scrollPosition = GetItemPosition(topInScreenIndex);
986                 scrollPosition -= offset;
987
988                 colView.ScrollTo(scrollPosition);
989             }
990             */
991
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);
997         }
998
999         /// <Inheritdoc/>
1000         [EditorBrowsable(EditorBrowsableState.Never)]
1001         public override void NotifyItemMoved(IItemSource source, int fromPosition, int toPosition)
1002         {
1003             // Reorder Single
1004             if (source == null) throw new ArgumentNullException(nameof(source));
1005
1006             // Will be null if not a group.
1007             float currentSize = StepCandidate;
1008             int diff = toPosition - fromPosition;
1009
1010             // Get the first Visible Position to adjust.
1011             /*
1012             int topInScreenIndex = 0;
1013             float offset = 0F;
1014             (topInScreenIndex, offset) = FindTopItemInScreen();
1015             */
1016             
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);
1022
1023             if ((endIndex >= FirstVisible) && (startIndex <= LastVisible))
1024             {
1025                 foreach (RecyclerViewItem item in VisibleItems)
1026                 {
1027                     if ((item.Index >= startIndex)
1028                         && (item.Index <= endIndex))
1029                     {
1030                         if (item.Index == fromPosition) item.Index = toPosition;
1031                         else
1032                         {
1033                             if (diff > 0) item.Index--;
1034                             else item.Index++;
1035                         }
1036                     }
1037                 }
1038             }
1039
1040             if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
1041             else colView.ContentContainer.SizeHeight = ScrollContentSize;
1042
1043             // Position Adjust
1044             float scrollPosition = PrevScrollPosition;
1045             /*
1046             // Insertion above Top Visible!
1047             if (((fromPosition > topInScreenIndex) && (toPosition < topInScreenIndex) ||
1048                 ((fromPosition < topInScreenIndex) && (toPosition > topInScreenIndex)))
1049             {
1050                 scrollPosition = GetItemPosition(topInScreenIndex);
1051                 scrollPosition -= offset;
1052
1053                 colView.ScrollTo(scrollPosition);
1054             }
1055             */
1056
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);
1062         }
1063
1064         /// <Inheritdoc/>
1065         [EditorBrowsable(EditorBrowsableState.Never)]
1066         public override void NotifyItemRangeMoved(IItemSource source, int fromPosition, int toPosition, int count)
1067         {
1068             // Reorder Groups
1069             if (source == null) throw new ArgumentNullException(nameof(source));
1070
1071             // Will be null if not a group.
1072             float currentSize = StepCandidate;
1073             int diff = toPosition - fromPosition;
1074
1075             int startIndex = ( diff > 0 ? fromPosition: toPosition);
1076             int endIndex = (diff > 0 ? toPosition + count - 1: fromPosition + count - 1);
1077
1078             // 2. Handle Group Case
1079             if (isGrouped)
1080             {
1081                 int fromParentIndex = 0;
1082                 int toParentIndex = 0;
1083                 bool findFrom = false;
1084                 bool findTo = false;
1085                 GroupInfo fromGroup = null;
1086                 GroupInfo toGroup = null;
1087
1088                 foreach(GroupInfo cur in groups)
1089                 {
1090                     if ((cur.StartIndex == fromPosition) && (cur.Count == count))
1091                     {
1092                         fromGroup = cur;
1093                         findFrom = true;
1094                         if (findFrom && findTo) break;
1095                     }
1096                     else if (cur.StartIndex == toPosition)
1097                     {
1098                         toGroup = cur;
1099                         findTo = true;
1100                         if (findFrom && findTo) break;
1101                     }
1102                     if (!findFrom) fromParentIndex++;
1103                     if (!findTo) toParentIndex++;
1104                 }
1105                 if (toGroup == null || fromGroup == null) throw new Exception("Cannot find group information!");
1106
1107                 fromGroup.StartIndex = toGroup.StartIndex;
1108                 fromGroup.GroupPosition = toGroup.GroupPosition;
1109
1110                 endIndex = (diff > 0 ? toPosition + toGroup.Count - 1: fromPosition + count - 1);
1111
1112                 groups.Remove(fromGroup);
1113                 groups.Insert(toParentIndex, fromGroup);
1114
1115                 int startGroup = (diff > 0? fromParentIndex: toParentIndex);
1116                 int endGroup =  (diff > 0? toParentIndex: fromParentIndex);
1117                 
1118                 for (int i = startGroup; i <= endGroup; i++)
1119                 {
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;
1125                 }
1126             }
1127             else
1128             {
1129                 //It must group case! throw exception!
1130                 throw new Exception("Range remove must group remove!");
1131             }
1132
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))
1137             {
1138                 foreach (RecyclerViewItem item in VisibleItems)
1139                 {
1140                     if ((item.Index >= startIndex)
1141                         && (item.Index <= endIndex))
1142                     {
1143                         if ((item.Index >= fromPosition) && (item.Index < fromPosition + count))
1144                         {
1145                             item.Index = fromPosition - item.Index + toPosition;
1146                         }
1147                         else
1148                         {
1149                             if (diff > 0) item.Index -= count;
1150                             else item.Index += count;
1151                         }
1152                     }
1153                 }
1154             }
1155
1156             // Position Adjust
1157             float scrollPosition = PrevScrollPosition;
1158             /*
1159             // Insertion above Top Visible!
1160             if (((fromPosition > topInScreenIndex) && (toPosition < topInScreenIndex) ||
1161                 ((fromPosition < topInScreenIndex) && (toPosition > topInScreenIndex)))
1162             {
1163                 scrollPosition = GetItemPosition(topInScreenIndex);
1164                 scrollPosition -= offset;
1165
1166                 colView.ScrollTo(scrollPosition);
1167             }
1168             */
1169
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);
1175         }
1176
1177         /// <Inheritdoc/>
1178         [EditorBrowsable(EditorBrowsableState.Never)]
1179         public override float CalculateLayoutOrientationSize()
1180         {
1181             //Console.WriteLine("[NUI] Calculate Layout ScrollContentSize {0}", ScrollContentSize);
1182             return ScrollContentSize;
1183         }
1184
1185         /// <Inheritdoc/>
1186         [EditorBrowsable(EditorBrowsableState.Never)]
1187         public override float CalculateCandidateScrollPosition(float scrollPosition)
1188         {
1189             //Console.WriteLine("[NUI] Calculate Candidate ScrollContentSize {0}", ScrollContentSize);
1190             return scrollPosition;
1191         }
1192
1193         /// <Inheritdoc/>
1194         [EditorBrowsable(EditorBrowsableState.Never)]
1195         public override View RequestNextFocusableView(View currentFocusedView, View.FocusDirection direction, bool loopEnabled)
1196         {
1197             if (currentFocusedView == null)
1198                 throw new ArgumentNullException(nameof(currentFocusedView));
1199
1200             View nextFocusedView = null;
1201             int targetSibling = -1;
1202             bool IsHorizontal = colView.ScrollingDirection == ScrollableBase.Direction.Horizontal;
1203
1204             switch (direction)
1205             {
1206                 case View.FocusDirection.Left:
1207                     {
1208                         targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder - 1 : targetSibling;
1209                         break;
1210                     }
1211                 case View.FocusDirection.Right:
1212                     {
1213                         targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder + 1 : targetSibling;
1214                         break;
1215                     }
1216                 case View.FocusDirection.Up:
1217                     {
1218                         targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder - 1;
1219                         break;
1220                     }
1221                 case View.FocusDirection.Down:
1222                     {
1223                         targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder + 1;
1224                         break;
1225                     }
1226             }
1227
1228             if (targetSibling > -1 && targetSibling < Container.Children.Count)
1229             {
1230                 RecyclerViewItem candidate = Container.Children[targetSibling] as RecyclerViewItem;
1231                 if (candidate.Index >= 0 && candidate.Index < colView.InternalItemSource.Count)
1232                 {
1233                     nextFocusedView = candidate;
1234                 }
1235             }
1236             return nextFocusedView;
1237         }
1238
1239         /// <inheritdoc/>
1240         [EditorBrowsable(EditorBrowsableState.Never)]
1241         protected override (int start, int end) FindVisibleItems((float X, float Y) visibleArea)
1242         {
1243             int MaxIndex = colView.InternalItemSource.Count - 1 - (hasFooter ? 1 : 0);
1244             int adds = spanSize * 2;
1245             int skipGroup = -1;
1246             (int start, int end) found = (0, 0);
1247
1248             // Header is Showing
1249             if (hasHeader && visibleArea.X < headerSize + (IsHorizontal? Padding.Start : Padding.Top))
1250             {
1251                 found.start = 0;
1252             }
1253             else
1254             {
1255                 if (isGrouped)
1256                 {
1257                     bool failed = true;
1258                     foreach (GroupInfo gInfo in groups)
1259                     {
1260                         skipGroup++;
1261                         // in the Group
1262                         if (gInfo.GroupPosition <= visibleArea.X &&
1263                             gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.X)
1264                         {
1265                             if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.X)
1266                             {
1267                                 found.start = gInfo.StartIndex - adds;
1268                                 failed = false;
1269                             }
1270                             //can be step in spanSize...
1271                             for (int i = 1; i < gInfo.Count; i++)
1272                             {
1273                                 if (!failed) break;
1274                                 // Reach last index of group.
1275                                 if (i == (gInfo.Count - 1))
1276                                 {
1277                                     found.start = gInfo.StartIndex + i - adds;
1278                                     failed = false;
1279                                     break;
1280
1281                                 }
1282                                 else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.X - gInfo.GroupPosition - groupHeaderSize)
1283                                 {
1284                                     found.start = gInfo.StartIndex + i - adds;
1285                                     failed = false;
1286                                     break;
1287                                 }
1288                             }
1289                         }
1290                     }
1291                     //footer only shows?
1292                     if (failed)
1293                     {
1294                         found.start = MaxIndex;
1295                     }
1296                 }
1297                 else
1298                 {
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;
1302                 }
1303                 if (found.start < 0) found.start = 0;
1304             }
1305
1306             if (hasFooter && visibleArea.Y > ScrollContentSize - footerSize - (IsHorizontal? Padding.End : Padding.Bottom))
1307             {
1308                 found.end = MaxIndex + 1;
1309             }
1310             else
1311             {
1312                 if (isGrouped)
1313                 {
1314                     bool failed = true;
1315                     // can it be start from founded group...?
1316                     //foreach(GroupInfo gInfo in groups.Skip(skipGroup))
1317                     foreach (GroupInfo gInfo in groups)
1318                     {
1319                         // in the Group
1320                         if (gInfo.GroupPosition <= visibleArea.Y &&
1321                             gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.Y)
1322                         {
1323                             if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.Y)
1324                             {
1325                                 found.end = gInfo.StartIndex + adds;
1326                                 failed = false;
1327                             }
1328                             //can be step in spanSize...
1329                             for (int i = 1; i < gInfo.Count; i++)
1330                             {
1331                                 if (!failed) break;
1332                                 // Reach last index of group.
1333                                 if (i == (gInfo.Count - 1))
1334                                 {
1335                                     found.end = gInfo.StartIndex + i + adds;
1336                                     failed = false;
1337                                     break;
1338                                 }
1339                                 else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.Y - gInfo.GroupPosition - groupHeaderSize)
1340                                 {
1341                                     found.end = gInfo.StartIndex + i + adds;
1342                                     failed = false;
1343                                     break;
1344                                 }
1345                             }
1346                         }
1347                     }
1348                     //footer only shows?
1349                     if (failed)
1350                     {
1351                         found.start = MaxIndex;
1352                     }
1353                 }
1354                 else
1355                 {
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;
1360                 }
1361                 if (found.end > (MaxIndex)) found.end = MaxIndex;
1362             }
1363             return found;
1364         }
1365
1366         internal override (float X, float Y) GetItemPosition(int index)
1367         {
1368             float xPos, yPos;
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));
1374
1375             if (hasHeader && index == 0)
1376             {
1377                 return (spaceStartX + headerMargin.Start, spaceStartY + headerMargin.Top);
1378             }
1379             if (hasFooter && index == colView.InternalItemSource.Count - 1)
1380             {
1381                 xPos = IsHorizontal?
1382                         ScrollContentSize - Padding.End - footerSize + footerMargin.Start:
1383                         spaceStartX;
1384                 yPos = IsHorizontal?
1385                         spaceStartY:
1386                         ScrollContentSize - Padding.Bottom - footerSize + footerMargin.Top;
1387                 return (xPos, yPos);
1388             }
1389             if (isGrouped)
1390             {
1391                 GroupInfo myGroup = GetGroupInfo(index);
1392                 if (colView.InternalItemSource.IsGroupHeader(index))
1393                 {
1394                     spaceStartX+= groupHeaderMargin.Start;
1395                     spaceStartY+= groupHeaderMargin.Top;
1396                     xPos = IsHorizontal?
1397                             myGroup.GroupPosition + groupHeaderMargin.Start:
1398                             spaceStartX;
1399                     yPos = IsHorizontal?
1400                             spaceStartY:
1401                             myGroup.GroupPosition + groupHeaderMargin.Top;
1402                 }
1403                 else if (colView.InternalItemSource.IsGroupFooter(index))
1404                 {
1405                     spaceStartX+= groupFooterMargin.Start;
1406                     spaceStartY+= groupFooterMargin.Top;
1407                     xPos = IsHorizontal?
1408                             myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize + groupFooterMargin.Start:
1409                             spaceStartX;
1410                     yPos = IsHorizontal?
1411                             spaceStartY:
1412                             myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize + groupFooterMargin.Top;
1413                 }
1414                 else
1415                 {
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;
1423
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;
1430                 }
1431             }
1432             else
1433             {
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;
1442
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;
1449             }
1450
1451             return (xPos, yPos);
1452         }
1453
1454         internal override (float Width, float Height) GetItemSize(int index)
1455         {            
1456             return (sizeCandidate.Width - CandidateMargin.Start - CandidateMargin.End,
1457                     sizeCandidate.Height - CandidateMargin.Top - CandidateMargin.Bottom);
1458         }
1459         private void DelayedRequestLayout(float scrollPosition , bool force = true)
1460         {
1461             if (requestLayoutTimer != null)
1462             {
1463                 requestLayoutTimer.Dispose();
1464             }
1465
1466             requestLayoutTimer = new Timer(1);
1467             requestLayoutTimer.Interval = 1;
1468             requestLayoutTimer.Tick += ((object target, Timer.TickEventArgs args) =>
1469             {
1470                 RequestLayout(scrollPosition, force);
1471                 return false;
1472             });
1473         }
1474
1475         private RecyclerViewItem GetVisibleItem(int index)
1476         {
1477             foreach (RecyclerViewItem item in VisibleItems)
1478             {
1479                 if (item.Index == index) return item;
1480             }
1481
1482             return null;
1483         }
1484
1485         private GroupInfo GetGroupInfo(int index)
1486         {
1487             if (Visited != null)
1488             {
1489                 if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index)
1490                     return Visited;
1491             }
1492             if (hasHeader && index == 0) return null;
1493             foreach (GroupInfo group in groups)
1494             {
1495                 if (group.StartIndex <= index && group.StartIndex + group.Count > index)
1496                 {
1497                     Visited = group;
1498                     return group;
1499                 }
1500             }
1501             Visited = null;
1502             return null;
1503         }
1504
1505         /*
1506         private object GetGroupParent(int index)
1507         {
1508             if (Visited != null)
1509             {
1510                 if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index)
1511                 return Visited.GroupParent;
1512             }
1513             if (hasHeader && index == 0) return null;
1514             foreach (GroupInfo group in groups)
1515             {
1516                 if (group.StartIndex <= index && group.StartIndex + group.Count > index)
1517                 {
1518                     Visited = group;
1519                     return group.GroupParent;
1520                 }
1521             }
1522             Visited = null;
1523             return null;
1524         }
1525         */
1526
1527         class GroupInfo
1528         {
1529             public object GroupParent;
1530             public int StartIndex;
1531             public int Count;
1532             public float GroupSize;
1533             public float GroupPosition;
1534             //Items relative position from the GroupPosition
1535         }
1536     }
1537 }