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.
18 using Tizen.NUI.BaseComponents;
19 using System.Collections;
20 using System.Collections.Generic;
21 using System.ComponentModel;
22 using Tizen.NUI.Binding;
24 namespace Tizen.NUI.Components
29 /// [Draft] This class implements a linear box layout.
31 [EditorBrowsable(EditorBrowsableState.Never)]
32 public class LinearLayouter : ItemsLayouter
34 private readonly List<float> ItemPosition = new List<float>();
35 private readonly List<float> ItemSize = new List<float>();
36 private int ItemSizeChanged = -1;
37 private CollectionView colView;
38 private bool hasHeader;
39 private float headerSize;
40 private bool hasFooter;
41 private float footerSize;
42 private bool isGrouped;
43 private readonly List<GroupInfo> groups = new List<GroupInfo>();
44 private float groupHeaderSize;
45 private float groupFooterSize;
46 private GroupInfo Visited;
49 /// Clean up ItemsLayouter.
51 /// <param name="view"> ItemsView of layouter.</param>
52 [EditorBrowsable(EditorBrowsableState.Never)]
53 public override void Initialize(RecyclerView view)
55 colView = view as CollectionView;
58 throw new ArgumentException("LinearLayouter only can be applied CollectionView.", nameof(view));
61 foreach (RecyclerViewItem item in VisibleItems)
63 colView.UnrealizeItem(item, false);
73 IsHorizontal = (colView.ScrollingDirection == ScrollableBase.Direction.Horizontal);
75 RecyclerViewItem header = colView?.Header;
76 RecyclerViewItem footer = colView?.Footer;
78 int count = colView.InternalItemSource.Count;
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 headerSize = IsHorizontal ? width : height;
90 colView.UnrealizeItem(header);
92 else hasHeader = false;
96 MeasureChild(colView, footer);
98 width = footer.Layout != null ? footer.Layout.MeasuredWidth.Size.AsRoundedValue() : 0;
99 height = footer.Layout != null ? footer.Layout.MeasuredHeight.Size.AsRoundedValue() : 0;
101 footerSize = IsHorizontal ? width : height;
102 footer.Index = count - 1;
105 colView.UnrealizeItem(footer);
107 else hasFooter = false;
109 //No Internal Source exist.
110 if (count == (hasHeader ? (hasFooter ? 2 : 1) : 0)) return;
112 int firstIndex = hasHeader ? 1 : 0;
114 if (colView.IsGrouped)
118 if (colView.GroupHeaderTemplate != null)
120 while (!colView.InternalItemSource.IsGroupHeader(firstIndex)) firstIndex++;
121 //must be always true
122 if (colView.InternalItemSource.IsGroupHeader(firstIndex))
124 RecyclerViewItem groupHeader = colView.RealizeItem(firstIndex);
127 if (groupHeader == null) throw new Exception("[" + firstIndex + "] Group Header failed to realize!");
129 // Need to Set proper height or width on scroll direction.
130 if (groupHeader.Layout == null)
132 width = groupHeader.WidthSpecification;
133 height = groupHeader.HeightSpecification;
137 MeasureChild(colView, groupHeader);
139 width = groupHeader.Layout.MeasuredWidth.Size.AsRoundedValue();
140 height = groupHeader.Layout.MeasuredHeight.Size.AsRoundedValue();
142 //Console.WriteLine("[NUI] GroupHeader Size {0} :{0}", width, height);
143 // pick the StepCandidate.
144 groupHeaderSize = IsHorizontal ? width : height;
145 colView.UnrealizeItem(groupHeader);
150 groupHeaderSize = 0F;
153 if (colView.GroupFooterTemplate != null)
155 int firstFooter = firstIndex;
156 while (!colView.InternalItemSource.IsGroupFooter(firstFooter)) firstFooter++;
157 //must be always true
158 if (colView.InternalItemSource.IsGroupFooter(firstFooter))
160 RecyclerViewItem groupFooter = colView.RealizeItem(firstFooter);
162 if (groupFooter == null) throw new Exception("[" + firstFooter + "] Group Footer failed to realize!");
163 // Need to Set proper height or width on scroll direction.
164 if (groupFooter.Layout == null)
166 width = groupFooter.WidthSpecification;
167 height = groupFooter.HeightSpecification;
171 MeasureChild(colView, groupFooter);
173 width = groupFooter.Layout.MeasuredWidth.Size.AsRoundedValue();
174 height = groupFooter.Layout.MeasuredHeight.Size.AsRoundedValue();
176 // pick the StepCandidate.
177 groupFooterSize = IsHorizontal ? width : height;
179 colView.UnrealizeItem(groupFooter);
184 groupFooterSize = 0F;
187 else isGrouped = false;
190 //Final Check of FirstIndex
191 while (colView.InternalItemSource.IsHeader(firstIndex) ||
192 colView.InternalItemSource.IsGroupHeader(firstIndex) ||
193 colView.InternalItemSource.IsGroupFooter(firstIndex))
195 if (colView.InternalItemSource.IsFooter(firstIndex))
206 RecyclerViewItem sizeDeligate = colView.RealizeItem(firstIndex);
207 if (sizeDeligate == null)
210 throw new Exception("Cannot create content from DatTemplate.");
213 sizeDeligate.BindingContext = colView.InternalItemSource.GetItem(firstIndex);
215 // Need to Set proper height or width on scroll direction.
216 if (sizeDeligate.Layout == null)
218 width = sizeDeligate.WidthSpecification;
219 height = sizeDeligate.HeightSpecification;
223 MeasureChild(colView, sizeDeligate);
225 width = sizeDeligate.Layout.MeasuredWidth.Size.AsRoundedValue();
226 height = sizeDeligate.Layout.MeasuredHeight.Size.AsRoundedValue();
228 //Console.WriteLine("[NUI] Layout Size {0} :{0}", width, height);
229 // pick the StepCandidate.
230 StepCandidate = IsHorizontal ? width : height;
231 if (StepCandidate == 0) StepCandidate = 1; //????
233 colView.UnrealizeItem(sizeDeligate);
236 float Current = 0.0F;
237 IGroupableItemSource source = colView.InternalItemSource;
238 GroupInfo currentGroup = null;
239 for (int i = 0; i < count; i++)
241 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
243 if (i == 0 && hasHeader)
244 ItemSize.Add(headerSize);
245 else if (i == count - 1 && hasFooter)
246 ItemSize.Add(footerSize);
247 else if (source.IsGroupHeader(i))
248 ItemSize.Add(groupHeaderSize);
249 else if (source.IsGroupFooter(i))
250 ItemSize.Add(groupFooterSize);
251 else ItemSize.Add(StepCandidate);
255 if (i == 0 && hasHeader)
257 //ItemPosition.Add(Current);
258 Current += headerSize;
260 else if (i == count - 1 && hasFooter)
262 //ItemPosition.Add(Current);
263 Current += footerSize;
267 //GroupHeader must always exist in group usage.
268 if (source.IsGroupHeader(i))
270 currentGroup = new GroupInfo()
272 GroupParent = source.GetGroupParent(i),
277 GroupSize = groupHeaderSize,
278 GroupPosition = Current
280 currentGroup.ItemPosition.Add(0);
281 groups.Add(currentGroup);
282 Current += groupHeaderSize;
285 else if (source.IsGroupFooter(i))
287 //currentGroup.hasFooter = true;
288 currentGroup.Count++;
289 currentGroup.GroupSize += groupFooterSize;
290 currentGroup.ItemPosition.Add(Current - currentGroup.GroupPosition);
291 Current += groupFooterSize;
295 currentGroup.Count++;
296 currentGroup.GroupSize += StepCandidate;
297 currentGroup.ItemPosition.Add(Current - currentGroup.GroupPosition);
298 Current += StepCandidate;
304 ItemPosition.Add(Current);
306 if (i == 0 && hasHeader) Current += headerSize;
307 else if (i == count - 1 && hasFooter) Current += footerSize;
308 else Current += StepCandidate;
312 ScrollContentSize = Current;
313 if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize;
314 else colView.ContentContainer.SizeHeight = ScrollContentSize;
317 base.Initialize(view);
318 //Console.WriteLine("[NUI] Init Done, StepCnadidate{0}, Scroll{1}", StepCandidate, ScrollContentSize);
322 /// This is called to find out where items are lain out according to current scroll position.
324 /// <param name="scrollPosition">Scroll position which is calculated by ScrollableBase</param>
325 /// <param name="force">boolean force flag to layouting forcely.</param>
326 [EditorBrowsable(EditorBrowsableState.Never)]
327 public override void RequestLayout(float scrollPosition, bool force = false)
329 // Layouting is only possible after once it initialized.
330 if (!IsInitialized) return;
331 int LastIndex = colView.InternalItemSource.Count - 1;
333 if (!force && PrevScrollPosition == Math.Abs(scrollPosition)) return;
334 PrevScrollPosition = Math.Abs(scrollPosition);
336 if (ItemSizeChanged >= 0)
338 for (int i = ItemSizeChanged; i <= LastIndex; i++)
340 ScrollContentSize = ItemPosition[LastIndex - 1] + GetItemSize(LastIndex);
343 int prevFirstVisible = FirstVisible;
344 int prevLastVisible = LastVisible;
346 (float X, float Y) visibleArea = (PrevScrollPosition,
347 PrevScrollPosition + (IsHorizontal ? colView.Size.Width : colView.Size.Height)
350 // 1. Set First/Last Visible Item Index.
351 (int start, int end) = FindVisibleItems(visibleArea);
352 FirstVisible = start;
355 // 2. Unrealize invisible items.
356 List<RecyclerViewItem> unrealizedItems = new List<RecyclerViewItem>();
357 foreach (RecyclerViewItem item in VisibleItems)
359 if (item.Index < FirstVisible || item.Index > LastVisible)
361 //Console.WriteLine("[NUI] Unrealize{0}!", item.Index);
362 unrealizedItems.Add(item);
363 colView.UnrealizeItem(item);
366 VisibleItems.RemoveAll(unrealizedItems.Contains);
368 // 3. Realize and placing visible items.
369 for (int i = FirstVisible; i <= LastVisible; i++)
371 RecyclerViewItem item = null;
372 // 4. Get item if visible or realize new.
373 if (i >= prevFirstVisible && i <= prevLastVisible)
375 item = GetVisibleItem(i);
378 if (item == null) item = colView.RealizeItem(i);
380 VisibleItems.Add(item);
383 float posX = 0F, posY = 0F;
387 if (colView.Header == item)
392 else if (colView.Footer == item)
394 posX = (IsHorizontal ? ScrollContentSize - item.SizeWidth : 0F);
395 posY = (IsHorizontal ? 0F : ScrollContentSize - item.SizeHeight);
399 GroupInfo gInfo = GetGroupInfo(i);
400 posX = (IsHorizontal ? gInfo.GroupPosition + gInfo.ItemPosition[i - gInfo.StartIndex] : 0F);
401 posY = (IsHorizontal ? 0F : gInfo.GroupPosition + gInfo.ItemPosition[i - gInfo.StartIndex]);
406 posX = (IsHorizontal ? ItemPosition[i] : 0F);
407 posY = (IsHorizontal ? 0F : ItemPosition[i]);
410 item.Position = new Position(posX, posY);
411 //Console.WriteLine("[NUI] ["+item+"]["+item.Index+"] :: ["+item.Position.X+", "+item.Position.Y+"] ==== \n");
416 [EditorBrowsable(EditorBrowsableState.Never)]
417 public override (float X, float Y) GetItemPosition(object item)
419 if (item == null) throw new ArgumentNullException(nameof(item));
420 // Layouting Items in scrollPosition.
421 float pos = ItemPosition[colView.InternalItemSource.GetPosition(item)];
423 return (IsHorizontal ? (pos, 0.0F) : (0.0F, pos));
427 [EditorBrowsable(EditorBrowsableState.Never)]
428 public override (float X, float Y) GetItemSize(object item)
430 if (item == null) throw new ArgumentNullException(nameof(item));
431 // Layouting Items in scrollPosition.
432 float size = GetItemSize(colView.InternalItemSource.GetPosition(item));
433 float view = (IsHorizontal ? colView.Size.Height : colView.Size.Width);
435 return (IsHorizontal ? (size, view) : (view, size));
439 [EditorBrowsable(EditorBrowsableState.Never)]
440 public override void NotifyItemSizeChanged(RecyclerViewItem item)
443 throw new ArgumentNullException(nameof(item));
445 if (!IsInitialized ||
446 (colView.SizingStrategy == ItemSizingStrategy.MeasureFirst &&
451 float PrevSize, CurrentSize;
452 if (item.Index == (colView.InternalItemSource.Count - 1))
454 PrevSize = ScrollContentSize - ItemPosition[item.Index];
458 PrevSize = ItemPosition[item.Index + 1] - ItemPosition[item.Index];
461 CurrentSize = (IsHorizontal ? item.Size.Width : item.Size.Height);
463 if (CurrentSize != PrevSize)
465 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
466 ItemSize[item.Index] = CurrentSize;
468 StepCandidate = CurrentSize;
470 if (ItemSizeChanged == -1) ItemSizeChanged = item.Index;
471 else ItemSizeChanged = Math.Min(ItemSizeChanged, item.Index);
473 //ScrollContentSize += Diff; UpdateOnce?
477 [EditorBrowsable(EditorBrowsableState.Never)]
478 public override float CalculateLayoutOrientationSize()
480 //Console.WriteLine("[NUI] Calculate Layout ScrollContentSize {0}", ScrollContentSize);
481 return ScrollContentSize;
485 [EditorBrowsable(EditorBrowsableState.Never)]
486 public override float CalculateCandidateScrollPosition(float scrollPosition)
488 //Console.WriteLine("[NUI] Calculate Candidate ScrollContentSize {0}", ScrollContentSize);
489 return scrollPosition;
493 [EditorBrowsable(EditorBrowsableState.Never)]
494 public override View RequestNextFocusableView(View currentFocusedView, View.FocusDirection direction, bool loopEnabled)
496 if (currentFocusedView == null)
497 throw new ArgumentNullException(nameof(currentFocusedView));
499 View nextFocusedView = null;
500 int targetSibling = -1;
504 case View.FocusDirection.Left:
506 targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder - 1 : targetSibling;
509 case View.FocusDirection.Right:
511 targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder + 1 : targetSibling;
514 case View.FocusDirection.Up:
516 targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder - 1;
519 case View.FocusDirection.Down:
521 targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder + 1;
526 if (targetSibling > -1 && targetSibling < Container.Children.Count)
528 RecyclerViewItem candidate = Container.Children[targetSibling] as RecyclerViewItem;
529 if (candidate.Index >= 0 && candidate.Index < colView.InternalItemSource.Count)
531 nextFocusedView = candidate;
535 return nextFocusedView;
539 [EditorBrowsable(EditorBrowsableState.Never)]
540 protected override (int start, int end) FindVisibleItems((float X, float Y) visibleArea)
542 int MaxIndex = colView.InternalItemSource.Count - 1 - (hasFooter ? 1 : 0);
545 (int start, int end) found = (0, 0);
547 // 1. Find the start index.
549 if (hasHeader && visibleArea.X <= headerSize)
558 foreach (GroupInfo gInfo in groups)
562 if (gInfo.GroupPosition <= visibleArea.X &&
563 gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.X)
565 for (int i = 0; i < gInfo.Count; i++)
567 // Reach last index of group.
568 if (i == (gInfo.Count - 1))
570 found.start = gInfo.StartIndex + i - adds;
575 else if (gInfo.ItemPosition[i] <= visibleArea.X - gInfo.GroupPosition &&
576 gInfo.ItemPosition[i + 1] >= visibleArea.X - gInfo.GroupPosition)
578 found.start = gInfo.StartIndex + i - adds;
588 found.start = MaxIndex;
593 float visibleAreaX = visibleArea.X - (hasHeader ? headerSize : 0);
594 found.start = (Convert.ToInt32(Math.Abs(visibleAreaX / StepCandidate)) - adds);
597 if (found.start < 0) found.start = 0;
600 if (hasFooter && visibleArea.Y > ScrollContentSize - footerSize)
602 found.end = MaxIndex + 1;
609 // can it be start from founded group...?
610 //foreach(GroupInfo gInfo in groups.Skip(skipGroup))
611 foreach (GroupInfo gInfo in groups)
614 if (gInfo.GroupPosition <= visibleArea.Y &&
615 gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.Y)
617 for (int i = 0; i < gInfo.Count; i++)
619 if (i == (gInfo.Count - 1))
621 //Should be groupFooter!
622 found.end = gInfo.StartIndex + i + adds;
627 else if (gInfo.ItemPosition[i] <= visibleArea.Y - gInfo.GroupPosition &&
628 gInfo.ItemPosition[i + 1] >= visibleArea.Y - gInfo.GroupPosition)
630 found.end = gInfo.StartIndex + i + adds;
637 if (failed) found.end = MaxIndex;
641 float visibleAreaY = visibleArea.Y - (hasHeader ? headerSize : 0);
642 found.end = (Convert.ToInt32(Math.Abs(visibleAreaY / StepCandidate)) + adds);
643 if (hasHeader) found.end += 1;
645 if (found.end > (MaxIndex)) found.end = MaxIndex;
650 private float GetItemSize(int index)
652 if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll)
654 return ItemSize[index];
658 if (index == 0 && hasHeader)
660 if (index == colView.InternalItemSource.Count - 1 && hasFooter)
662 return StepCandidate;
666 private void UpdatePosition(int index)
668 bool IsGroup = (colView.InternalItemSource is IGroupableItemSource);
670 if (index <= 0) return;
671 if (index >= colView.InternalItemSource.Count)
675 //IsGroupHeader = (colView.InternalItemSource as IGroupableItemSource).IsGroupHeader(index);
676 //IsGroupFooter = (colView.InternalItemSource as IGroupableItemSource).IsGroupFooter(index);
680 ItemPosition[index] = ItemPosition[index - 1] + GetItemSize(index - 1);
683 private RecyclerViewItem GetVisibleItem(int index)
685 foreach (RecyclerViewItem item in VisibleItems)
687 if (item.Index == index) return item;
692 private GroupInfo GetGroupInfo(int index)
696 if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index)
699 if (hasHeader && index == 0) return null;
700 foreach (GroupInfo group in groups)
702 if (group.StartIndex <= index && group.StartIndex + group.Count > index)
712 private object GetGroupParent(int index)
716 if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index)
717 return Visited.GroupParent;
719 if (hasHeader && index == 0) return null;
720 foreach (GroupInfo group in groups)
722 if (group.StartIndex <= index && group.StartIndex + group.Count > index)
725 return group.GroupParent;
734 public object GroupParent;
735 public int StartIndex;
737 public float GroupSize;
738 public float GroupPosition;
739 //Items relative position from the GroupPosition
740 public List<float> ItemPosition = new List<float>();