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