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 Size2D sizeCandidate;
31 private int spanSize = 1;
32 private float align = 0.5f;
33 private bool hasHeader;
34 private float headerSize;
35 private bool hasFooter;
36 private float footerSize;
37 private bool isGrouped;
38 private readonly List<GroupInfo> groups = new List<GroupInfo>();
39 private float groupHeaderSize;
40 private float groupFooterSize;
41 private GroupInfo Visited;
44 /// Clean up ItemsLayouter.
46 /// <param name="view"> ItemsView of layouter. </param>
47 [EditorBrowsable(EditorBrowsableState.Never)]
48 public override void Initialize(RecyclerView view)
50 colView = view as CollectionView;
53 throw new ArgumentException("GridLayouter only can be applied CollectionView.", nameof(view));
57 foreach (RecyclerViewItem item in VisibleItems)
59 colView.UnrealizeItem(item, false);
67 IsHorizontal = (colView.ScrollingDirection == ScrollableBase.Direction.Horizontal);
69 RecyclerViewItem header = colView?.Header;
70 RecyclerViewItem footer = colView?.Footer;
72 int count = colView.InternalItemSource.Count;
73 int pureCount = count - (header ? 1 : 0) - (footer ? 1 : 0);
75 // 2. Get the header / footer and size deligated item and measure the size.
78 MeasureChild(colView, header);
80 width = header.Layout != null ? header.Layout.MeasuredWidth.Size.AsRoundedValue() : 0;
81 height = header.Layout != null ? header.Layout.MeasuredHeight.Size.AsRoundedValue() : 0;
83 headerSize = IsHorizontal ? width : height;
86 colView.UnrealizeItem(header);
91 MeasureChild(colView, footer);
93 width = footer.Layout != null ? footer.Layout.MeasuredWidth.Size.AsRoundedValue() : 0;
94 height = footer.Layout != null ? footer.Layout.MeasuredHeight.Size.AsRoundedValue() : 0;
96 footerSize = IsHorizontal ? width : height;
97 footer.Index = count - 1;
100 colView.UnrealizeItem(footer);
103 int firstIndex = header ? 1 : 0;
105 if (colView.IsGrouped)
109 if (colView.GroupHeaderTemplate != null)
111 while (!colView.InternalItemSource.IsGroupHeader(firstIndex)) firstIndex++;
112 //must be always true
113 if (colView.InternalItemSource.IsGroupHeader(firstIndex))
115 RecyclerViewItem groupHeader = colView.RealizeItem(firstIndex);
118 if (groupHeader == null) throw new Exception("[" + firstIndex + "] Group Header failed to realize!");
120 // Need to Set proper hieght or width on scroll direciton.
121 if (groupHeader.Layout == null)
123 width = groupHeader.WidthSpecification;
124 height = groupHeader.HeightSpecification;
128 MeasureChild(colView, groupHeader);
130 width = groupHeader.Layout.MeasuredWidth.Size.AsRoundedValue();
131 height = groupHeader.Layout.MeasuredHeight.Size.AsRoundedValue();
133 //Console.WriteLine("[NUI] GroupHeader Size {0} :{0}", width, height);
134 // pick the StepCandidate.
135 groupHeaderSize = IsHorizontal ? width : height;
136 colView.UnrealizeItem(groupHeader);
141 groupHeaderSize = 0F;
144 if (colView.GroupFooterTemplate != null)
146 int firstFooter = firstIndex;
147 while (!colView.InternalItemSource.IsGroupFooter(firstFooter)) firstFooter++;
148 //must be always true
149 if (colView.InternalItemSource.IsGroupFooter(firstFooter))
151 RecyclerViewItem groupFooter = colView.RealizeItem(firstFooter);
153 if (groupFooter == null) throw new Exception("[" + firstFooter + "] Group Footer failed to realize!");
154 // Need to Set proper hieght or width on scroll direciton.
155 if (groupFooter.Layout == null)
157 width = groupFooter.WidthSpecification;
158 height = groupFooter.HeightSpecification;
162 MeasureChild(colView, groupFooter);
164 width = groupFooter.Layout.MeasuredWidth.Size.AsRoundedValue();
165 height = groupFooter.Layout.MeasuredHeight.Size.AsRoundedValue();
167 // pick the StepCandidate.
168 groupFooterSize = IsHorizontal ? width : height;
170 colView.UnrealizeItem(groupFooter);
175 groupFooterSize = 0F;
178 else isGrouped = false;
181 //Final Check of FirstIndex
182 while (colView.InternalItemSource.IsHeader(firstIndex) ||
183 colView.InternalItemSource.IsGroupHeader(firstIndex) ||
184 colView.InternalItemSource.IsGroupFooter(firstIndex))
186 if (colView.InternalItemSource.IsFooter(firstIndex))
195 sizeCandidate = new Size2D(0, 0);
198 // Get Size Deligate. FIXME if group exist index must be changed.
199 RecyclerViewItem sizeDeligate = colView.RealizeItem(firstIndex);
200 if (sizeDeligate == null)
202 throw new Exception("Cannot create content from DatTemplate.");
204 sizeDeligate.BindingContext = colView.InternalItemSource.GetItem(firstIndex);
206 // Need to Set proper hieght or width on scroll direciton.
207 if (sizeDeligate.Layout == null)
209 width = sizeDeligate.WidthSpecification;
210 height = sizeDeligate.HeightSpecification;
214 MeasureChild(colView, sizeDeligate);
216 width = sizeDeligate.Layout.MeasuredWidth.Size.AsRoundedValue();
217 height = sizeDeligate.Layout.MeasuredHeight.Size.AsRoundedValue();
219 //Console.WriteLine("[NUI] item Size {0} :{1}", width, height);
221 // pick the StepCandidate.
222 StepCandidate = IsHorizontal ? width : height;
223 spanSize = IsHorizontal ? Convert.ToInt32(Math.Truncate((double)(colView.Size.Height / height))) :
224 Convert.ToInt32(Math.Truncate((double)(colView.Size.Width / width)));
226 sizeCandidate = new Size2D(Convert.ToInt32(width), Convert.ToInt32(height));
228 colView.UnrealizeItem(sizeDeligate);
231 if (StepCandidate < 1) StepCandidate = 1;
232 if (spanSize < 1) spanSize = 1;
236 float Current = 0.0F;
237 IGroupableItemSource source = colView.InternalItemSource;
238 GroupInfo currentGroup = null;
240 for (int i = 0; i < count; i++)
242 if (i == 0 && hasHeader)
244 Current += headerSize;
246 else if (i == count - 1 && hasFooter)
248 Current += footerSize;
252 //GroupHeader must always exist in group usage.
253 if (source.IsGroupHeader(i))
255 currentGroup = new GroupInfo()
257 GroupParent = source.GetGroupParent(i),
260 GroupSize = groupHeaderSize,
261 GroupPosition = Current
263 groups.Add(currentGroup);
264 Current += groupHeaderSize;
267 else if (source.IsGroupFooter(i))
269 //currentGroup.hasFooter = true;
270 currentGroup.Count++;
271 currentGroup.GroupSize += groupFooterSize;
272 Current += groupFooterSize;
276 currentGroup.Count++;
277 int index = i - currentGroup.StartIndex - 1; // groupHeader must always exist.
278 if ((index % spanSize) == 0)
280 currentGroup.GroupSize += StepCandidate;
281 Current += StepCandidate;
286 ScrollContentSize = Current;
290 // 3. Measure the scroller content size.
291 ScrollContentSize = StepCandidate * Convert.ToInt32(Math.Ceiling((double)pureCount / (double)spanSize));
292 if (hasHeader) ScrollContentSize += headerSize;
293 if (hasFooter) ScrollContentSize += footerSize;
296 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
297 else colView.ContentContainer.SizeHeight = ScrollContentSize;
299 base.Initialize(colView);
300 //Console.WriteLine("Init Done, StepCnadidate{0}, spanSize{1}, Scroll{2}", StepCandidate, spanSize, ScrollContentSize);
304 /// This is called to find out where items are lain out according to current scroll position.
306 /// <param name="scrollPosition">Scroll position which is calculated by ScrollableBase</param>
307 /// <param name="force">boolean force flag to layouting forcely.</param>
308 public override void RequestLayout(float scrollPosition, bool force = false)
310 // Layouting is only possible after once it intialized.
311 if (!IsInitialized) return;
312 int LastIndex = colView.InternalItemSource.Count;
314 if (!force && PrevScrollPosition == Math.Abs(scrollPosition)) return;
315 PrevScrollPosition = Math.Abs(scrollPosition);
317 int prevFirstVisible = FirstVisible;
318 int prevLastVisible = LastVisible;
319 bool IsHorizontal = (colView.ScrollingDirection == ScrollableBase.Direction.Horizontal);
321 (float X, float Y) visibleArea = (PrevScrollPosition,
322 PrevScrollPosition + (IsHorizontal ? colView.Size.Width : colView.Size.Height)
325 //Console.WriteLine("[NUI] itemsView [{0},{1}] [{2},{3}]", colView.Size.Width, colView.Size.Height, colView.ContentContainer.Size.Width, colView.ContentContainer.Size.Height);
327 // 1. Set First/Last Visible Item Index.
328 (int start, int end) = FindVisibleItems(visibleArea);
329 FirstVisible = start;
332 //Console.WriteLine("[NUI] {0} :visibleArea before [{1},{2}] after [{3},{4}]", scrollPosition, prevFirstVisible, prevLastVisible, FirstVisible, LastVisible);
334 // 2. Unrealize invisible items.
335 List<RecyclerViewItem> unrealizedItems = new List<RecyclerViewItem>();
336 foreach (RecyclerViewItem item in VisibleItems)
338 if (item.Index < FirstVisible || item.Index > LastVisible)
340 //Console.WriteLine("[NUI] Unrealize{0}!", item.Index);
341 unrealizedItems.Add(item);
342 colView.UnrealizeItem(item);
345 VisibleItems.RemoveAll(unrealizedItems.Contains);
347 //Console.WriteLine("Realize Begin [{0} to {1}]", FirstVisible, LastVisible);
348 // 3. Realize and placing visible items.
349 for (int i = FirstVisible; i <= LastVisible; i++)
351 //Console.WriteLine("[NUI] Realize!");
352 RecyclerViewItem item = null;
353 // 4. Get item if visible or realize new.
354 if (i >= prevFirstVisible && i <= prevLastVisible)
356 item = GetVisibleItem(i);
359 if (item == null) item = colView.RealizeItem(i);
360 VisibleItems.Add(item);
362 (float x, float y) = GetItemPosition(i);
364 item.Position = new Position(x, y);
365 //Console.WriteLine("[NUI] ["+item.Index+"] ["+item.Position.X+", "+item.Position.Y+" ==== \n");
367 //Console.WriteLine("Realize Done");
371 [EditorBrowsable(EditorBrowsableState.Never)]
372 public override (float X, float Y) GetItemPosition(object item)
374 if (item == null) throw new ArgumentNullException(nameof(item));
375 if (colView == null) return (0, 0);
377 return GetItemPosition(colView.InternalItemSource.GetPosition(item));
381 [EditorBrowsable(EditorBrowsableState.Never)]
382 public override (float X, float Y) GetItemSize(object item)
384 if (item == null) throw new ArgumentNullException(nameof(item));
385 if (sizeCandidate == null) return (0, 0);
389 int index = colView.InternalItemSource.GetPosition(item);
390 float view = (IsHorizontal ? colView.Size.Height : colView.Size.Width);
392 if (colView.InternalItemSource.IsGroupHeader(index))
394 return (IsHorizontal ? (groupHeaderSize, view) : (view, groupHeaderSize));
396 else if (colView.InternalItemSource.IsGroupFooter(index))
398 return (IsHorizontal ? (groupFooterSize, view) : (view, groupFooterSize));
402 return (sizeCandidate.Width, sizeCandidate.Height);
406 public override void NotifyItemSizeChanged(RecyclerViewItem item)
408 // All Item size need to be same in grid!
409 // if you want to change item size, change dataTemplate to re-initing.
414 [EditorBrowsable(EditorBrowsableState.Never)]
415 public override float CalculateLayoutOrientationSize()
417 //Console.WriteLine("[NUI] Calculate Layout ScrollContentSize {0}", ScrollContentSize);
418 return ScrollContentSize;
422 [EditorBrowsable(EditorBrowsableState.Never)]
423 public override float CalculateCandidateScrollPosition(float scrollPosition)
425 //Console.WriteLine("[NUI] Calculate Candidate ScrollContentSize {0}", ScrollContentSize);
426 return scrollPosition;
430 [EditorBrowsable(EditorBrowsableState.Never)]
431 public override View RequestNextFocusableView(View currentFocusedView, View.FocusDirection direction, bool loopEnabled)
433 if (currentFocusedView == null)
434 throw new ArgumentNullException(nameof(currentFocusedView));
436 View nextFocusedView = null;
437 int targetSibling = -1;
438 bool IsHorizontal = colView.ScrollingDirection == ScrollableBase.Direction.Horizontal;
442 case View.FocusDirection.Left:
444 targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder - 1 : targetSibling;
447 case View.FocusDirection.Right:
449 targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder + 1 : targetSibling;
452 case View.FocusDirection.Up:
454 targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder - 1;
457 case View.FocusDirection.Down:
459 targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder + 1;
464 if (targetSibling > -1 && targetSibling < Container.Children.Count)
466 RecyclerViewItem candidate = Container.Children[targetSibling] as RecyclerViewItem;
467 if (candidate.Index >= 0 && candidate.Index < colView.InternalItemSource.Count)
469 nextFocusedView = candidate;
472 return nextFocusedView;
476 [EditorBrowsable(EditorBrowsableState.Never)]
477 protected override (int start, int end) FindVisibleItems((float X, float Y) visibleArea)
479 int MaxIndex = colView.InternalItemSource.Count - 1 - (hasFooter ? 1 : 0);
480 int adds = spanSize * 2;
482 (int start, int end) found = (0, 0);
485 if (hasHeader && visibleArea.X < headerSize)
494 foreach (GroupInfo gInfo in groups)
498 if (gInfo.GroupPosition <= visibleArea.X &&
499 gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.X)
501 if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.X)
503 found.start = gInfo.StartIndex - adds;
506 //can be step in spanSize...
507 for (int i = 1; i < gInfo.Count; i++)
510 // Reach last index of group.
511 if (i == (gInfo.Count - 1))
513 found.start = gInfo.StartIndex + i - adds;
518 else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.X - gInfo.GroupPosition - groupHeaderSize)
520 found.start = gInfo.StartIndex + i - adds;
530 found.start = MaxIndex;
535 float visibleAreaX = visibleArea.X - (hasHeader ? headerSize : 0);
536 found.start = (Convert.ToInt32(Math.Abs(visibleAreaX / StepCandidate)) - 1) * spanSize;
537 if (hasHeader) found.start += 1;
539 if (found.start < 0) found.start = 0;
542 if (hasFooter && visibleArea.Y > ScrollContentSize - footerSize)
544 found.end = MaxIndex + 1;
551 // can it be start from founded group...?
552 //foreach(GroupInfo gInfo in groups.Skip(skipGroup))
553 foreach (GroupInfo gInfo in groups)
556 if (gInfo.GroupPosition <= visibleArea.Y &&
557 gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.Y)
559 if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.Y)
561 found.end = gInfo.StartIndex + adds;
564 //can be step in spanSize...
565 for (int i = 1; i < gInfo.Count; i++)
568 // Reach last index of group.
569 if (i == (gInfo.Count - 1))
571 found.end = gInfo.StartIndex + i + adds;
575 else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.Y - gInfo.GroupPosition - groupHeaderSize)
577 found.end = gInfo.StartIndex + i + adds;
587 found.start = MaxIndex;
592 float visibleAreaY = visibleArea.Y - (hasHeader ? headerSize : 0);
593 //Need to Consider GroupHeight!!!!
594 found.end = (Convert.ToInt32(Math.Abs(visibleAreaY / StepCandidate)) + 1) * spanSize + adds;
595 if (hasHeader) found.end += 1;
597 if (found.end > (MaxIndex)) found.end = MaxIndex;
602 private (float X, float Y) GetItemPosition(int index)
605 if (sizeCandidate == null) return (0, 0);
607 if (hasHeader && index == 0)
611 if (hasFooter && index == colView.InternalItemSource.Count - 1)
613 xPos = IsHorizontal ? ScrollContentSize - footerSize : 0;
614 yPos = IsHorizontal ? 0 : ScrollContentSize - footerSize;
619 GroupInfo myGroup = GetGroupInfo(index);
620 if (colView.InternalItemSource.IsGroupHeader(index))
622 xPos = IsHorizontal ? myGroup.GroupPosition : 0;
623 yPos = IsHorizontal ? 0 : myGroup.GroupPosition;
625 else if (colView.InternalItemSource.IsGroupFooter(index))
627 xPos = IsHorizontal ? myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize : 0;
628 yPos = IsHorizontal ? 0 : myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize;
632 int pureIndex = index - myGroup.StartIndex - 1;
633 int division = pureIndex / spanSize;
634 int remainder = pureIndex % spanSize;
635 int emptyArea = IsHorizontal ? (int)(colView.Size.Height - (sizeCandidate.Height * spanSize)) :
636 (int)(colView.Size.Width - (sizeCandidate.Width * spanSize));
637 if (division < 0) division = 0;
638 if (remainder < 0) remainder = 0;
640 xPos = IsHorizontal ? division * sizeCandidate.Width + myGroup.GroupPosition + groupHeaderSize : emptyArea * align + remainder * sizeCandidate.Width;
641 yPos = IsHorizontal ? emptyArea * align + remainder * sizeCandidate.Height : division * sizeCandidate.Height + myGroup.GroupPosition + groupHeaderSize;
646 int pureIndex = index - (colView.Header ? 1 : 0);
647 // int convert must be truncate value.
648 int division = pureIndex / spanSize;
649 int remainder = pureIndex % spanSize;
650 int emptyArea = IsHorizontal ? (int)(colView.Size.Height - (sizeCandidate.Height * spanSize)) :
651 (int)(colView.Size.Width - (sizeCandidate.Width * spanSize));
652 if (division < 0) division = 0;
653 if (remainder < 0) remainder = 0;
655 xPos = IsHorizontal ? division * sizeCandidate.Width + (hasHeader ? headerSize : 0) : emptyArea * align + remainder * sizeCandidate.Width;
656 yPos = IsHorizontal ? emptyArea * align + remainder * sizeCandidate.Height : division * sizeCandidate.Height + (hasHeader ? headerSize : 0);
662 private RecyclerViewItem GetVisibleItem(int index)
664 foreach (RecyclerViewItem item in VisibleItems)
666 if (item.Index == index) return item;
672 private GroupInfo GetGroupInfo(int index)
676 if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index)
679 if (hasHeader && index == 0) return null;
680 foreach (GroupInfo group in groups)
682 if (group.StartIndex <= index && group.StartIndex + group.Count > index)
693 private object GetGroupParent(int index)
697 if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index)
698 return Visited.GroupParent;
700 if (hasHeader && index == 0) return null;
701 foreach (GroupInfo group in groups)
703 if (group.StartIndex <= index && group.StartIndex + group.Count > index)
706 return group.GroupParent;
716 public object GroupParent;
717 public int StartIndex;
719 public float GroupSize;
720 public float GroupPosition;
721 //Items relative position from the GroupPosition