/* Copyright (c) 2021 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ using System; using System.Collections.Generic; using System.ComponentModel; using Tizen.NUI.BaseComponents; namespace Tizen.NUI.Components { /// /// Layouter for CollectionView to display items in grid layout. /// /// 9 public class GridLayouter : ItemsLayouter { private CollectionView collectionView; private (float Width, float Height) sizeCandidate; private int spanSize = 1; private float align = 0.5f; private bool hasHeader; private Extents headerMargin; private float headerSize; private Extents footerMargin; private bool hasFooter; private float footerSize; private bool isGrouped; private readonly List groups = new List(); private float groupHeaderSize; private Extents groupHeaderMargin; private float groupFooterSize; private Extents groupFooterMargin; private GroupInfo Visited; private Timer requestLayoutTimer = null; private bool isSourceEmpty; /// [EditorBrowsable(EditorBrowsableState.Never)] protected new IGroupableItemSource Source => collectionView?.InternalSource; /// /// Span Size /// [EditorBrowsable(EditorBrowsableState.Never)] protected int SpanSize => spanSize; /// /// Size Candidate /// [EditorBrowsable(EditorBrowsableState.Never)] protected (float Width, float Height) SizeCandidate => sizeCandidate; /// /// Visible ViewItem. /// [EditorBrowsable(EditorBrowsableState.Never)] protected override List GroupItems => groups; /// /// Clean up ItemsLayouter. /// /// CollectionView of layouter. /// please note that, view must be type of CollectionView /// 9 public override void Initialize(RecyclerView view) { collectionView = view as CollectionView; if (collectionView == null) { throw new ArgumentException("GridLayouter only can be applied CollectionView.", nameof(view)); } // 1. Clean Up foreach (RecyclerViewItem item in VisibleItems) { collectionView.UnrealizeItem(item, false); } VisibleItems.Clear(); groups.Clear(); FirstVisible = 0; LastVisible = 0; IsHorizontal = (collectionView.ScrollingDirection == ScrollableBase.Direction.Horizontal); RecyclerViewItem header = collectionView?.Header; RecyclerViewItem footer = collectionView?.Footer; float width, height; int count = Source.Count; int pureCount = count - (header? 1 : 0) - (footer? 1 : 0); // 2. Get the header / footer and size deligated item and measure the size. if (header != null) { MeasureChild(collectionView, header); width = header.Layout != null? header.Layout.MeasuredWidth.Size.AsRoundedValue() : 0; height = header.Layout != null? header.Layout.MeasuredHeight.Size.AsRoundedValue() : 0; Extents itemMargin = header.Margin; headerSize = IsHorizontal? width + itemMargin.Start + itemMargin.End: height + itemMargin.Top + itemMargin.Bottom; headerMargin = new Extents(itemMargin); hasHeader = true; collectionView.UnrealizeItem(header); } if (footer != null) { MeasureChild(collectionView, footer); width = footer.Layout != null? footer.Layout.MeasuredWidth.Size.AsRoundedValue() : 0; height = footer.Layout != null? footer.Layout.MeasuredHeight.Size.AsRoundedValue() : 0; Extents itemMargin = footer.Margin; footerSize = IsHorizontal? width + itemMargin.Start + itemMargin.End: height + itemMargin.Top + itemMargin.Bottom; footerMargin = new Extents(itemMargin); footer.Index = count - 1; hasFooter = true; collectionView.UnrealizeItem(footer); } if (pureCount == 0) { isSourceEmpty = true; base.Initialize(collectionView); return; } isSourceEmpty = false; int firstIndex = header? 1 : 0; if (collectionView.IsGrouped) { isGrouped = true; if (collectionView.GroupHeaderTemplate != null) { while (!Source.IsGroupHeader(firstIndex)) firstIndex++; //must be always true if (Source.IsGroupHeader(firstIndex)) { RecyclerViewItem groupHeader = collectionView.RealizeItem(firstIndex); firstIndex++; if (groupHeader == null) throw new Exception("[" + firstIndex + "] Group Header failed to realize!"); // Need to Set proper height or width on scroll direction. if (groupHeader.Layout == null) { width = groupHeader.WidthSpecification; height = groupHeader.HeightSpecification; } else { MeasureChild(collectionView, groupHeader); width = groupHeader.Layout.MeasuredWidth.Size.AsRoundedValue(); height = groupHeader.Layout.MeasuredHeight.Size.AsRoundedValue(); } //Console.WriteLine("[NUI] GroupHeader Size {0} :{0}", width, height); // pick the StepCandidate. Extents itemMargin = groupHeader.Margin; groupHeaderSize = IsHorizontal? width + itemMargin.Start + itemMargin.End: height + itemMargin.Top + itemMargin.Bottom; groupHeaderMargin = new Extents(itemMargin); collectionView.UnrealizeItem(groupHeader); } } else { groupHeaderSize = 0F; } if (collectionView.GroupFooterTemplate != null) { int firstFooter = firstIndex; while (!Source.IsGroupFooter(firstFooter)) firstFooter++; //must be always true if (Source.IsGroupFooter(firstFooter)) { RecyclerViewItem groupFooter = collectionView.RealizeItem(firstFooter); if (groupFooter == null) throw new Exception("[" + firstFooter + "] Group Footer failed to realize!"); // Need to Set proper height or width on scroll direction. if (groupFooter.Layout == null) { width = groupFooter.WidthSpecification; height = groupFooter.HeightSpecification; } else { MeasureChild(collectionView, groupFooter); width = groupFooter.Layout.MeasuredWidth.Size.AsRoundedValue(); height = groupFooter.Layout.MeasuredHeight.Size.AsRoundedValue(); } // pick the StepCandidate. Extents itemMargin = groupFooter.Margin; groupFooterSize = IsHorizontal? width + itemMargin.Start + itemMargin.End: height + itemMargin.Top + itemMargin.Bottom; groupFooterMargin = new Extents(itemMargin); collectionView.UnrealizeItem(groupFooter); } } else { groupFooterSize = 0F; } } else isGrouped = false; bool failed = false; //Final Check of FirstIndex if ((Source.Count - 1 < firstIndex) || (Source.IsFooter(firstIndex) && (Source.Count - 1) == firstIndex)) { StepCandidate = 0F; failed = true; } while (!failed && Source.IsHeader(firstIndex) || Source.IsGroupHeader(firstIndex) || Source.IsGroupFooter(firstIndex)) { if (Source.IsFooter(firstIndex) || ((Source.Count - 1) <= firstIndex)) { StepCandidate = 0F; failed = true; break; } firstIndex++; } sizeCandidate = (0, 0); if (!failed) { // Get Size Deligate. FIXME if group exist index must be changed. RecyclerViewItem sizeDeligate = collectionView.RealizeItem(firstIndex); if (sizeDeligate == null) { throw new Exception("Cannot create content from DatTemplate."); } sizeDeligate.BindingContext = Source.GetItem(firstIndex); // Need to Set proper height or width on scroll direction. if (sizeDeligate.Layout == null) { width = sizeDeligate.WidthSpecification; height = sizeDeligate.HeightSpecification; } else { MeasureChild(collectionView, sizeDeligate); width = sizeDeligate.Layout.MeasuredWidth.Size.AsRoundedValue(); height = sizeDeligate.Layout.MeasuredHeight.Size.AsRoundedValue(); } //Console.WriteLine("[NUI] item Size {0} :{1}", width, height); // pick the StepCandidate. Extents itemMargin = sizeDeligate.Margin; width = width + itemMargin.Start + itemMargin.End; height = height + itemMargin.Top + itemMargin.Bottom; StepCandidate = IsHorizontal? width : height; CandidateMargin = new Extents(itemMargin); // Prevent zero division. if (width == 0) width = 1; if (height == 0) height = 1; spanSize = IsHorizontal? Convert.ToInt32(Math.Truncate((double)((collectionView.Size.Height - Padding.Top - Padding.Bottom) / height))) : Convert.ToInt32(Math.Truncate((double)((collectionView.Size.Width - Padding.Start - Padding.End) / width))); sizeCandidate = (width, height); collectionView.UnrealizeItem(sizeDeligate); } if (StepCandidate < 1) StepCandidate = 1; if (spanSize < 1) spanSize = 1; if (isGrouped) { float Current = 0.0F; IGroupableItemSource source = Source; GroupInfo currentGroup = null; object currentParent = null; for (int i = 0; i < count; i++) { if (i == 0 && hasHeader) { Current += headerSize; } else if (i == count - 1 && hasFooter) { Current += footerSize; } else { if (source.GetGroupParent(i) != currentParent) { currentParent = source.GetGroupParent(i); float currentSize = (source.IsGroupHeader(i)? groupHeaderSize : (source.IsGroupFooter(i)? groupFooterSize: StepCandidate)); currentGroup = new GroupInfo() { GroupParent = source.GetGroupParent(i), StartIndex = i, Count = 1, GroupSize = currentSize, GroupPosition = Current }; groups.Add(currentGroup); Current += currentSize; } //optional else if (source.IsGroupFooter(i)) { //currentGroup.hasFooter = true; if (currentGroup != null) { currentGroup.Count++; currentGroup.GroupSize += groupFooterSize; Current += groupFooterSize; } } else { if (currentGroup != null) { currentGroup.Count++; int index = i - currentGroup.StartIndex - ((collectionView.GroupHeaderTemplate != null) ? 1 : 0); if ((index % spanSize) == 0) { currentGroup.GroupSize += StepCandidate; Current += StepCandidate; } } } } } ScrollContentSize = Current; } else { // 3. Measure the scroller content size. ScrollContentSize = StepCandidate * Convert.ToInt32(Math.Ceiling((double)pureCount / (double)spanSize)); if (hasHeader) ScrollContentSize += headerSize; if (hasFooter) ScrollContentSize += footerSize; } ScrollContentSize = IsHorizontal? ScrollContentSize + Padding.Start + Padding.End: ScrollContentSize + Padding.Top + Padding.Bottom; if (IsHorizontal) collectionView.ContentContainer.SizeWidth = ScrollContentSize; else collectionView.ContentContainer.SizeHeight = ScrollContentSize; base.Initialize(collectionView); //Console.WriteLine("Init Done, StepCnadidate{0}, spanSize{1}, Scroll{2}", StepCandidate, spanSize, ScrollContentSize); } /// /// This is called to find out where items are lain out according to current scroll position. /// /// Scroll position which is calculated by ScrollableBase /// boolean force flag to layouting forcely. /// 9 public override void RequestLayout(float scrollPosition, bool force = false) { // Layouting is only possible after once it intialized. if (!IsInitialized) return; int LastIndex = Source.Count; if (!force && PrevScrollPosition == Math.Abs(scrollPosition)) return; PrevScrollPosition = Math.Abs(scrollPosition); int prevFirstVisible = FirstVisible; int prevLastVisible = LastVisible; bool IsHorizontal = (collectionView.ScrollingDirection == ScrollableBase.Direction.Horizontal); (float X, float Y) visibleArea = (PrevScrollPosition, PrevScrollPosition + (IsHorizontal? collectionView.Size.Width : collectionView.Size.Height) ); //Console.WriteLine("[NUI] itemsView [{0},{1}] [{2},{3}]", collectionView.Size.Width, collectionView.Size.Height, collectionView.ContentContainer.Size.Width, collectionView.ContentContainer.Size.Height); // 1. Set First/Last Visible Item Index. (int start, int end) = FindVisibleItems(visibleArea); FirstVisible = start; LastVisible = end; //Console.WriteLine("[NUI] {0} :visibleArea before [{1},{2}] after [{3},{4}]", scrollPosition, prevFirstVisible, prevLastVisible, FirstVisible, LastVisible); // 2. Unrealize invisible items. List unrealizedItems = new List(); foreach (RecyclerViewItem item in VisibleItems) { if (item.Index < FirstVisible || item.Index > LastVisible) { //Console.WriteLine("[NUI] Unrealize{0}!", item.Index); unrealizedItems.Add(item); collectionView.UnrealizeItem(item); } } VisibleItems.RemoveAll(unrealizedItems.Contains); //Console.WriteLine("Realize Begin [{0} to {1}]", FirstVisible, LastVisible); // 3. Realize and placing visible items. for (int i = FirstVisible; i <= LastVisible; i++) { //Console.WriteLine("[NUI] Realize!"); RecyclerViewItem item = null; // 4. Get item if visible or realize new. if (i >= prevFirstVisible && i <= prevLastVisible) { item = GetVisibleItem(i); if (item != null && !force) continue; } if (item == null) { item = collectionView.RealizeItem(i); if (item != null) VisibleItems.Add(item); else throw new Exception("Failed to create RecycerViewItem index of ["+ i + "]"); } //item Position without Padding and Margin. (float x, float y) = GetItemPosition(i); // 5. Placing item with Padding and Margin. item.Position = new Position(x, y); //Linear Item need to be resized! if (item.IsHeader || item.IsFooter || item.IsGroupHeader || item.IsGroupFooter) { var size = (IsHorizontal? item.SizeWidth: item.SizeHeight); if (collectionView.SizingStrategy == ItemSizingStrategy.MeasureFirst) { if (item.IsHeader) size = headerSize; else if (item.IsFooter) size = footerSize; else if (item.IsGroupHeader) size = groupHeaderSize; else if (item.IsGroupFooter) size = groupFooterSize; } if (IsHorizontal && item.HeightSpecification == LayoutParamPolicies.MatchParent) { item.Size = new Size(size, Container.Size.Height - Padding.Top - Padding.Bottom - item.Margin.Top - item.Margin.Bottom); } else if (!IsHorizontal && item.WidthSpecification == LayoutParamPolicies.MatchParent) { item.Size = new Size(Container.Size.Width - Padding.Start - Padding.End - item.Margin.Start - item.Margin.End, size); } } //Console.WriteLine("[NUI] ["+item.Index+"] ["+item.Position.X+", "+item.Position.Y+" ==== \n"); } //Console.WriteLine("Realize Done"); } /// /// Clear the current screen and all properties. /// [EditorBrowsable(EditorBrowsableState.Never)] public override void Clear() { // Clean Up if (requestLayoutTimer != null) { requestLayoutTimer.Dispose(); } if (groups != null) { /* foreach (GroupInfo group in groups) { //group.ItemPosition?.Clear(); // if Disposable? //group.Dispose(); } */ groups.Clear(); } if (headerMargin != null) { headerMargin.Dispose(); headerMargin = null; } if (footerMargin != null) { footerMargin.Dispose(); footerMargin = null; } if (groupHeaderMargin != null) { groupHeaderMargin.Dispose(); groupHeaderMargin = null; } if (groupFooterMargin != null) { groupFooterMargin.Dispose(); groupFooterMargin = null; } base.Clear(); } /// public override void NotifyItemSizeChanged(RecyclerViewItem item) { // All Item size need to be same in grid! // if you want to change item size, change dataTemplate to re-initing. return; } /// [EditorBrowsable(EditorBrowsableState.Never)] public override void NotifyItemInserted(IItemSource source, int startIndex) { // Insert Single item. if (source == null) throw new ArgumentNullException(nameof(source)); if (collectionView == null) return; if (isSourceEmpty || StepCandidate <= 1) { Initialize(collectionView); } // Will be null if not a group. float currentSize = 0; IGroupableItemSource gSource = source as IGroupableItemSource; // Get the first Visible Position to adjust. /* int topInScreenIndex = 0; float offset = 0F; (topInScreenIndex, offset) = FindTopItemInScreen(); */ //2. Handle Group Case. if (isGrouped && gSource != null) { GroupInfo groupInfo = null; object groupParent = gSource.GetGroupParent(startIndex); int parentIndex = gSource.GetPosition(groupParent); if (gSource.HasHeader) parentIndex--; // Check item is group parent or not // if group parent, add new gorupinfo if (gSource.IsHeader(startIndex)) { // This is childless group. // create new groupInfo! groupInfo = new GroupInfo() { GroupParent = groupParent, StartIndex = startIndex, Count = 1, GroupSize = groupHeaderSize, }; if (parentIndex >= groups.Count) { groupInfo.GroupPosition = ScrollContentSize; groups.Add(groupInfo); } else { groupInfo.GroupPosition = groups[parentIndex].GroupPosition; groups.Insert(parentIndex, groupInfo); } currentSize = groupHeaderSize; } else { // If not group parent, add item into the groupinfo. if (parentIndex >= groups.Count) throw new Exception("group parent is bigger than group counts."); groupInfo = groups[parentIndex];//GetGroupInfo(groupParent); if (groupInfo == null) throw new Exception("Cannot find group information!"); if (gSource.IsGroupFooter(startIndex)) { // It doesn't make sence to adding footer by notify... // if GroupFooterTemplate is added, // need to implement on here. } else { if (collectionView.SizingStrategy == ItemSizingStrategy.MeasureAll) { // Wrong! Grid Layouter do not support MeasureAll! } else { int pureCount = groupInfo.Count - 1 - (collectionView.GroupFooterTemplate == null? 0: 1); if (pureCount % spanSize == 0) { currentSize = StepCandidate; groupInfo.GroupSize += currentSize; } } } groupInfo.Count++; } if (parentIndex + 1 < groups.Count) { for(int i = parentIndex + 1; i < groups.Count; i++) { groups[i].GroupPosition += currentSize; groups[i].StartIndex++; } } } else { if (collectionView.SizingStrategy == ItemSizingStrategy.MeasureAll) { // Wrong! Grid Layouter do not support MeasureAll! } int pureCount = Source.Count - (hasHeader? 1: 0) - (hasFooter? 1: 0); // Count comes after updated in ungrouped case! if (pureCount % spanSize == 1) { currentSize = StepCandidate; } } // 3. Update Scroll Content Size ScrollContentSize += currentSize; if (IsHorizontal) collectionView.ContentContainer.SizeWidth = ScrollContentSize; else collectionView.ContentContainer.SizeHeight = ScrollContentSize; // 4. Update Visible Items. foreach (RecyclerViewItem item in VisibleItems) { if (item.Index >= startIndex) { item.Index++; } } float scrollPosition = PrevScrollPosition; /* // Position Adjust // Insertion above Top Visible! if (startIndex <= topInScreenIndex) { scrollPosition = GetItemPosition(topInScreenIndex); scrollPosition -= offset; collectionView.ScrollTo(scrollPosition); } */ // Update Viewport in delay. // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop. // but currently we do not have any accessor to pre-calculation so instead of this, // using Timer temporarily. DelayedRequestLayout(scrollPosition); } /// [EditorBrowsable(EditorBrowsableState.Never)] public override void NotifyItemRangeInserted(IItemSource source, int startIndex, int count) { // Insert Group if (source == null) throw new ArgumentNullException(nameof(source)); if (collectionView == null) return; if (isSourceEmpty || StepCandidate <= 1) { Initialize(collectionView); } float currentSize = StepCandidate; // Will be null if not a group. IGroupableItemSource gSource = source as IGroupableItemSource; // Get the first Visible Position to adjust. /* int topInScreenIndex = 0; float offset = 0F; (topInScreenIndex, offset) = FindTopItemInScreen(); */ // 2. Handle Group Case // Adding ranged items should all same new groups. if (isGrouped && gSource != null) { GroupInfo groupInfo = null; object groupParent = gSource.GetGroupParent(startIndex); int parentIndex = gSource.GetPosition(groupParent); if (gSource.HasHeader) parentIndex--; // We guess here that range inserted from GroupStartIndex. int groupStartIndex = startIndex; for (int current = startIndex; current - startIndex < count; current++) { // Check item is group parent or not // if group parent, add new gorupinfo if (groupStartIndex == current) { //create new groupInfo! currentSize = (gSource.IsGroupHeader(current)? groupHeaderSize : (gSource.IsGroupFooter(current)? groupFooterSize: currentSize)); groupInfo = new GroupInfo() { GroupParent = groupParent, StartIndex = current, Count = 1, GroupSize = StepCandidate, }; currentSize += StepCandidate; } else { //if not group parent, add item into the groupinfo. //groupInfo = GetGroupInfo(groupStartIndex); if (groupInfo == null) throw new Exception("Cannot find group information!"); groupInfo.Count++; if (gSource.IsGroupFooter(current)) { groupInfo.GroupSize += groupFooterSize; currentSize += groupFooterSize; } else { if (collectionView.SizingStrategy == ItemSizingStrategy.MeasureAll) { // Wrong! Grid Layouter do not support MeasureAll! } else { int index = current - groupStartIndex - ((collectionView.GroupHeaderTemplate != null)? 1: 0); if ((index % spanSize) == 0) { groupInfo.GroupSize += StepCandidate; currentSize += StepCandidate; } } } } } if (parentIndex >= groups.Count) { groupInfo.GroupPosition = ScrollContentSize; groups.Add(groupInfo); } else { groupInfo.GroupPosition = groups[parentIndex].GroupPosition; groups.Insert(parentIndex, groupInfo); } // Update other below group's position if (parentIndex + 1 < groups.Count) { for(int i = parentIndex + 1; i < groups.Count; i++) { groups[i].GroupPosition += currentSize; groups[i].StartIndex += count; } } ScrollContentSize += currentSize; } else { throw new Exception("Cannot insert ungrouped range items!"); } // 3. Update Scroll Content Size if (IsHorizontal) collectionView.ContentContainer.SizeWidth = ScrollContentSize; else collectionView.ContentContainer.SizeHeight = ScrollContentSize; // 4. Update Visible Items. foreach (RecyclerViewItem item in VisibleItems) { if (item.Index >= startIndex) { item.Index += count; } } // Position Adjust float scrollPosition = PrevScrollPosition; /* // Insertion above Top Visible! if (startIndex + count <= topInScreenIndex) { scrollPosition = GetItemPosition(topInScreenIndex); scrollPosition -= offset; collectionView.ScrollTo(scrollPosition); } */ // Update Viewport in delay. // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop. // but currently we do not have any accessor to pre-calculation so instead of this, // using Timer temporarily. DelayedRequestLayout(scrollPosition); } /// [EditorBrowsable(EditorBrowsableState.Never)] public override void NotifyItemRemoved(IItemSource source, int startIndex) { // Remove Single if (source == null) throw new ArgumentNullException(nameof(source)); if (collectionView == null) return; // Will be null if not a group. float currentSize = 0; IGroupableItemSource gSource = source as IGroupableItemSource; // Get the first Visible Position to adjust. /* int topInScreenIndex = 0; float offset = 0F; (topInScreenIndex, offset) = FindTopItemInScreen(); */ // 2. Handle Group Case if (isGrouped && gSource != null) { int parentIndex = 0; GroupInfo groupInfo = null; foreach(GroupInfo cur in groups) { if ((cur.StartIndex <= startIndex) && (cur.StartIndex + cur.Count - 1 >= startIndex)) { groupInfo = cur; break; } parentIndex++; } if (groupInfo == null) throw new Exception("Cannot find group information!"); // Check item is group parent or not // if group parent, add new gorupinfo if (groupInfo.StartIndex == startIndex) { // This is empty group! // check group is empty. if (groupInfo.Count != 1) { throw new Exception("Cannot remove group parent"); } currentSize = groupInfo.GroupSize; // Remove Group // groupInfo.Dispose(); groups.Remove(groupInfo); parentIndex--; } else { groupInfo.Count--; // Skip footer case as footer cannot exist alone without header. if (collectionView.SizingStrategy == ItemSizingStrategy.MeasureAll) { // Wrong! Grid Layouter do not support MeasureAll! } else { int pureCount = groupInfo.Count - 1 - (collectionView.GroupFooterTemplate == null? 0: 1); if (pureCount % spanSize == 0) { currentSize = StepCandidate; groupInfo.GroupSize -= currentSize; } } } for (int i = parentIndex + 1; i < groups.Count; i++) { groups[i].GroupPosition -= currentSize; groups[i].StartIndex--; } } else { if (collectionView.SizingStrategy == ItemSizingStrategy.MeasureAll) { // Wrong! Grid Layouter do not support MeasureAll! } int pureCount = Source.Count - (hasHeader? 1: 0) - (hasFooter? 1: 0); // Count comes after updated in ungrouped case! if (pureCount % spanSize == 0) { currentSize = StepCandidate; } } ScrollContentSize -= currentSize; // 3. Update Scroll Content Size if (IsHorizontal) collectionView.ContentContainer.SizeWidth = ScrollContentSize; else collectionView.ContentContainer.SizeHeight = ScrollContentSize; // 4. Update Visible Items. RecyclerViewItem targetItem = null; foreach (RecyclerViewItem item in VisibleItems) { if (item.Index == startIndex) { targetItem = item; collectionView.UnrealizeItem(item); } else if (item.Index > startIndex) { item.Index--; } } VisibleItems.Remove(targetItem); // Position Adjust float scrollPosition = PrevScrollPosition; /* // Insertion above Top Visible! if (startIndex <= topInScreenIndex) { scrollPosition = GetItemPosition(topInScreenIndex); scrollPosition -= offset; collectionView.ScrollTo(scrollPosition); } */ // Update Viewport in delay. // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop. // but currently we do not have any accessor to pre-calculation so instead of this, // using Timer temporarily. DelayedRequestLayout(scrollPosition); } /// [EditorBrowsable(EditorBrowsableState.Never)] public override void NotifyItemRangeRemoved(IItemSource source, int startIndex, int count) { // Remove Group if (source == null) throw new ArgumentNullException(nameof(source)); if (collectionView == null) return; // Will be null if not a group. float currentSize = StepCandidate; IGroupableItemSource gSource = source as IGroupableItemSource; // Get the first Visible Position to adjust. /* int topInScreenIndex = 0; float offset = 0F; (topInScreenIndex, offset) = FindTopItemInScreen(); */ // 1. Handle Group Case if (isGrouped && gSource != null) { int parentIndex = 0; GroupInfo groupInfo = null; foreach(GroupInfo cur in groups) { if ((cur.StartIndex == startIndex) && (cur.Count == count)) { groupInfo = cur; break; } parentIndex++; } if (groupInfo == null) throw new Exception("Cannot find group information!"); // Check item is group parent or not // if group parent, add new gorupinfo currentSize = groupInfo.GroupSize; if (collectionView.SizingStrategy == ItemSizingStrategy.MeasureAll) { // Wrong! Grid Layouter do not support MeasureAll! } // Remove Group // groupInfo.Dispose(); groups.Remove(groupInfo); for (int i = parentIndex; i < groups.Count; i++) { groups[i].GroupPosition -= currentSize; groups[i].StartIndex -= count; } } else { // It must group case! throw exception! throw new Exception("Range remove must group remove!"); } ScrollContentSize -= currentSize; // 2. Update Scroll Content Size if (IsHorizontal) collectionView.ContentContainer.SizeWidth = ScrollContentSize; else collectionView.ContentContainer.SizeHeight = ScrollContentSize; // 3. Update Visible Items. List unrealizedItems = new List(); foreach (RecyclerViewItem item in VisibleItems) { if ((item.Index >= startIndex) && (item.Index < startIndex + count)) { unrealizedItems.Add(item); collectionView.UnrealizeItem(item); } else if (item.Index >= startIndex + count) { item.Index -= count; } } VisibleItems.RemoveAll(unrealizedItems.Contains); unrealizedItems.Clear(); // Position Adjust float scrollPosition = PrevScrollPosition; /* // Insertion above Top Visible! if (startIndex <= topInScreenIndex) { scrollPosition = GetItemPosition(topInScreenIndex); scrollPosition -= offset; collectionView.ScrollTo(scrollPosition); } */ // Update Viewport in delay. // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop. // but currently we do not have any accessor to pre-calculation so instead of this, // using Timer temporarily. DelayedRequestLayout(scrollPosition); } /// [EditorBrowsable(EditorBrowsableState.Never)] public override void NotifyItemMoved(IItemSource source, int fromPosition, int toPosition) { // Reorder Single if (source == null) throw new ArgumentNullException(nameof(source)); if (collectionView == null) return; // Will be null if not a group. float currentSize = StepCandidate; int diff = toPosition - fromPosition; // Get the first Visible Position to adjust. /* int topInScreenIndex = 0; float offset = 0F; (topInScreenIndex, offset) = FindTopItemInScreen(); */ // Move can only happen in it's own groups. // so there will be no changes in position, startIndex in ohter groups. // check visible item and update indexs. int startIndex = ( diff > 0 ? fromPosition: toPosition); int endIndex = (diff > 0 ? toPosition: fromPosition); if ((endIndex >= FirstVisible) && (startIndex <= LastVisible)) { foreach (RecyclerViewItem item in VisibleItems) { if ((item.Index >= startIndex) && (item.Index <= endIndex)) { if (item.Index == fromPosition) item.Index = toPosition; else { if (diff > 0) item.Index--; else item.Index++; } } } } if (IsHorizontal) collectionView.ContentContainer.SizeWidth = ScrollContentSize; else collectionView.ContentContainer.SizeHeight = ScrollContentSize; // Position Adjust float scrollPosition = PrevScrollPosition; /* // Insertion above Top Visible! if (((fromPosition > topInScreenIndex) && (toPosition < topInScreenIndex) || ((fromPosition < topInScreenIndex) && (toPosition > topInScreenIndex))) { scrollPosition = GetItemPosition(topInScreenIndex); scrollPosition -= offset; collectionView.ScrollTo(scrollPosition); } */ // Update Viewport in delay. // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop. // but currently we do not have any accessor to pre-calculation so instead of this, // using Timer temporarily. DelayedRequestLayout(scrollPosition); } /// [EditorBrowsable(EditorBrowsableState.Never)] public override void NotifyItemRangeMoved(IItemSource source, int fromPosition, int toPosition, int count) { // Reorder Groups if (source == null) throw new ArgumentNullException(nameof(source)); if (collectionView == null) return; // Will be null if not a group. float currentSize = StepCandidate; int diff = toPosition - fromPosition; int startIndex = ( diff > 0 ? fromPosition: toPosition); int endIndex = (diff > 0 ? toPosition + count - 1: fromPosition + count - 1); // 2. Handle Group Case if (isGrouped) { int fromParentIndex = 0; int toParentIndex = 0; bool findFrom = false; bool findTo = false; GroupInfo fromGroup = null; GroupInfo toGroup = null; foreach(GroupInfo cur in groups) { if ((cur.StartIndex == fromPosition) && (cur.Count == count)) { fromGroup = cur; findFrom = true; if (findFrom && findTo) break; } else if (cur.StartIndex == toPosition) { toGroup = cur; findTo = true; if (findFrom && findTo) break; } if (!findFrom) fromParentIndex++; if (!findTo) toParentIndex++; } if (toGroup == null || fromGroup == null) throw new Exception("Cannot find group information!"); fromGroup.StartIndex = toGroup.StartIndex; fromGroup.GroupPosition = toGroup.GroupPosition; endIndex = (diff > 0 ? toPosition + toGroup.Count - 1: fromPosition + count - 1); groups.Remove(fromGroup); groups.Insert(toParentIndex, fromGroup); int startGroup = (diff > 0? fromParentIndex: toParentIndex); int endGroup = (diff > 0? toParentIndex: fromParentIndex); for (int i = startGroup; i <= endGroup; i++) { if (i == toParentIndex) continue; float prevPos = groups[i].GroupPosition; int prevIdx = groups[i].StartIndex; groups[i].GroupPosition = groups[i].GroupPosition + (diff > 0? -1 : 1) * fromGroup.GroupSize; groups[i].StartIndex = groups[i].StartIndex + (diff > 0? -1 : 1) * fromGroup.Count; } } else { //It must group case! throw exception! throw new Exception("Range remove must group remove!"); } // Move can only happen in it's own groups. // so there will be no changes in position, startIndex in ohter groups. // check visible item and update indexs. if ((endIndex >= FirstVisible) && (startIndex <= LastVisible)) { foreach (RecyclerViewItem item in VisibleItems) { if ((item.Index >= startIndex) && (item.Index <= endIndex)) { if ((item.Index >= fromPosition) && (item.Index < fromPosition + count)) { item.Index = fromPosition - item.Index + toPosition; } else { if (diff > 0) item.Index -= count; else item.Index += count; } } } } // Position Adjust float scrollPosition = PrevScrollPosition; /* // Insertion above Top Visible! if (((fromPosition > topInScreenIndex) && (toPosition < topInScreenIndex) || ((fromPosition < topInScreenIndex) && (toPosition > topInScreenIndex))) { scrollPosition = GetItemPosition(topInScreenIndex); scrollPosition -= offset; collectionView.ScrollTo(scrollPosition); } */ // Update Viewport in delay. // FIMXE: original we only need to process RequestLayout once before layout calculation in main loop. // but currently we do not have any accessor to pre-calculation so instead of this, // using Timer temporarily. DelayedRequestLayout(scrollPosition); } /// [EditorBrowsable(EditorBrowsableState.Never)] public override float CalculateLayoutOrientationSize() { //Console.WriteLine("[NUI] Calculate Layout ScrollContentSize {0}", ScrollContentSize); return ScrollContentSize; } /// [EditorBrowsable(EditorBrowsableState.Never)] public override float CalculateCandidateScrollPosition(float scrollPosition) { //Console.WriteLine("[NUI] Calculate Candidate ScrollContentSize {0}", ScrollContentSize); return scrollPosition; } /// [EditorBrowsable(EditorBrowsableState.Never)] public override View RequestNextFocusableView(View currentFocusedView, View.FocusDirection direction, bool loopEnabled) { if (currentFocusedView == null) throw new ArgumentNullException(nameof(currentFocusedView)); View nextFocusedView = null; int targetSibling = -1; bool IsHorizontal = collectionView.ScrollingDirection == ScrollableBase.Direction.Horizontal; switch (direction) { case View.FocusDirection.Left: { targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder - 1 : targetSibling; break; } case View.FocusDirection.Right: { targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder + 1 : targetSibling; break; } case View.FocusDirection.Up: { targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder - 1; break; } case View.FocusDirection.Down: { targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder + 1; break; } } if (targetSibling > -1 && targetSibling < Container.Children.Count) { RecyclerViewItem candidate = Container.Children[targetSibling] as RecyclerViewItem; if (candidate != null && candidate.Index >= 0 && candidate.Index < Source.Count) { nextFocusedView = candidate; } } return nextFocusedView; } /// [EditorBrowsable(EditorBrowsableState.Never)] protected override (int start, int end) FindVisibleItems((float X, float Y) visibleArea) { int MaxIndex = Source.Count - 1 - (hasFooter ? 1 : 0); int adds = spanSize * 2; int skipGroup = -1; (int start, int end) found = (0, 0); // Header is Showing if (hasHeader && visibleArea.X < headerSize + (IsHorizontal? Padding.Start : Padding.Top)) { found.start = 0; } else { if (isGrouped) { bool failed = true; foreach (GroupInfo gInfo in groups) { skipGroup++; // in the Group if (gInfo.GroupPosition <= visibleArea.X && gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.X) { if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.X) { found.start = gInfo.StartIndex - adds; failed = false; break; } //can be step in spanSize... for (int i = 1; i < gInfo.Count; i++) { if (!failed) break; // Reach last index of group. if (i == (gInfo.Count - 1)) { found.start = gInfo.StartIndex + i - adds; failed = false; break; } else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.X - gInfo.GroupPosition - groupHeaderSize) { found.start = gInfo.StartIndex + i - adds; failed = false; break; } } if (!failed) break; } } //footer only shows? if (failed) { found.start = MaxIndex; } } else { float visibleAreaX = visibleArea.X - (hasHeader ? headerSize : 0); // Prevent zero division. var itemSize = (StepCandidate != 0)? StepCandidate: 1f; found.start = (Convert.ToInt32(Math.Abs(visibleAreaX / itemSize)) - 1) * spanSize; if (hasHeader) found.start += 1; } if (found.start < 0) found.start = 0; } if (hasFooter && visibleArea.Y > ScrollContentSize - footerSize - (IsHorizontal? Padding.End : Padding.Bottom)) { found.end = MaxIndex + 1; } else { if (isGrouped) { bool failed = true; // can it be start from founded group...? //foreach(GroupInfo gInfo in groups.Skip(skipGroup)) foreach (GroupInfo gInfo in groups) { // in the Group if (gInfo.GroupPosition <= visibleArea.Y && gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.Y) { if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.Y) { found.end = gInfo.StartIndex + adds; failed = false; break; } //can be step in spanSize... for (int i = 1; i < gInfo.Count; i++) { if (!failed) break; // Reach last index of group. if (i == (gInfo.Count - 1)) { found.end = gInfo.StartIndex + i + adds; failed = false; break; } else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.Y - gInfo.GroupPosition - groupHeaderSize) { found.end = gInfo.StartIndex + i + adds; failed = false; break; } } if (!failed) break; } } //footer only shows? if (failed) { found.end = MaxIndex; } } else { float visibleAreaY = visibleArea.Y - (hasHeader ? headerSize : 0); //Need to Consider GroupHeight!!!! // Prevent zero division. var itemSize = (StepCandidate != 0)? StepCandidate: 1f; found.end = (Convert.ToInt32(Math.Abs(visibleAreaY / itemSize)) + 1) * spanSize + adds; if (hasHeader) found.end += 1; } if (found.end > (MaxIndex)) found.end = MaxIndex; } return found; } /// [EditorBrowsable(EditorBrowsableState.Never)] protected internal override (float X, float Y) GetItemPosition(int index) { float xPos, yPos; int spaceStartX = Padding.Start; int spaceStartY = Padding.Top; int emptyArea = IsHorizontal? (int)(collectionView.Size.Height - Padding.Top - Padding.Bottom - (sizeCandidate.Height * spanSize)) : (int)(collectionView.Size.Width - Padding.Start - Padding.End - (sizeCandidate.Width * spanSize)); if (hasHeader && index == 0) { return (spaceStartX + headerMargin.Start, spaceStartY + headerMargin.Top); } if (hasFooter && index == Source.Count - 1) { xPos = IsHorizontal? ScrollContentSize - Padding.End - footerSize + footerMargin.Start: spaceStartX; yPos = IsHorizontal? spaceStartY: ScrollContentSize - Padding.Bottom - footerSize + footerMargin.Top; return (xPos, yPos); } GroupInfo myGroup = GetGroupInfo(index); if (isGrouped && null != myGroup) { if (Source.IsGroupHeader(index)) { spaceStartX+= groupHeaderMargin.Start; spaceStartY+= groupHeaderMargin.Top; xPos = IsHorizontal? myGroup.GroupPosition + groupHeaderMargin.Start: spaceStartX; yPos = IsHorizontal? spaceStartY: myGroup.GroupPosition + groupHeaderMargin.Top; } else if (Source.IsGroupFooter(index)) { spaceStartX+= groupFooterMargin.Start; spaceStartY+= groupFooterMargin.Top; xPos = IsHorizontal? myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize + groupFooterMargin.Start: spaceStartX; yPos = IsHorizontal? spaceStartY: myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize + groupFooterMargin.Top; } else { int pureIndex = index - myGroup.StartIndex - ((collectionView.GroupHeaderTemplate != null)? 1: 0); int division = pureIndex / spanSize; int remainder = pureIndex % spanSize; if (division < 0) division = 0; if (remainder < 0) remainder = 0; spaceStartX+= CandidateMargin.Start; spaceStartY+= CandidateMargin.Top; xPos = IsHorizontal? (division * sizeCandidate.Width) + myGroup.GroupPosition + groupHeaderSize + CandidateMargin.Start: (emptyArea * align) + (remainder * sizeCandidate.Width) + spaceStartX; yPos = IsHorizontal? (emptyArea * align) + (remainder * sizeCandidate.Height) + spaceStartY: (division * sizeCandidate.Height) + myGroup.GroupPosition + groupHeaderSize + CandidateMargin.Top; } } else { int pureIndex = index - (collectionView.Header ? 1 : 0); // int convert must be truncate value. int division = pureIndex / spanSize; int remainder = pureIndex % spanSize; if (division < 0) division = 0; if (remainder < 0) remainder = 0; spaceStartX+= CandidateMargin.Start; spaceStartY+= CandidateMargin.Top; xPos = IsHorizontal? (division * sizeCandidate.Width) + (hasHeader? headerSize : 0) + spaceStartX: (emptyArea * align) + (remainder * sizeCandidate.Width) + spaceStartX; yPos = IsHorizontal? (emptyArea * align) + (remainder * sizeCandidate.Height) + spaceStartY: (division * sizeCandidate.Height) + (hasHeader? headerSize : 0) + spaceStartY; } return (xPos, yPos); } /// [EditorBrowsable(EditorBrowsableState.Never)] protected internal override (float Width, float Height) GetItemSize(int index) { return (sizeCandidate.Width - CandidateMargin.Start - CandidateMargin.End, sizeCandidate.Height - CandidateMargin.Top - CandidateMargin.Bottom); } private void DelayedRequestLayout(float scrollPosition , bool force = true) { if (requestLayoutTimer != null) { requestLayoutTimer.Dispose(); } requestLayoutTimer = new Timer(1); requestLayoutTimer.Interval = 1; requestLayoutTimer.Tick += ((object target, Timer.TickEventArgs args) => { RequestLayout(scrollPosition, force); return false; }); requestLayoutTimer.Start(); } private GroupInfo GetGroupInfo(int index) { if (Visited != null) { if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index) return Visited; } if (hasHeader && index == 0) return null; foreach (GroupInfo group in groups) { if (group.StartIndex <= index && group.StartIndex + group.Count > index) { Visited = group; return group; } } Visited = null; return null; } /* private object GetGroupParent(int index) { if (Visited != null) { if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index) return Visited.GroupParent; } if (hasHeader && index == 0) return null; foreach (GroupInfo group in groups) { if (group.StartIndex <= index && group.StartIndex + group.Count > index) { Visited = group; return group.GroupParent; } } Visited = null; return null; } */ } }