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 System.Collections;
19 using System.Collections.Generic;
20 using System.Collections.Specialized;
22 namespace Tizen.NUI.Components
24 internal class ObservableGroupedSource : IGroupableItemSource, ICollectionChangedNotifier
26 readonly ICollectionChangedNotifier notifier;
27 readonly IList groupSource;
28 readonly List<IItemSource> groups = new List<IItemSource>();
29 readonly bool hasGroupHeaders;
30 readonly bool hasGroupFooters;
37 var groupContents = 0;
39 for (int n = 0; n < groups.Count; n++)
41 groupContents += groups[n].Count;
44 return (HasHeader ? 1 : 0)
50 public bool HasHeader { get; set; }
51 public bool HasFooter { get; set; }
53 public ObservableGroupedSource(CollectionView colView, ICollectionChangedNotifier changedNotifier)
55 var source = colView.ItemsSource;
57 notifier = changedNotifier;
58 groupSource = source as IList ?? new ListSource(source);
60 hasGroupFooters = colView.GroupFooterTemplate != null;
61 hasGroupHeaders = colView.GroupHeaderTemplate != null;
62 HasHeader = colView.Header != null;
63 HasFooter = colView.Footer != null;
65 if (groupSource is INotifyCollectionChanged incc)
67 incc.CollectionChanged += CollectionChanged;
70 UpdateGroupTracking();
78 public bool IsFooter(int position)
85 return position == Count - 1;
88 public bool IsHeader(int position)
90 return HasHeader && position == 0;
93 public bool IsGroupHeader(int position)
95 if (IsFooter(position) || IsHeader(position))
100 var (group, inGroup) = GetGroupAndIndex(position);
102 return groups[group].IsHeader(inGroup);
105 public bool IsGroupFooter(int position)
107 if (IsFooter(position) || IsHeader(position))
112 var (group, inGroup) = GetGroupAndIndex(position);
114 return groups[group].IsFooter(inGroup);
117 public int GetPosition(object item)
119 int previousGroupsOffset = 0;
121 for (int groupIndex = 0; groupIndex < groupSource.Count; groupIndex++)
123 if (groupSource[groupIndex].Equals(item))
125 return AdjustPositionForHeader(groupIndex);
128 var group = groups[groupIndex];
129 var inGroup = group.GetPosition(item);
133 return AdjustPositionForHeader(previousGroupsOffset + inGroup);
136 previousGroupsOffset += group.Count;
142 public object GetItem(int position)
144 var (group, inGroup) = GetGroupAndIndex(position);
146 if (IsGroupFooter(position) || IsGroupHeader(position))
148 // This is looping to find the group/index twice, need to make it less inefficient
149 return groupSource[group];
152 return groups[group].GetItem(inGroup);
155 public object GetGroupParent(int position)
157 var (group, inGroup) = GetGroupAndIndex(position);
158 return groupSource[group];
161 // The ICollectionChangedNotifier methods are called by child observable items sources (i.e., the groups)
162 // This class can then translate their local changes into global positions for upstream notification
163 // (e.g., to the actual RecyclerView.Adapter, so that it can notify the RecyclerView and handle animating
165 public void NotifyDataSetChanged()
170 public void NotifyItemChanged(IItemSource group, int localIndex)
172 localIndex = GetAbsolutePosition(group, localIndex);
173 notifier.NotifyItemChanged(this, localIndex);
176 public void NotifyItemInserted(IItemSource group, int localIndex)
178 localIndex = GetAbsolutePosition(group, localIndex);
179 notifier.NotifyItemInserted(this, localIndex);
182 public void NotifyItemMoved(IItemSource group, int localFromIndex, int localToIndex)
184 localFromIndex = GetAbsolutePosition(group, localFromIndex);
185 localToIndex = GetAbsolutePosition(group, localToIndex);
186 notifier.NotifyItemMoved(this, localFromIndex, localToIndex);
189 public void NotifyItemRangeChanged(IItemSource group, int localStartIndex, int localEndIndex)
191 localStartIndex = GetAbsolutePosition(group, localStartIndex);
192 localEndIndex = GetAbsolutePosition(group, localEndIndex);
193 notifier.NotifyItemRangeChanged(this, localStartIndex, localEndIndex);
196 public void NotifyItemRangeInserted(IItemSource group, int localIndex, int count)
198 localIndex = GetAbsolutePosition(group, localIndex);
199 notifier.NotifyItemRangeInserted(this, localIndex, count);
202 public void NotifyItemRangeRemoved(IItemSource group, int localIndex, int count)
204 localIndex = GetAbsolutePosition(group, localIndex);
205 notifier.NotifyItemRangeRemoved(this, localIndex, count);
208 public void NotifyItemRemoved(IItemSource group, int localIndex)
210 localIndex = GetAbsolutePosition(group, localIndex);
211 notifier.NotifyItemRemoved(this, localIndex);
214 protected virtual void Dispose(bool disposing)
225 ClearGroupTracking();
227 if (groupSource is INotifyCollectionChanged notifyCollectionChanged)
229 notifyCollectionChanged.CollectionChanged -= CollectionChanged;
231 if (groupSource is IDisposable dispoableSource) dispoableSource.Dispose();
235 void UpdateGroupTracking()
237 ClearGroupTracking();
239 for (int n = 0; n < groupSource.Count; n++)
241 var source = ItemsSourceFactory.Create(groupSource[n] as IEnumerable, this);
242 source.HasFooter = hasGroupFooters;
243 source.HasHeader = hasGroupHeaders;
248 void ClearGroupTracking()
250 for (int n = groups.Count - 1; n >= 0; n--)
257 void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
259 if (Device.IsInvokeRequired)
261 Device.BeginInvokeOnMainThread(() => CollectionChanged(args));
266 CollectionChanged(args);
270 void CollectionChanged(NotifyCollectionChangedEventArgs args)
274 case NotifyCollectionChangedAction.Add:
277 case NotifyCollectionChangedAction.Remove:
280 case NotifyCollectionChangedAction.Replace:
283 case NotifyCollectionChangedAction.Move:
286 case NotifyCollectionChangedAction.Reset:
290 throw new ArgumentOutOfRangeException(nameof(args));
296 UpdateGroupTracking();
297 notifier.NotifyDataSetChanged();
300 void Add(NotifyCollectionChangedEventArgs args)
302 var groupIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : groupSource.IndexOf(args.NewItems[0]);
303 var groupCount = args.NewItems.Count;
305 UpdateGroupTracking();
307 // Determine the absolute starting position and the number of items in the groups being added
308 var absolutePosition = GetAbsolutePosition(groups[groupIndex], 0);
309 var itemCount = CountItemsInGroups(groupIndex, groupCount);
313 notifier.NotifyItemInserted(this, absolutePosition);
317 notifier.NotifyItemRangeInserted(this, absolutePosition, itemCount);
320 void Remove(NotifyCollectionChangedEventArgs args)
322 var groupIndex = args.OldStartingIndex;
326 // INCC implementation isn't giving us enough information to know where the removed groups was in the
327 // collection. So the best we can do is a full reload.
332 // If we have a start index, we can be more clever about removing the group(s) (and get the nifty animations)
333 var groupCount = args.OldItems.Count;
335 var absolutePosition = GetAbsolutePosition(groups[groupIndex], 0);
337 // Figure out how many items are in the groups we're removing
338 var itemCount = CountItemsInGroups(groupIndex, groupCount);
342 notifier.NotifyItemRemoved(this, absolutePosition);
344 UpdateGroupTracking();
349 notifier.NotifyItemRangeRemoved(this, absolutePosition, itemCount);
351 UpdateGroupTracking();
354 void Replace(NotifyCollectionChangedEventArgs args)
356 var groupCount = args.NewItems.Count;
358 if (groupCount != args.OldItems.Count)
360 // The original and replacement sets are of unequal size; this means that most everything currently in
361 // view will have to be updated. So just reload the whole thing.
366 var newStartIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : groupSource.IndexOf(args.NewItems[0]);
367 var oldStartIndex = args.OldStartingIndex > -1 ? args.OldStartingIndex : groupSource.IndexOf(args.OldItems[0]);
369 var newItemCount = CountItemsInGroups(newStartIndex, groupCount);
370 var oldItemCount = CountItemsInGroups(oldStartIndex, groupCount);
372 if (newItemCount != oldItemCount)
374 // The original and replacement sets are of unequal size; this means that most everything currently in
375 // view will have to be updated. So just reload the whole thing.
380 // We are replacing one set of items with a set of equal size; we can do a simple item or range notification
381 var firstGroupIndex = Math.Min(newStartIndex, oldStartIndex);
382 var absolutePosition = GetAbsolutePosition(groups[firstGroupIndex], 0);
384 if (newItemCount == 1)
386 notifier.NotifyItemChanged(this, absolutePosition);
387 UpdateGroupTracking();
391 notifier.NotifyItemRangeChanged(this, absolutePosition, newItemCount * 2);
392 UpdateGroupTracking();
396 void Move(NotifyCollectionChangedEventArgs args)
398 var start = Math.Min(args.OldStartingIndex, args.NewStartingIndex);
399 var end = Math.Max(args.OldStartingIndex, args.NewStartingIndex) + args.NewItems.Count;
401 var itemCount = CountItemsInGroups(start, end - start);
402 var absolutePosition = GetAbsolutePosition(groups[start], 0);
404 notifier.NotifyItemRangeChanged(this, absolutePosition, itemCount);
406 UpdateGroupTracking();
409 int GetAbsolutePosition(IItemSource group, int indexInGroup)
411 var groupIndex = groups.IndexOf(group);
413 var runningIndex = 0;
415 for (int n = 0; n < groupIndex; n++)
417 runningIndex += groups[n].Count;
420 return AdjustPositionForHeader(runningIndex + indexInGroup);
423 (int, int) GetGroupAndIndex(int absolutePosition)
425 absolutePosition = AdjustIndexForHeader(absolutePosition);
430 while (absolutePosition > 0)
434 if (localIndex == groups[group].Count)
440 absolutePosition -= 1;
443 return (group, localIndex);
446 int AdjustIndexForHeader(int index)
448 return index - (HasHeader ? 1 : 0);
451 int AdjustPositionForHeader(int position)
453 return position + (HasHeader ? 1 : 0);
456 int CountItemsInGroups(int groupStartIndex, int groupCount)
459 for (int n = 0; n < groupCount; n++)
461 itemCount += groups[groupStartIndex + n].Count;