[NUI] Introduce CollectionView and related classes. (#2525)
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI.Components / Controls / RecyclerView / ItemSource / ObservableGroupedSource.cs
1 /* Copyright (c) 2021 Samsung Electronics Co., Ltd.
2  *
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
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
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.
14  *
15  */
16
17 using System;
18 using System.Collections;
19 using System.Collections.Generic;
20 using System.Collections.Specialized;
21
22 namespace Tizen.NUI.Components
23 {
24     internal class ObservableGroupedSource : IGroupableItemSource, ICollectionChangedNotifier
25     {
26         readonly ICollectionChangedNotifier notifier;
27         readonly IList groupSource;
28         readonly List<IItemSource> groups = new List<IItemSource>();
29         readonly bool hasGroupHeaders;
30         readonly bool hasGroupFooters;
31         bool disposed;
32
33         public int Count
34         {
35             get
36             {
37                 var groupContents = 0;
38
39                 for (int n = 0; n < groups.Count; n++)
40                 {
41                     groupContents += groups[n].Count;
42                 }
43
44                 return (HasHeader ? 1 : 0)
45                      + (HasFooter ? 1 : 0)
46                      + groupContents;
47             }
48         }
49
50         public bool HasHeader { get; set; }
51         public bool HasFooter { get; set; }
52
53         public ObservableGroupedSource(CollectionView colView, ICollectionChangedNotifier changedNotifier)
54         {
55             var source = colView.ItemsSource;
56
57             notifier = changedNotifier;
58             groupSource = source as IList ?? new ListSource(source);
59
60             hasGroupFooters = colView.GroupFooterTemplate != null;
61             hasGroupHeaders = colView.GroupHeaderTemplate != null;
62             HasHeader = colView.Header != null;
63             HasFooter = colView.Footer != null;
64
65             if (groupSource is INotifyCollectionChanged incc)
66             {
67                 incc.CollectionChanged += CollectionChanged;
68             }
69
70             UpdateGroupTracking();
71         }
72
73         public void Dispose()
74         {
75             Dispose(true);
76         }
77
78         public bool IsFooter(int position)
79         {
80             if (!HasFooter)
81             {
82                 return false;
83             }
84
85             return position == Count - 1;
86         }
87
88         public bool IsHeader(int position)
89         {
90             return HasHeader && position == 0;
91         }
92
93         public bool IsGroupHeader(int position)
94         {
95             if (IsFooter(position) || IsHeader(position))
96             {
97                 return false;
98             }
99
100             var (group, inGroup) = GetGroupAndIndex(position);
101
102             return groups[group].IsHeader(inGroup);
103         }
104
105         public bool IsGroupFooter(int position)
106         {
107             if (IsFooter(position) || IsHeader(position))
108             {
109                 return false;
110             }
111
112             var (group, inGroup) = GetGroupAndIndex(position);
113
114             return groups[group].IsFooter(inGroup);
115         }
116
117         public int GetPosition(object item)
118         {
119             int previousGroupsOffset = 0;
120
121             for (int groupIndex = 0; groupIndex < groupSource.Count; groupIndex++)
122             {
123                 if (groupSource[groupIndex].Equals(item))
124                 {
125                     return AdjustPositionForHeader(groupIndex);
126                 }
127
128                 var group = groups[groupIndex];
129                 var inGroup = group.GetPosition(item);
130
131                 if (inGroup > -1)
132                 {
133                     return AdjustPositionForHeader(previousGroupsOffset + inGroup);
134                 }
135
136                 previousGroupsOffset += group.Count;
137             }
138
139             return -1;
140         }
141
142         public object GetItem(int position)
143         {
144             var (group, inGroup) = GetGroupAndIndex(position);
145
146             if (IsGroupFooter(position) || IsGroupHeader(position))
147             {
148                 // This is looping to find the group/index twice, need to make it less inefficient
149                 return groupSource[group];
150             }
151
152             return groups[group].GetItem(inGroup);
153         }
154
155         public object GetGroupParent(int position)
156         {
157             var (group, inGroup) = GetGroupAndIndex(position);
158             return groupSource[group];
159         }
160
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
164         // the changes)
165         public void NotifyDataSetChanged()
166         {
167             Reload();
168         }
169
170         public void NotifyItemChanged(IItemSource group, int localIndex)
171         {
172             localIndex = GetAbsolutePosition(group, localIndex);
173             notifier.NotifyItemChanged(this, localIndex);
174         }
175
176         public void NotifyItemInserted(IItemSource group, int localIndex)
177         {
178             localIndex = GetAbsolutePosition(group, localIndex);
179             notifier.NotifyItemInserted(this, localIndex);
180         }
181
182         public void NotifyItemMoved(IItemSource group, int localFromIndex, int localToIndex)
183         {
184             localFromIndex = GetAbsolutePosition(group, localFromIndex);
185             localToIndex = GetAbsolutePosition(group, localToIndex);
186             notifier.NotifyItemMoved(this, localFromIndex, localToIndex);
187         }
188
189         public void NotifyItemRangeChanged(IItemSource group, int localStartIndex, int localEndIndex)
190         {
191             localStartIndex = GetAbsolutePosition(group, localStartIndex);
192             localEndIndex = GetAbsolutePosition(group, localEndIndex);
193             notifier.NotifyItemRangeChanged(this, localStartIndex, localEndIndex);
194         }
195
196         public void NotifyItemRangeInserted(IItemSource group, int localIndex, int count)
197         {
198             localIndex = GetAbsolutePosition(group, localIndex);
199             notifier.NotifyItemRangeInserted(this, localIndex, count);
200         }
201
202         public void NotifyItemRangeRemoved(IItemSource group, int localIndex, int count)
203         {
204             localIndex = GetAbsolutePosition(group, localIndex);
205             notifier.NotifyItemRangeRemoved(this, localIndex, count);
206         }
207
208         public void NotifyItemRemoved(IItemSource group, int localIndex)
209         {
210             localIndex = GetAbsolutePosition(group, localIndex);
211             notifier.NotifyItemRemoved(this, localIndex);
212         }
213
214         protected virtual void Dispose(bool disposing)
215         {
216             if (disposed)
217             {
218                 return;
219             }
220
221            disposed = true;
222
223             if (disposing)
224             {
225                 ClearGroupTracking();
226
227                 if (groupSource is INotifyCollectionChanged notifyCollectionChanged)
228                 {
229                     notifyCollectionChanged.CollectionChanged -= CollectionChanged;
230                 }
231                 if (groupSource is IDisposable dispoableSource) dispoableSource.Dispose();
232             }
233         }
234
235         void UpdateGroupTracking()
236         {
237             ClearGroupTracking();
238
239             for (int n = 0; n < groupSource.Count; n++)
240             {
241                 var source = ItemsSourceFactory.Create(groupSource[n] as IEnumerable, this);
242                 source.HasFooter = hasGroupFooters;
243                 source.HasHeader = hasGroupHeaders;
244                 groups.Add(source);
245             }
246         }
247
248         void ClearGroupTracking()
249         {
250             for (int n = groups.Count - 1; n >= 0; n--)
251             {
252                 groups[n].Dispose();
253                 groups.RemoveAt(n);
254             }
255         }
256
257         void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
258         {/*
259             if (Device.IsInvokeRequired)
260             {
261                 Device.BeginInvokeOnMainThread(() => CollectionChanged(args));
262             }
263             else
264             {
265                 */
266                 CollectionChanged(args);
267             //}
268         }
269
270         void CollectionChanged(NotifyCollectionChangedEventArgs args)
271         {
272             switch (args.Action)
273             {
274                 case NotifyCollectionChangedAction.Add:
275                     Add(args);
276                     break;
277                 case NotifyCollectionChangedAction.Remove:
278                     Remove(args);
279                     break;
280                 case NotifyCollectionChangedAction.Replace:
281                     Replace(args);
282                     break;
283                 case NotifyCollectionChangedAction.Move:
284                     Move(args);
285                     break;
286                 case NotifyCollectionChangedAction.Reset:
287                     Reload();
288                     break;
289                 default:
290                     throw new ArgumentOutOfRangeException(nameof(args));
291             }
292         }
293
294         void Reload()
295         {
296             UpdateGroupTracking();
297             notifier.NotifyDataSetChanged();
298         }
299
300         void Add(NotifyCollectionChangedEventArgs args)
301         {
302             var groupIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : groupSource.IndexOf(args.NewItems[0]);
303             var groupCount = args.NewItems.Count;
304
305             UpdateGroupTracking();
306
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);
310
311             if (itemCount == 1)
312             {
313                 notifier.NotifyItemInserted(this, absolutePosition);
314                 return;
315             }
316
317             notifier.NotifyItemRangeInserted(this, absolutePosition, itemCount);
318         }
319
320         void Remove(NotifyCollectionChangedEventArgs args)
321         {
322             var groupIndex = args.OldStartingIndex;
323
324             if (groupIndex < 0)
325             {
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.
328                 Reload();
329                 return;
330             }
331
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;
334
335             var absolutePosition = GetAbsolutePosition(groups[groupIndex], 0);
336
337             // Figure out how many items are in the groups we're removing
338             var itemCount = CountItemsInGroups(groupIndex, groupCount);
339
340             if (itemCount == 1)
341             {
342                 notifier.NotifyItemRemoved(this, absolutePosition);
343
344                 UpdateGroupTracking();
345
346                 return;
347             }
348
349             notifier.NotifyItemRangeRemoved(this, absolutePosition, itemCount);
350
351             UpdateGroupTracking();
352         }
353
354         void Replace(NotifyCollectionChangedEventArgs args)
355         {
356             var groupCount = args.NewItems.Count;
357
358             if (groupCount != args.OldItems.Count)
359             {
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.
362                 Reload();
363                 return;
364             }
365
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]);
368
369             var newItemCount = CountItemsInGroups(newStartIndex, groupCount);
370             var oldItemCount = CountItemsInGroups(oldStartIndex, groupCount);
371
372             if (newItemCount != oldItemCount)
373             {
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.
376                 Reload();
377                 return;
378             }
379
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);
383
384             if (newItemCount == 1)
385             {
386                 notifier.NotifyItemChanged(this, absolutePosition);
387                 UpdateGroupTracking();
388             }
389             else
390             {
391                 notifier.NotifyItemRangeChanged(this, absolutePosition, newItemCount * 2);
392                 UpdateGroupTracking();
393             }
394         }
395
396         void Move(NotifyCollectionChangedEventArgs args)
397         {
398             var start = Math.Min(args.OldStartingIndex, args.NewStartingIndex);
399             var end = Math.Max(args.OldStartingIndex, args.NewStartingIndex) + args.NewItems.Count;
400
401             var itemCount = CountItemsInGroups(start, end - start);
402             var absolutePosition = GetAbsolutePosition(groups[start], 0);
403
404             notifier.NotifyItemRangeChanged(this, absolutePosition, itemCount);
405
406             UpdateGroupTracking();
407         }
408
409         int GetAbsolutePosition(IItemSource group, int indexInGroup)
410         {
411             var groupIndex = groups.IndexOf(group);
412
413             var runningIndex = 0;
414
415             for (int n = 0; n < groupIndex; n++)
416             {
417                 runningIndex += groups[n].Count;
418             }
419
420             return AdjustPositionForHeader(runningIndex + indexInGroup);
421         }
422
423         (int, int) GetGroupAndIndex(int absolutePosition)
424         {
425             absolutePosition = AdjustIndexForHeader(absolutePosition);
426
427             var group = 0;
428             var localIndex = 0;
429
430             while (absolutePosition > 0)
431             {
432                 localIndex += 1;
433
434                 if (localIndex == groups[group].Count)
435                 {
436                     group += 1;
437                     localIndex = 0;
438                 }
439
440                 absolutePosition -= 1;
441             }
442
443             return (group, localIndex);
444         }
445
446         int AdjustIndexForHeader(int index)
447         {
448             return index - (HasHeader ? 1 : 0);
449         }
450
451         int AdjustPositionForHeader(int position)
452         {
453             return position + (HasHeader ? 1 : 0);
454         }
455
456         int CountItemsInGroups(int groupStartIndex, int groupCount)
457         {
458             var itemCount = 0;
459             for (int n = 0; n < groupCount; n++)
460             {
461                 itemCount += groups[groupStartIndex + n].Count;
462             }
463             return itemCount;
464         }
465     }
466 }