[NUI] Minor refactoring CollectionView and RecyclerView
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI.Components / Controls / RecyclerView / RecyclerView.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 using System;
17 using System.Collections;
18 using System.Collections.Generic;
19 using System.ComponentModel;
20 using Tizen.NUI.Binding;
21
22 namespace Tizen.NUI.Components
23 {
24     /// <summary>
25     /// A View that serves as a base class for views that contain a templated list of items.
26     /// </summary>
27     /// <since_tizen> 9 </since_tizen>
28     public abstract class RecyclerView : ScrollableBase, ICollectionChangedNotifier
29     {
30         /// <summary>
31         /// ItemsSourceProperty
32         /// </summary>
33         [EditorBrowsable(EditorBrowsableState.Never)]
34         public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable), typeof(RecyclerView), null, propertyChanged: (bindable, oldValue, newValue) =>
35         {
36             var instance = bindable as RecyclerView;
37             if (instance == null)
38             {
39                 throw new Exception("Bindable object is not RecyclerView.");
40             }
41             if (newValue != null)
42             {
43                 instance.InternalItemsSource = newValue as IEnumerable;
44             }
45         },
46         defaultValueCreator: (bindable) =>
47         {
48             var instance = bindable as RecyclerView;
49             if (instance == null)
50             {
51                 throw new Exception("Bindable object is not RecyclerView.");
52             }
53             return instance.InternalItemsSource;
54         });
55
56         /// <summary>
57         /// ItemTemplateProperty
58         /// </summary>
59         [EditorBrowsable(EditorBrowsableState.Never)]
60         public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(RecyclerView), null, propertyChanged: (bindable, oldValue, newValue) =>
61         {
62             var instance = bindable as RecyclerView;
63             if (instance == null)
64             {
65                 throw new Exception("Bindable object is not RecyclerView.");
66             }
67             if (newValue != null)
68             {
69                 instance.InternalItemTemplate = newValue as DataTemplate;
70             }
71         },
72         defaultValueCreator: (bindable) =>
73         {
74             var instance = bindable as RecyclerView;
75             if (instance == null)
76             {
77                 throw new Exception("Bindable object is not RecyclerView.");
78             }
79             return instance.InternalItemTemplate;
80         });
81
82         /// <summary>
83         /// Base Constructor
84         /// </summary>
85         /// <since_tizen> 9 </since_tizen>
86         public RecyclerView() : base()
87         {
88             Scrolling += OnScrolling;
89         }
90
91         /// <summary>
92         /// Item's source data.
93         /// </summary>
94         /// <since_tizen> 9 </since_tizen>
95         public virtual IEnumerable ItemsSource
96         {
97             get
98             {
99                 return GetValue(ItemsSourceProperty) as IEnumerable;
100             }
101             set
102             {
103                 SetValue(ItemsSourceProperty, value);
104                 NotifyPropertyChanged();
105             }
106         }
107         internal virtual IEnumerable InternalItemsSource { get; set; }
108
109         /// <summary>
110         /// DataTemplate for items.
111         /// </summary>
112         /// <since_tizen> 9 </since_tizen>
113         public virtual DataTemplate ItemTemplate
114         {
115             get
116             {
117                 return GetValue(ItemTemplateProperty) as DataTemplate;
118             }
119             set
120             {
121                 SetValue(ItemTemplateProperty, value);
122                 NotifyPropertyChanged();
123             }
124         }
125         internal virtual DataTemplate InternalItemTemplate { get; set; }
126
127         /// <summary>
128         /// Internal encapsulated items data source.
129         /// </summary>
130         internal IItemSource InternalItemSource { get; set; }
131
132         /// <summary>
133         /// RecycleCache of ViewItem.
134         /// </summary>
135         [EditorBrowsable(EditorBrowsableState.Never)]
136         protected List<RecyclerViewItem> RecycleCache { get; } = new List<RecyclerViewItem>();
137
138         /// <summary>
139         /// Internal Items Layouter.
140         /// </summary>
141         [EditorBrowsable(EditorBrowsableState.Never)]
142         protected virtual ItemsLayouter InternalItemsLayouter { get; set; }
143
144         /// <summary>
145         /// Max size of RecycleCache. Default is 50.
146         /// </summary>
147         [EditorBrowsable(EditorBrowsableState.Never)]
148         protected int CacheMax { get; set; } = 50;
149
150         /// <inheritdoc/>
151         /// <since_tizen> 9 </since_tizen>
152         public override void OnRelayout(Vector2 size, RelayoutContainer container)
153         {
154             //Console.WriteLine("[NUI] On ReLayout [{0} {0}]", size.X, size.Y);
155             base.OnRelayout(size, container);
156             if (InternalItemsLayouter != null && ItemsSource != null && ItemTemplate != null)
157             {
158                 InternalItemsLayouter.Initialize(this);
159                 InternalItemsLayouter.RequestLayout(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y, true);
160             }
161         }
162
163         /// <summary>
164         /// Notify Dataset is Changed.
165         /// </summary>
166         [EditorBrowsable(EditorBrowsableState.Never)]
167         public virtual void NotifyDataSetChanged()
168         {
169             //Need to update view.
170             if (InternalItemsLayouter != null)
171             {
172                 InternalItemsLayouter.NotifyDataSetChanged();
173                 if (ScrollingDirection == Direction.Horizontal)
174                 {
175                     ContentContainer.SizeWidth =
176                         InternalItemsLayouter.CalculateLayoutOrientationSize();
177                 }
178                 else
179                 {
180                     ContentContainer.SizeHeight =
181                         InternalItemsLayouter.CalculateLayoutOrientationSize();
182                 }
183             }
184         }
185
186         /// <summary>
187         /// Notify observable item is changed.
188         /// </summary>
189         /// <param name="source">Dataset source.</param>
190         /// <param name="startIndex">Changed item index.</param>
191         [EditorBrowsable(EditorBrowsableState.Never)]
192         public virtual void NotifyItemChanged(IItemSource source, int startIndex)
193         {
194             if (InternalItemsLayouter != null)
195             {
196                 InternalItemsLayouter.NotifyItemChanged(source, startIndex);
197             }
198         }
199
200         /// <summary>
201         /// Notify range of observable items from start to end are changed.
202         /// </summary>
203         /// <param name="source">Dataset source.</param>
204         /// <param name="startRange">Start index of changed items range.</param>
205         /// <param name="endRange">End index of changed items range.</param>
206         [EditorBrowsable(EditorBrowsableState.Never)]
207         public virtual void NotifyItemRangeChanged(IItemSource source, int startRange, int endRange)
208         {
209             if (InternalItemsLayouter != null)
210             {
211                 InternalItemsLayouter.NotifyItemRangeChanged(source, startRange, endRange);
212             }
213         }
214
215         /// <summary>
216         /// Notify observable item is inserted in dataset.
217         /// </summary>
218         /// <param name="source">Dataset source.</param>
219         /// <param name="startIndex">Inserted item index.</param>
220         [EditorBrowsable(EditorBrowsableState.Never)]
221         public virtual void NotifyItemInserted(IItemSource source, int startIndex)
222         {
223             if (InternalItemsLayouter != null)
224             {
225                 InternalItemsLayouter.NotifyItemInserted(source, startIndex);
226             }
227         }
228
229         /// <summary>
230         /// Notify count range of observable count items are inserted in startIndex.
231         /// </summary>
232         /// <param name="source">Dataset source.</param>
233         /// <param name="startIndex">Start index of inserted items range.</param>
234         /// <param name="count">The number of inserted items.</param>
235         [EditorBrowsable(EditorBrowsableState.Never)]
236         public virtual void NotifyItemRangeInserted(IItemSource source, int startIndex, int count)
237         {
238             if (InternalItemsLayouter != null)
239             {
240                 InternalItemsLayouter.NotifyItemRangeInserted(source, startIndex, count);
241             }
242         }
243
244         /// <summary>
245         /// Notify observable item is moved from fromPosition to ToPosition.
246         /// </summary>
247         /// <param name="source">Dataset source.</param>
248         /// <param name="fromPosition">Previous item position.</param>
249         /// <param name="toPosition">Moved item position.</param>
250         [EditorBrowsable(EditorBrowsableState.Never)]
251         public virtual void NotifyItemMoved(IItemSource source, int fromPosition, int toPosition)
252         {
253             if (InternalItemsLayouter != null)
254             {
255                 InternalItemsLayouter.NotifyItemMoved(source, fromPosition, toPosition);
256             }
257         }
258
259         /// <summary>
260         /// Notify the observable item is moved from fromPosition to ToPosition.
261         /// </summary>
262         /// <param name="source"></param>
263         /// <param name="fromPosition"></param>
264         /// <param name="toPosition"></param>
265         /// <param name="count"></param>
266         [EditorBrowsable(EditorBrowsableState.Never)]
267         public virtual void NotifyItemRangeMoved(IItemSource source, int fromPosition, int toPosition, int count)
268         {
269             if (InternalItemsLayouter != null)
270             {
271                 InternalItemsLayouter.NotifyItemRangeMoved(source, fromPosition, toPosition, count);
272             }
273         }
274
275         /// <summary>
276         /// Notify the observable item in startIndex is removed.
277         /// </summary>
278         /// <param name="source">Dataset source.</param>
279         /// <param name="startIndex">Index of removed item.</param>
280         [EditorBrowsable(EditorBrowsableState.Never)]
281         public virtual void NotifyItemRemoved(IItemSource source, int startIndex)
282         {
283             if (InternalItemsLayouter != null)
284             {
285                 InternalItemsLayouter.NotifyItemRemoved(source, startIndex);
286             }
287         }
288
289         /// <summary>
290         /// Notify the count range of observable items from the startIndex are removed.
291         /// </summary>
292         /// <param name="source">Dataset source.</param>
293         /// <param name="startIndex">Start index of removed items range.</param>
294         /// <param name="count">The number of removed items</param>
295         [EditorBrowsable(EditorBrowsableState.Never)]
296         public virtual void NotifyItemRangeRemoved(IItemSource source, int startIndex, int count)
297         {
298             if (InternalItemsLayouter != null)
299             {
300                 InternalItemsLayouter.NotifyItemRangeRemoved(source, startIndex, count);
301             }
302         }
303
304         /// <summary>
305         /// Realize indexed item.
306         /// </summary>
307         /// <param name="index"> Index position of realizing item </param>
308         internal virtual RecyclerViewItem RealizeItem(int index)
309         {
310             object context = InternalItemSource.GetItem(index);
311             // Check DataTemplate is Same!
312             if (ItemTemplate is DataTemplateSelector)
313             {
314                 // Need to implements for caching of selector!
315             }
316             else
317             {
318                 // pop item
319                 RecyclerViewItem item = PopRecycleCache(ItemTemplate);
320                 if (item != null)
321                 {
322                     DecorateItem(item, index, context);
323                     return item;
324                 }
325             }
326
327             object content = DataTemplateExtensions.CreateContent(ItemTemplate, context, (BindableObject)this) ?? throw new Exception("Template return null object.");
328             if (content is RecyclerViewItem)
329             {
330                 RecyclerViewItem item = (RecyclerViewItem)content;
331                 ContentContainer.Add(item);
332                 DecorateItem(item, index, context);
333                 return item;
334             }
335             else
336             {
337                 throw new Exception("Template content must be type of ViewItem");
338             }
339
340         }
341
342         /// <summary>
343         /// Unrealize indexed item.
344         /// </summary>
345         /// <param name="item"> Target item for unrealizing </param>
346         /// <param name="recycle"> Allow recycle. default is true </param>
347         internal virtual void UnrealizeItem(RecyclerViewItem item, bool recycle = true)
348         {
349             if (item == null)
350             {
351                 return;
352             }
353
354             item.Index = -1;
355             item.ParentItemsView = null;
356             item.BindingContext = null;
357             item.IsPressed = false;
358             item.IsSelected = false;
359             item.IsEnabled = true;
360             item.UpdateState();
361             item.Relayout -= OnItemRelayout;
362
363             if (!recycle || !PushRecycleCache(item))
364             {
365                 //ContentContainer.Remove(item);
366                 Utility.Dispose(item);
367             }
368         }
369
370         /// <summary>
371         /// Adjust scrolling position by own scrolling rules.
372         /// Override this function when developer wants to change destination of flicking.(e.g. always snap to center of item)
373         /// </summary>
374         /// <param name="position">Scroll position which is calculated by ScrollableBase.</param>
375         /// <returns>Adjusted scroll destination</returns>
376         [EditorBrowsable(EditorBrowsableState.Never)]
377         protected override float AdjustTargetPositionOfScrollAnimation(float position)
378         {
379             // Destination is depending on implementation of layout manager.
380             // Get destination from layout manager.
381             return InternalItemsLayouter.CalculateCandidateScrollPosition(position);
382         }
383
384         /// <summary>
385         /// Push the item into the recycle cache. this item will be reused in view update.
386         /// </summary>
387         /// <param name="item"> Target item to push into recycle cache. </param>
388         [EditorBrowsable(EditorBrowsableState.Never)]
389         protected virtual bool PushRecycleCache(RecyclerViewItem item)
390         {
391             if (item == null)
392             {
393                 throw new ArgumentNullException(nameof(item));
394             }
395
396             if (item.Template == null || RecycleCache.Count >= CacheMax)
397             {
398                 return false;
399             }
400
401             item.Hide();
402             item.Index = -1;
403             RecycleCache.Add(item);
404
405             return true;
406         }
407
408         /// <summary>
409         /// Pop the item from the recycle cache.
410         /// </summary>
411         /// <param name="Template"> Template of wanted item. </param>
412         [EditorBrowsable(EditorBrowsableState.Never)]
413         protected virtual RecyclerViewItem PopRecycleCache(DataTemplate Template)
414         {
415             for (int i = 0; i < RecycleCache.Count; i++)
416             {
417                 RecyclerViewItem item = RecycleCache[i];
418                 if (item.Template == Template)
419                 {
420                     RecycleCache.Remove(item);
421                     item.Show();
422                     return item;
423                 }
424             }
425             return null;
426         }
427
428         /// <summary>
429         /// Clear all remaining caches.
430         /// </summary>
431         [EditorBrowsable(EditorBrowsableState.Never)]
432         protected virtual void ClearCache()
433         {
434             foreach (RecyclerViewItem item in RecycleCache)
435             {
436                 Utility.Dispose(item);
437             }
438             RecycleCache.Clear();
439         }
440
441         /// <summary>
442         /// On scroll event callback.
443         /// </summary>
444         /// <since_tizen> 9 </since_tizen>
445         protected virtual void OnScrolling(object source, ScrollEventArgs args)
446         {
447             if (args == null)
448             {
449                 throw new ArgumentNullException(nameof(args));
450             }
451
452             if (!disposed && InternalItemsLayouter != null && ItemsSource != null && ItemTemplate != null)
453             {
454                 //Console.WriteLine("[NUI] On Scrolling! {0} => {1}", ScrollPosition.Y, args.Position.Y);
455                 InternalItemsLayouter.RequestLayout(ScrollingDirection == Direction.Horizontal ? args.Position.X : args.Position.Y);
456             }
457         }
458
459         /// <summary>
460         /// Dispose ItemsView and all children on it.
461         /// </summary>
462         /// <param name="type">Dispose type.</param>
463         [EditorBrowsable(EditorBrowsableState.Never)]
464         protected override void Dispose(DisposeTypes type)
465         {
466             if (disposed)
467             {
468                 return;
469             }
470
471             if (type == DisposeTypes.Explicit)
472             {
473                 // call the clear!
474                 if (RecycleCache != null)
475                 {
476                     ClearCache();
477                 }
478                 InternalItemsLayouter?.Clear();
479                 InternalItemsLayouter = null;
480                 ItemsSource = null;
481                 ItemTemplate = null;
482                 if (InternalItemSource != null)
483                 {
484                     InternalItemSource.Dispose();
485                     InternalItemSource = null;
486                 }
487                 //
488             }
489
490             base.Dispose(type);
491         }
492
493         private void OnItemRelayout(object sender, EventArgs e)
494         {
495             //FIXME: we need to skip the first relayout and only call size changed when real size change happen.
496             //InternalItemsLayouter.NotifyItemSizeChanged((sender as ViewItem));
497             //InternalItemsLayouter.RequestLayout(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
498         }
499
500         private void DecorateItem(RecyclerViewItem item, int index, object context)
501         {
502             item.Index = index;
503             item.ParentItemsView = this;
504             item.Template = (ItemTemplate as DataTemplateSelector)?.SelectDataTemplate(InternalItemSource.GetItem(index), this) ?? ItemTemplate;
505             item.BindingContext = context;
506             item.Relayout += OnItemRelayout;
507         }
508     }
509 }