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