[NUI] Introduce CollectionView and related classes. (#2525)
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI.Components / Controls / RecyclerView / ItemSource / ObservableItemSource.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.Specialized;
20
21 namespace Tizen.NUI.Components
22 {
23     internal class ObservableItemSource : IItemSource
24     {
25         readonly IEnumerable itemsSource;
26         readonly ICollectionChangedNotifier notifier;
27         bool disposed;
28
29         public ObservableItemSource(IEnumerable itemSource, ICollectionChangedNotifier changedNotifier)
30         {
31             itemsSource = itemSource as IList ?? itemSource as IEnumerable;
32             notifier = changedNotifier;
33
34             ((INotifyCollectionChanged)itemSource).CollectionChanged += CollectionChanged;
35         }
36
37
38         internal event NotifyCollectionChangedEventHandler CollectionItemsSourceChanged;
39
40         public int Count => ItemsCount() + (HasHeader ? 1 : 0) + (HasFooter ? 1 : 0);
41
42         public bool HasHeader { get; set; }
43         public bool HasFooter { get; set; }
44
45         public void Dispose()
46         {
47             Dispose(true);
48         }
49
50         public bool IsFooter(int index)
51         {
52             return HasFooter && index == Count - 1;
53         }
54
55         public bool IsHeader(int index)
56         {
57             return HasHeader && index == 0;
58         }
59
60         public int GetPosition(object item)
61         {
62             for (int n = 0; n < ItemsCount(); n++)
63             {
64                 var elementByIndex = ElementAt(n);
65                 var isEqual = elementByIndex == item || (elementByIndex != null && item != null && elementByIndex.Equals(item));
66
67                 if (isEqual)
68                 {
69                     return AdjustPositionForHeader(n);
70                 }
71             }
72
73             return -1;
74         }
75
76         public object GetItem(int position)
77         {
78             return ElementAt(AdjustIndexForHeader(position));
79         }
80
81         protected virtual void Dispose(bool disposing)
82         {
83             if (disposed)
84             {
85                 return;
86             }
87
88             disposed = true;
89
90             if (disposing)
91             {
92                 ((INotifyCollectionChanged)itemsSource).CollectionChanged -= CollectionChanged;
93             }
94         }
95
96         int AdjustIndexForHeader(int index)
97         {
98             return index - (HasHeader ? 1 : 0);
99         }
100
101         int AdjustPositionForHeader(int position)
102         {
103             return position + (HasHeader ? 1 : 0);
104         }
105
106         void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
107         {/*
108             if (Device.IsInvokeRequired)
109             {
110                 Device.BeginInvokeOnMainThread(() => CollectionChanged(args));
111             }
112             else
113             {*/
114                 CollectionChanged(args);
115             //}
116
117         }
118
119         void CollectionChanged(NotifyCollectionChangedEventArgs args)
120         {
121             switch (args.Action)
122             {
123                 case NotifyCollectionChangedAction.Add:
124                     Add(args);
125                     break;
126                 case NotifyCollectionChangedAction.Remove:
127                     Remove(args);
128                     break;
129                 case NotifyCollectionChangedAction.Replace:
130                     Replace(args);
131                     break;
132                 case NotifyCollectionChangedAction.Move:
133                     Move(args);
134                     break;
135                 case NotifyCollectionChangedAction.Reset:
136                     notifier.NotifyDataSetChanged();
137                     break;
138                 default:
139                     throw new ArgumentOutOfRangeException(nameof(args));
140             }
141             CollectionItemsSourceChanged?.Invoke(this, args);
142         }
143
144         void Move(NotifyCollectionChangedEventArgs args)
145         {
146             var count = args.NewItems.Count;
147
148             if (count == 1)
149             {
150                 // For a single item, we can use NotifyItemMoved and get the animation
151                 notifier.NotifyItemMoved(this, AdjustPositionForHeader(args.OldStartingIndex), AdjustPositionForHeader(args.NewStartingIndex));
152                 return;
153             }
154
155             var start = AdjustPositionForHeader(Math.Min(args.OldStartingIndex, args.NewStartingIndex));
156             var end = AdjustPositionForHeader(Math.Max(args.OldStartingIndex, args.NewStartingIndex) + count);
157             notifier.NotifyItemRangeChanged(this, start, end);
158         }
159
160         void Add(NotifyCollectionChangedEventArgs args)
161         {
162             var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : IndexOf(args.NewItems[0]);
163             startIndex = AdjustPositionForHeader(startIndex);
164             var count = args.NewItems.Count;
165
166             if (count == 1)
167             {
168                 notifier.NotifyItemInserted(this, startIndex);
169                 return;
170             }
171
172             notifier.NotifyItemRangeInserted(this, startIndex, count);
173         }
174
175         void Remove(NotifyCollectionChangedEventArgs args)
176         {
177             var startIndex = args.OldStartingIndex;
178
179             if (startIndex < 0)
180             {
181                 // INCC implementation isn't giving us enough information to know where the removed items were in the
182                 // collection. So the best we can do is a NotifyDataSetChanged()
183                 notifier.NotifyDataSetChanged();
184                 return;
185             }
186
187             startIndex = AdjustPositionForHeader(startIndex);
188
189             // If we have a start index, we can be more clever about removing the item(s) (and get the nifty animations)
190             var count = args.OldItems.Count;
191
192             if (count == 1)
193             {
194                 notifier.NotifyItemRemoved(this, startIndex);
195                 return;
196             }
197
198             notifier.NotifyItemRangeRemoved(this, startIndex, count);
199         }
200
201         void Replace(NotifyCollectionChangedEventArgs args)
202         {
203             var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : IndexOf(args.NewItems[0]);
204             startIndex = AdjustPositionForHeader(startIndex);
205             var newCount = args.NewItems.Count;
206
207             if (newCount == args.OldItems.Count)
208             {
209                 // We are replacing one set of items with a set of equal size; we can do a simple item or range 
210                 // notification to the adapter
211                 if (newCount == 1)
212                 {
213                     notifier.NotifyItemChanged(this, startIndex);
214                 }
215                 else
216                 {
217                     notifier.NotifyItemRangeChanged(this, startIndex, newCount);
218                 }
219
220                 return;
221             }
222
223             // The original and replacement sets are of unequal size; this means that everything currently in view will 
224             // have to be updated. So we just have to use NotifyDataSetChanged and let the RecyclerView update everything
225             notifier.NotifyDataSetChanged();
226         }
227
228         internal int ItemsCount()
229         {
230             if (itemsSource is IList list)
231                 return list.Count;
232
233             int count = 0;
234             foreach (var item in itemsSource)
235                 count++;
236             return count;
237         }
238
239         internal object ElementAt(int index)
240         {
241             if (itemsSource is IList list)
242                 return list[index];
243
244             int count = 0;
245             foreach (var item in itemsSource)
246             {
247                 if (count == index)
248                     return item;
249                 count++;
250             }
251
252             return -1;
253         }
254
255         internal int IndexOf(object item)
256         {
257             if (itemsSource is IList list)
258                 return list.IndexOf(item);
259
260             int count = 0;
261             foreach (var i in itemsSource)
262             {
263                 if (i == item)
264                     return count;
265                 count++;
266             }
267
268             return -1;
269         }
270     }
271 }