1 /* Copyright (c) 2021 Samsung Electronics Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
7 * http://www.apache.org/licenses/LICENSE-2.0
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
17 using System.Collections.Generic;
18 using System.ComponentModel;
19 using Tizen.NUI.BaseComponents;
21 namespace Tizen.NUI.Components
24 /// This class implements a grid box layout.
26 [EditorBrowsable(EditorBrowsableState.Never)]
27 public class GridLayouter : ItemsLayouter
29 private CollectionView colView;
30 private (float Width, float Height) sizeCandidate;
31 private int spanSize = 1;
32 private float align = 0.5f;
33 private bool hasHeader;
34 private Extents headerMargin;
35 private float headerSize;
36 private Extents footerMargin;
37 private bool hasFooter;
38 private float footerSize;
39 private bool isGrouped;
40 private readonly List<GroupInfo> groups = new List<GroupInfo>();
41 private float groupHeaderSize;
42 private Extents groupHeaderMargin;
43 private float groupFooterSize;
44 private Extents groupFooterMargin;
45 private GroupInfo Visited;
48 /// Clean up ItemsLayouter.
50 /// <param name="view"> ItemsView of layouter. </param>
51 [EditorBrowsable(EditorBrowsableState.Never)]
52 public override void Initialize(RecyclerView view)
54 colView = view as CollectionView;
57 throw new ArgumentException("GridLayouter only can be applied CollectionView.", nameof(view));
61 foreach (RecyclerViewItem item in VisibleItems)
63 colView.UnrealizeItem(item, false);
71 IsHorizontal = (colView.ScrollingDirection == ScrollableBase.Direction.Horizontal);
73 RecyclerViewItem header = colView?.Header;
74 RecyclerViewItem footer = colView?.Footer;
76 int count = colView.InternalItemSource.Count;
77 int pureCount = count - (header? 1 : 0) - (footer? 1 : 0);
79 // 2. Get the header / footer and size deligated item and measure the size.
82 MeasureChild(colView, header);
84 width = header.Layout != null? header.Layout.MeasuredWidth.Size.AsRoundedValue() : 0;
85 height = header.Layout != null? header.Layout.MeasuredHeight.Size.AsRoundedValue() : 0;
87 Extents itemMargin = header.Margin;
88 headerSize = IsHorizontal?
89 width + itemMargin.Start + itemMargin.End:
90 height + itemMargin.Top + itemMargin.Bottom;
91 headerMargin = new Extents(itemMargin);
94 colView.UnrealizeItem(header);
99 MeasureChild(colView, footer);
101 width = footer.Layout != null? footer.Layout.MeasuredWidth.Size.AsRoundedValue() : 0;
102 height = footer.Layout != null? footer.Layout.MeasuredHeight.Size.AsRoundedValue() : 0;
104 Extents itemMargin = footer.Margin;
105 footerSize = IsHorizontal?
106 width + itemMargin.Start + itemMargin.End:
107 height + itemMargin.Top + itemMargin.Bottom;
108 footerMargin = new Extents(itemMargin);
109 footer.Index = count - 1;
112 colView.UnrealizeItem(footer);
115 int firstIndex = header? 1 : 0;
117 if (colView.IsGrouped)
121 if (colView.GroupHeaderTemplate != null)
123 while (!colView.InternalItemSource.IsGroupHeader(firstIndex)) firstIndex++;
124 //must be always true
125 if (colView.InternalItemSource.IsGroupHeader(firstIndex))
127 RecyclerViewItem groupHeader = colView.RealizeItem(firstIndex);
130 if (groupHeader == null) throw new Exception("[" + firstIndex + "] Group Header failed to realize!");
132 // Need to Set proper height or width on scroll direction.
133 if (groupHeader.Layout == null)
135 width = groupHeader.WidthSpecification;
136 height = groupHeader.HeightSpecification;
140 MeasureChild(colView, groupHeader);
142 width = groupHeader.Layout.MeasuredWidth.Size.AsRoundedValue();
143 height = groupHeader.Layout.MeasuredHeight.Size.AsRoundedValue();
145 //Console.WriteLine("[NUI] GroupHeader Size {0} :{0}", width, height);
146 // pick the StepCandidate.
147 Extents itemMargin = groupHeader.Margin;
148 groupHeaderSize = IsHorizontal?
149 width + itemMargin.Start + itemMargin.End:
150 height + itemMargin.Top + itemMargin.Bottom;
151 groupHeaderMargin = new Extents(itemMargin);
152 colView.UnrealizeItem(groupHeader);
157 groupHeaderSize = 0F;
160 if (colView.GroupFooterTemplate != null)
162 int firstFooter = firstIndex;
163 while (!colView.InternalItemSource.IsGroupFooter(firstFooter)) firstFooter++;
164 //must be always true
165 if (colView.InternalItemSource.IsGroupFooter(firstFooter))
167 RecyclerViewItem groupFooter = colView.RealizeItem(firstFooter);
169 if (groupFooter == null) throw new Exception("[" + firstFooter + "] Group Footer failed to realize!");
170 // Need to Set proper height or width on scroll direction.
171 if (groupFooter.Layout == null)
173 width = groupFooter.WidthSpecification;
174 height = groupFooter.HeightSpecification;
178 MeasureChild(colView, groupFooter);
180 width = groupFooter.Layout.MeasuredWidth.Size.AsRoundedValue();
181 height = groupFooter.Layout.MeasuredHeight.Size.AsRoundedValue();
183 // pick the StepCandidate.
184 Extents itemMargin = groupFooter.Margin;
185 groupFooterSize = IsHorizontal?
186 width + itemMargin.Start + itemMargin.End:
187 height + itemMargin.Top + itemMargin.Bottom;
188 groupFooterMargin = new Extents(itemMargin);
190 colView.UnrealizeItem(groupFooter);
195 groupFooterSize = 0F;
198 else isGrouped = false;
201 //Final Check of FirstIndex
202 while (colView.InternalItemSource.IsHeader(firstIndex) ||
203 colView.InternalItemSource.IsGroupHeader(firstIndex) ||
204 colView.InternalItemSource.IsGroupFooter(firstIndex))
206 if (colView.InternalItemSource.IsFooter(firstIndex))
215 sizeCandidate = (0, 0);
218 // Get Size Deligate. FIXME if group exist index must be changed.
219 RecyclerViewItem sizeDeligate = colView.RealizeItem(firstIndex);
220 if (sizeDeligate == null)
222 throw new Exception("Cannot create content from DatTemplate.");
224 sizeDeligate.BindingContext = colView.InternalItemSource.GetItem(firstIndex);
226 // Need to Set proper height or width on scroll direction.
227 if (sizeDeligate.Layout == null)
229 width = sizeDeligate.WidthSpecification;
230 height = sizeDeligate.HeightSpecification;
234 MeasureChild(colView, sizeDeligate);
236 width = sizeDeligate.Layout.MeasuredWidth.Size.AsRoundedValue();
237 height = sizeDeligate.Layout.MeasuredHeight.Size.AsRoundedValue();
239 //Console.WriteLine("[NUI] item Size {0} :{1}", width, height);
241 // pick the StepCandidate.
242 Extents itemMargin = sizeDeligate.Margin;
243 width = width + itemMargin.Start + itemMargin.End;
244 height = height + itemMargin.Top + itemMargin.Bottom;
245 StepCandidate = IsHorizontal? width : height;
246 CandidateMargin = new Extents(itemMargin);
247 spanSize = IsHorizontal?
248 Convert.ToInt32(Math.Truncate((double)((colView.Size.Height - Padding.Top - Padding.Bottom) / height))) :
249 Convert.ToInt32(Math.Truncate((double)((colView.Size.Width - Padding.Start - Padding.End) / width)));
251 sizeCandidate = (width, height);
253 colView.UnrealizeItem(sizeDeligate);
256 if (StepCandidate < 1) StepCandidate = 1;
257 if (spanSize < 1) spanSize = 1;
261 float Current = 0.0F;
262 IGroupableItemSource source = colView.InternalItemSource;
263 GroupInfo currentGroup = null;
265 for (int i = 0; i < count; i++)
267 if (i == 0 && hasHeader)
269 Current += headerSize;
271 else if (i == count - 1 && hasFooter)
273 Current += footerSize;
277 //GroupHeader must always exist in group usage.
278 if (source.IsGroupHeader(i))
280 currentGroup = new GroupInfo()
282 GroupParent = source.GetGroupParent(i),
285 GroupSize = groupHeaderSize,
286 GroupPosition = Current
288 groups.Add(currentGroup);
289 Current += groupHeaderSize;
292 else if (source.IsGroupFooter(i))
294 //currentGroup.hasFooter = true;
295 currentGroup.Count++;
296 currentGroup.GroupSize += groupFooterSize;
297 Current += groupFooterSize;
301 currentGroup.Count++;
302 int index = i - currentGroup.StartIndex - 1; // groupHeader must always exist.
303 if ((index % spanSize) == 0)
305 currentGroup.GroupSize += StepCandidate;
306 Current += StepCandidate;
311 ScrollContentSize = Current;
315 // 3. Measure the scroller content size.
316 ScrollContentSize = StepCandidate * Convert.ToInt32(Math.Ceiling((double)pureCount / (double)spanSize));
317 if (hasHeader) ScrollContentSize += headerSize;
318 if (hasFooter) ScrollContentSize += footerSize;
321 ScrollContentSize = IsHorizontal?
322 ScrollContentSize + Padding.Start + Padding.End:
323 ScrollContentSize + Padding.Top + Padding.Bottom;
325 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
326 else colView.ContentContainer.SizeHeight = ScrollContentSize;
328 base.Initialize(colView);
329 //Console.WriteLine("Init Done, StepCnadidate{0}, spanSize{1}, Scroll{2}", StepCandidate, spanSize, ScrollContentSize);
333 /// This is called to find out where items are lain out according to current scroll position.
335 /// <param name="scrollPosition">Scroll position which is calculated by ScrollableBase</param>
336 /// <param name="force">boolean force flag to layouting forcely.</param>
337 public override void RequestLayout(float scrollPosition, bool force = false)
339 // Layouting is only possible after once it intialized.
340 if (!IsInitialized) return;
341 int LastIndex = colView.InternalItemSource.Count;
343 if (!force && PrevScrollPosition == Math.Abs(scrollPosition)) return;
344 PrevScrollPosition = Math.Abs(scrollPosition);
346 int prevFirstVisible = FirstVisible;
347 int prevLastVisible = LastVisible;
348 bool IsHorizontal = (colView.ScrollingDirection == ScrollableBase.Direction.Horizontal);
350 (float X, float Y) visibleArea = (PrevScrollPosition,
351 PrevScrollPosition + (IsHorizontal? colView.Size.Width : colView.Size.Height)
354 //Console.WriteLine("[NUI] itemsView [{0},{1}] [{2},{3}]", colView.Size.Width, colView.Size.Height, colView.ContentContainer.Size.Width, colView.ContentContainer.Size.Height);
356 // 1. Set First/Last Visible Item Index.
357 (int start, int end) = FindVisibleItems(visibleArea);
358 FirstVisible = start;
361 //Console.WriteLine("[NUI] {0} :visibleArea before [{1},{2}] after [{3},{4}]", scrollPosition, prevFirstVisible, prevLastVisible, FirstVisible, LastVisible);
363 // 2. Unrealize invisible items.
364 List<RecyclerViewItem> unrealizedItems = new List<RecyclerViewItem>();
365 foreach (RecyclerViewItem item in VisibleItems)
367 if (item.Index < FirstVisible || item.Index > LastVisible)
369 //Console.WriteLine("[NUI] Unrealize{0}!", item.Index);
370 unrealizedItems.Add(item);
371 colView.UnrealizeItem(item);
374 VisibleItems.RemoveAll(unrealizedItems.Contains);
376 //Console.WriteLine("Realize Begin [{0} to {1}]", FirstVisible, LastVisible);
377 // 3. Realize and placing visible items.
378 for (int i = FirstVisible; i <= LastVisible; i++)
380 //Console.WriteLine("[NUI] Realize!");
381 RecyclerViewItem item = null;
382 // 4. Get item if visible or realize new.
383 if (i >= prevFirstVisible && i <= prevLastVisible)
385 item = GetVisibleItem(i);
388 if (item == null) item = colView.RealizeItem(i);
389 VisibleItems.Add(item);
391 //item Position without Padding and Margin.
392 (float x, float y) = GetItemPosition(i);
393 // 5. Placing item with Padding and Margin.
394 item.Position = new Position(x, y);
396 //Linear Item need to be resized!
397 if (item.IsHeader || item.IsFooter || item.isGroupHeader || item.isGroupFooter)
399 if (IsHorizontal && item.HeightSpecification == LayoutParamPolicies.MatchParent)
401 item.Size = new Size(item.Size.Width, Container.Size.Height - Padding.Top - Padding.Bottom - item.Margin.Top - item.Margin.Bottom);
403 else if (!IsHorizontal && item.WidthSpecification == LayoutParamPolicies.MatchParent)
405 item.Size = new Size(Container.Size.Width - Padding.Start - Padding.End - item.Margin.Start - item.Margin.End, item.Size.Height);
408 //Console.WriteLine("[NUI] ["+item.Index+"] ["+item.Position.X+", "+item.Position.Y+" ==== \n");
410 //Console.WriteLine("Realize Done");
414 public override void NotifyItemSizeChanged(RecyclerViewItem item)
416 // All Item size need to be same in grid!
417 // if you want to change item size, change dataTemplate to re-initing.
422 [EditorBrowsable(EditorBrowsableState.Never)]
423 public override float CalculateLayoutOrientationSize()
425 //Console.WriteLine("[NUI] Calculate Layout ScrollContentSize {0}", ScrollContentSize);
426 return ScrollContentSize;
430 [EditorBrowsable(EditorBrowsableState.Never)]
431 public override float CalculateCandidateScrollPosition(float scrollPosition)
433 //Console.WriteLine("[NUI] Calculate Candidate ScrollContentSize {0}", ScrollContentSize);
434 return scrollPosition;
438 [EditorBrowsable(EditorBrowsableState.Never)]
439 public override View RequestNextFocusableView(View currentFocusedView, View.FocusDirection direction, bool loopEnabled)
441 if (currentFocusedView == null)
442 throw new ArgumentNullException(nameof(currentFocusedView));
444 View nextFocusedView = null;
445 int targetSibling = -1;
446 bool IsHorizontal = colView.ScrollingDirection == ScrollableBase.Direction.Horizontal;
450 case View.FocusDirection.Left:
452 targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder - 1 : targetSibling;
455 case View.FocusDirection.Right:
457 targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder + 1 : targetSibling;
460 case View.FocusDirection.Up:
462 targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder - 1;
465 case View.FocusDirection.Down:
467 targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder + 1;
472 if (targetSibling > -1 && targetSibling < Container.Children.Count)
474 RecyclerViewItem candidate = Container.Children[targetSibling] as RecyclerViewItem;
475 if (candidate.Index >= 0 && candidate.Index < colView.InternalItemSource.Count)
477 nextFocusedView = candidate;
480 return nextFocusedView;
484 [EditorBrowsable(EditorBrowsableState.Never)]
485 protected override (int start, int end) FindVisibleItems((float X, float Y) visibleArea)
487 int MaxIndex = colView.InternalItemSource.Count - 1 - (hasFooter ? 1 : 0);
488 int adds = spanSize * 2;
490 (int start, int end) found = (0, 0);
493 if (hasHeader && visibleArea.X < headerSize + (IsHorizontal? Padding.Start : Padding.Top))
502 foreach (GroupInfo gInfo in groups)
506 if (gInfo.GroupPosition <= visibleArea.X &&
507 gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.X)
509 if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.X)
511 found.start = gInfo.StartIndex - adds;
514 //can be step in spanSize...
515 for (int i = 1; i < gInfo.Count; i++)
518 // Reach last index of group.
519 if (i == (gInfo.Count - 1))
521 found.start = gInfo.StartIndex + i - adds;
526 else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.X - gInfo.GroupPosition - groupHeaderSize)
528 found.start = gInfo.StartIndex + i - adds;
538 found.start = MaxIndex;
543 float visibleAreaX = visibleArea.X - (hasHeader ? headerSize : 0);
544 found.start = (Convert.ToInt32(Math.Abs(visibleAreaX / StepCandidate)) - 1) * spanSize;
545 if (hasHeader) found.start += 1;
547 if (found.start < 0) found.start = 0;
550 if (hasFooter && visibleArea.Y > ScrollContentSize - footerSize - (IsHorizontal? Padding.End : Padding.Bottom))
552 found.end = MaxIndex + 1;
559 // can it be start from founded group...?
560 //foreach(GroupInfo gInfo in groups.Skip(skipGroup))
561 foreach (GroupInfo gInfo in groups)
564 if (gInfo.GroupPosition <= visibleArea.Y &&
565 gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.Y)
567 if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.Y)
569 found.end = gInfo.StartIndex + adds;
572 //can be step in spanSize...
573 for (int i = 1; i < gInfo.Count; i++)
576 // Reach last index of group.
577 if (i == (gInfo.Count - 1))
579 found.end = gInfo.StartIndex + i + adds;
583 else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.Y - gInfo.GroupPosition - groupHeaderSize)
585 found.end = gInfo.StartIndex + i + adds;
595 found.start = MaxIndex;
600 float visibleAreaY = visibleArea.Y - (hasHeader ? headerSize : 0);
601 //Need to Consider GroupHeight!!!!
602 found.end = (Convert.ToInt32(Math.Abs(visibleAreaY / StepCandidate)) + 1) * spanSize + adds;
603 if (hasHeader) found.end += 1;
605 if (found.end > (MaxIndex)) found.end = MaxIndex;
610 internal override (float X, float Y) GetItemPosition(int index)
613 int spaceStartX = Padding.Start;
614 int spaceStartY = Padding.Top;
615 int emptyArea = IsHorizontal?
616 (int)(colView.Size.Height - Padding.Top - Padding.Bottom - (sizeCandidate.Height * spanSize)) :
617 (int)(colView.Size.Width - Padding.Start - Padding.End - (sizeCandidate.Width * spanSize));
619 if (hasHeader && index == 0)
621 return (spaceStartX + headerMargin.Start, spaceStartY + headerMargin.Top);
623 if (hasFooter && index == colView.InternalItemSource.Count - 1)
626 ScrollContentSize - Padding.End - footerSize + footerMargin.Start:
630 ScrollContentSize - Padding.Bottom - footerSize + footerMargin.Top;
635 GroupInfo myGroup = GetGroupInfo(index);
636 if (colView.InternalItemSource.IsGroupHeader(index))
638 spaceStartX+= groupHeaderMargin.Start;
639 spaceStartY+= groupHeaderMargin.Top;
641 myGroup.GroupPosition + groupHeaderMargin.Start:
645 myGroup.GroupPosition + groupHeaderMargin.Top;
647 else if (colView.InternalItemSource.IsGroupFooter(index))
649 spaceStartX+= groupFooterMargin.Start;
650 spaceStartY+= groupFooterMargin.Top;
652 myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize + groupFooterMargin.Start:
656 myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize + groupFooterMargin.Top;
660 int pureIndex = index - myGroup.StartIndex - 1;
661 int division = pureIndex / spanSize;
662 int remainder = pureIndex % spanSize;
663 if (division < 0) division = 0;
664 if (remainder < 0) remainder = 0;
665 spaceStartX+= CandidateMargin.Start;
666 spaceStartY+= CandidateMargin.Top;
669 (division * sizeCandidate.Width) + myGroup.GroupPosition + groupHeaderSize + CandidateMargin.Start:
670 (emptyArea * align) + (remainder * sizeCandidate.Width) + spaceStartX;
672 (emptyArea * align) + (remainder * sizeCandidate.Height) + spaceStartY:
673 (division * sizeCandidate.Height) + myGroup.GroupPosition + groupHeaderSize + CandidateMargin.Top;
678 int pureIndex = index - (colView.Header ? 1 : 0);
679 // int convert must be truncate value.
680 int division = pureIndex / spanSize;
681 int remainder = pureIndex % spanSize;
682 if (division < 0) division = 0;
683 if (remainder < 0) remainder = 0;
684 spaceStartX+= CandidateMargin.Start;
685 spaceStartY+= CandidateMargin.Top;
688 (division * sizeCandidate.Width) + (hasHeader? headerSize : 0) + spaceStartX:
689 (emptyArea * align) + (remainder * sizeCandidate.Width) + spaceStartX;
691 (emptyArea * align) + (remainder * sizeCandidate.Height) + spaceStartY:
692 (division * sizeCandidate.Height) + (hasHeader? headerSize : 0) + spaceStartY;
698 internal override (float Width, float Height) GetItemSize(int index)
700 return (sizeCandidate.Width - CandidateMargin.Start - CandidateMargin.End,
701 sizeCandidate.Height - CandidateMargin.Top - CandidateMargin.Bottom);
704 private RecyclerViewItem GetVisibleItem(int index)
706 foreach (RecyclerViewItem item in VisibleItems)
708 if (item.Index == index) return item;
714 private GroupInfo GetGroupInfo(int index)
718 if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index)
721 if (hasHeader && index == 0) return null;
722 foreach (GroupInfo group in groups)
724 if (group.StartIndex <= index && group.StartIndex + group.Count > index)
735 private object GetGroupParent(int index)
739 if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index)
740 return Visited.GroupParent;
742 if (hasHeader && index == 0) return null;
743 foreach (GroupInfo group in groups)
745 if (group.StartIndex <= index && group.StartIndex + group.Count > index)
748 return group.GroupParent;
758 public object GroupParent;
759 public int StartIndex;
761 public float GroupSize;
762 public float GroupPosition;
763 //Items relative position from the GroupPosition