[NUI] Introduce CollectionView and related classes. (#2525)
[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.Linq;
18 using System.Collections;
19 using System.Collections.Generic;
20 using System.ComponentModel;
21 using Tizen.NUI.BaseComponents;
22 using Tizen.NUI.Binding;
23
24 namespace Tizen.NUI.Components
25 {
26     /// <summary>
27     /// [Draft] This class provides a View that can layouting items in list and grid with high performance.
28     /// </summary>
29     [EditorBrowsable(EditorBrowsableState.Never)]
30     public abstract class RecyclerView : ScrollableBase, ICollectionChangedNotifier
31     {
32         /// <summary>
33         /// Base Constructor
34         /// </summary>
35        [EditorBrowsable(EditorBrowsableState.Never)]
36         public RecyclerView() : base()
37         {
38             Scrolling += OnScrolling;
39         }
40
41         /// <summary>
42         /// Item's source data.
43         /// </summary>
44         [EditorBrowsable(EditorBrowsableState.Never)]
45         public virtual IEnumerable ItemsSource { get; set; }
46
47         /// <summary>
48         /// DataTemplate for items.
49         /// </summary>
50         [EditorBrowsable(EditorBrowsableState.Never)]
51         public virtual DataTemplate ItemTemplate { get; set; }
52
53         /// <summary>
54         /// Internal encapsulated items data source.
55         /// </summary>
56         internal IItemSource InternalItemSource { get; set;}
57
58         /// <summary>
59         /// RecycleCache of ViewItem.
60         /// </summary>
61         [EditorBrowsable(EditorBrowsableState.Never)]
62         protected List<RecyclerViewItem> RecycleCache { get; } = new List<RecyclerViewItem>();
63
64         /// <summary>
65         /// Internal Items Layouter.
66         /// </summary>
67         [EditorBrowsable(EditorBrowsableState.Never)]
68         protected ItemsLayouter InternalItemsLayouter {get; set; }
69
70         /// <summary>
71         /// Max size of RecycleCache. Default is 50.
72         /// </summary>
73         [EditorBrowsable(EditorBrowsableState.Never)]
74         protected int CacheMax { get; set; } = 50;
75
76         /// <inheritdoc/>
77         [EditorBrowsable(EditorBrowsableState.Never)]
78         public override void OnRelayout(Vector2 size, RelayoutContainer container)
79         {
80             //Console.WriteLine("[NUI] On ReLayout [{0} {0}]", size.X, size.Y);
81             base.OnRelayout(size, container);
82             if (InternalItemsLayouter != null && ItemsSource != null && ItemTemplate != null) 
83             {
84                 InternalItemsLayouter.Initialize(this);
85                 InternalItemsLayouter.RequestLayout(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y, true);
86             }
87         }
88
89         /// <summary>
90         /// Notify Dataset is Changed.
91         /// </summary>
92         [EditorBrowsable(EditorBrowsableState.Never)]
93         public void NotifyDataSetChanged()
94         {
95             //Need to update view.
96             if (InternalItemsLayouter != null)
97             {
98                 InternalItemsLayouter.NotifyDataSetChanged();
99                 if (ScrollingDirection == Direction.Horizontal)
100                 {
101                     ContentContainer.SizeWidth =
102                         InternalItemsLayouter.CalculateLayoutOrientationSize();
103                 }
104                 else
105                 {
106                     ContentContainer.SizeHeight =
107                         InternalItemsLayouter.CalculateLayoutOrientationSize();
108                 }
109             }
110         }
111
112         /// <summary>
113         /// Notify observable item is changed.
114         /// </summary>
115         /// <param name="source">Dataset source.</param>
116         /// <param name="startIndex">Changed item index.</param>
117         [EditorBrowsable(EditorBrowsableState.Never)]
118         public void NotifyItemChanged(IItemSource source, int startIndex)
119         {
120             if (InternalItemsLayouter != null)
121             {
122                 InternalItemsLayouter.NotifyItemChanged(source, startIndex);
123             }
124         }
125
126         /// <summary>
127         /// Notify observable item is inserted in dataset.
128         /// </summary>
129         /// <param name="source">Dataset source.</param>
130         /// <param name="startIndex">Inserted item index.</param>
131         [EditorBrowsable(EditorBrowsableState.Never)]
132         public void NotifyItemInserted(IItemSource source, int startIndex)
133         {
134             if (InternalItemsLayouter != null)
135             {
136                 InternalItemsLayouter.NotifyItemInserted(source, startIndex);
137             }
138         }
139
140         /// <summary>
141         /// Notify observable item is moved from fromPosition to ToPosition.
142         /// </summary>
143         /// <param name="source">Dataset source.</param>
144         /// <param name="fromPosition">Previous item position.</param>
145         /// <param name="toPosition">Moved item position.</param>
146         [EditorBrowsable(EditorBrowsableState.Never)]
147         public void NotifyItemMoved(IItemSource source, int fromPosition, int toPosition)
148         {
149             if (InternalItemsLayouter != null)
150             {
151                 InternalItemsLayouter.NotifyItemMoved(source, fromPosition, toPosition);
152             }
153         }
154
155         /// <summary>
156         /// Notify range of observable items from start to end are changed.
157         /// </summary>
158         /// <param name="source">Dataset source.</param>
159         /// <param name="start">Start index of changed items range.</param>
160         /// <param name="end">End index of changed items range.</param>
161         [EditorBrowsable(EditorBrowsableState.Never)]
162         public void NotifyItemRangeChanged(IItemSource source, int start, int end)
163         {
164             if (InternalItemsLayouter != null)
165             {
166                 InternalItemsLayouter.NotifyItemRangeChanged(source, start, end);
167             }
168         }
169
170         /// <summary>
171         /// Notify count range of observable count items are inserted in startIndex.
172         /// </summary>
173         /// <param name="source">Dataset source.</param>
174         /// <param name="startIndex">Start index of inserted items range.</param>
175         /// <param name="count">The number of inserted items.</param>
176         [EditorBrowsable(EditorBrowsableState.Never)]
177         public void NotifyItemRangeInserted(IItemSource source, int startIndex, int count)
178         {
179             if (InternalItemsLayouter != null)
180             {
181                 InternalItemsLayouter.NotifyItemRangeInserted(source, startIndex, count);
182             }
183         }
184
185         /// <summary>
186         /// Notify the count range of observable items from the startIndex are removed.
187         /// </summary>
188         /// <param name="source">Dataset source.</param>
189         /// <param name="startIndex">Start index of removed items range.</param>
190         /// <param name="count">The number of removed items</param>
191         [EditorBrowsable(EditorBrowsableState.Never)]
192         public void NotifyItemRangeRemoved(IItemSource source, int startIndex, int count)
193         {
194             if (InternalItemsLayouter != null)
195             {
196                 InternalItemsLayouter.NotifyItemRangeRemoved(source, startIndex, count);
197             }
198         }
199
200         /// <summary>
201         /// Notify the observable item in startIndex is removed.
202         /// </summary>
203         /// <param name="source">Dataset source.</param>
204         /// <param name="startIndex">Index of removed item.</param>
205         [EditorBrowsable(EditorBrowsableState.Never)]
206         public void NotifyItemRemoved(IItemSource source, int startIndex)
207         {
208             if (InternalItemsLayouter != null)
209             {
210                 InternalItemsLayouter.NotifyItemRemoved(source, startIndex);
211             }
212         }
213
214         /// <summary>
215         /// Realize indexed item.
216         /// </summary>
217         /// <param name="index"> Index position of realizing item </param>
218         internal virtual RecyclerViewItem RealizeItem(int index)
219         {
220             object context = InternalItemSource.GetItem(index);
221             // Check DataTemplate is Same!
222             if (ItemTemplate is DataTemplateSelector)
223             {
224                 // Need to implements for caching of selector!
225             }
226             else
227             {
228                // pop item
229                RecyclerViewItem item = PopRecycleCache(ItemTemplate);
230                if (item != null)
231                {
232                     DecorateItem(item, index, context);
233                     return item;
234                }
235             }
236
237             object content = DataTemplateExtensions.CreateContent(ItemTemplate, context, (BindableObject)this) ?? throw new Exception("Template return null object.");
238             if (content is RecyclerViewItem)
239             {
240                 RecyclerViewItem item = (RecyclerViewItem)content;
241                 ContentContainer.Add(item);
242                 DecorateItem(item, index, context);
243                 return item;
244             }
245             else
246             {
247                 throw new Exception("Template content must be type of ViewItem");
248             }
249
250         }
251
252         /// <summary>
253         /// Unrealize indexed item.
254         /// </summary>
255         /// <param name="item"> Target item for unrealizing </param>
256         /// <param name="recycle"> Allow recycle. default is true </param>
257         internal virtual void UnrealizeItem(RecyclerViewItem item, bool recycle = true)
258         {
259             item.Index = -1;
260             item.ParentItemsView = null;
261             // Remove BindingContext null set for performance improving.
262             //item.BindingContext = null; 
263             item.IsPressed = false;
264             item.IsSelected = false;
265             item.IsEnabled = true;
266             // Remove Update Style on default for performance improving.
267             //item.UpdateState();
268             item.Relayout -= OnItemRelayout;
269
270             if (!recycle || !PushRecycleCache(item))
271             {
272                 //ContentContainer.Remove(item);
273                 Utility.Dispose(item);
274             }
275         }
276
277         /// <summary>
278         /// Adjust scrolling position by own scrolling rules.
279         /// Override this function when developer wants to change destination of flicking.(e.g. always snap to center of item)
280         /// </summary>
281         /// <param name="position">Scroll position which is calculated by ScrollableBase.</param>
282         /// <returns>Adjusted scroll destination</returns>
283         [EditorBrowsable(EditorBrowsableState.Never)]
284         protected override float AdjustTargetPositionOfScrollAnimation(float position)
285         {
286             // Destination is depending on implementation of layout manager.
287             // Get destination from layout manager.
288             return InternalItemsLayouter.CalculateCandidateScrollPosition(position);
289         }
290
291         /// <summary>
292         /// Push the item into the recycle cache. this item will be reused in view update.
293         /// </summary>
294         /// <param name="item"> Target item to push into recycle cache. </param>
295         [EditorBrowsable(EditorBrowsableState.Never)]
296         protected virtual bool PushRecycleCache(RecyclerViewItem item)
297         {
298             if (item == null) throw new ArgumentNullException(nameof(item));
299             if (RecycleCache.Count >= CacheMax) return false;
300             if (item.Template == null) return false;
301             item.Hide();
302             item.Index = -1;
303             RecycleCache.Add(item);
304             return true;
305         }
306
307         /// <summary>
308         /// Pop the item from the recycle cache.
309         /// </summary>
310         /// <param name="Template"> Template of wanted item. </param>
311         [EditorBrowsable(EditorBrowsableState.Never)]
312         protected virtual RecyclerViewItem PopRecycleCache(DataTemplate Template)
313         {
314            for (int i = 0; i < RecycleCache.Count; i++)
315            {
316                RecyclerViewItem item = RecycleCache[i];
317                if (item.Template == Template)
318                {
319                    RecycleCache.Remove(item);
320                    item.Show();
321                    return item;
322                }
323            }
324            return null;
325         }
326
327         /// <summary>
328         /// On scroll event callback.
329         /// </summary>
330         [EditorBrowsable(EditorBrowsableState.Never)]
331         protected virtual void OnScrolling(object source, ScrollEventArgs args)
332         {
333             if (args == null) throw new ArgumentNullException(nameof(args));
334             if (!disposed && InternalItemsLayouter != null && ItemsSource != null && ItemTemplate != null)
335             {
336                 //Console.WriteLine("[NUI] On Scrolling! {0} => {1}", ScrollPosition.Y, args.Position.Y);
337                 InternalItemsLayouter.RequestLayout(ScrollingDirection == Direction.Horizontal ? args.Position.X : args.Position.Y);
338             }
339         }
340
341         /// <summary>
342         /// Dispose ItemsView and all children on it.
343         /// </summary>
344         /// <param name="type">Dispose type.</param>
345         [EditorBrowsable(EditorBrowsableState.Never)]
346         protected override void Dispose(DisposeTypes type)
347         {
348             if (disposed)
349             {
350                 return;
351             }
352
353             if (type == DisposeTypes.Explicit)
354             {
355                 disposed = true;
356                 // call the clear!
357                 if (RecycleCache != null)
358                 {
359                     foreach (RecyclerViewItem item in RecycleCache)
360                     {
361                         //ContentContainer.Remove(item);
362                         Utility.Dispose(item);
363                     }
364                     RecycleCache.Clear();
365                 }
366                 InternalItemsLayouter.Clear();
367                 InternalItemsLayouter = null;
368                 ItemsSource = null;
369                 ItemTemplate = null;
370                 if (InternalItemSource != null) InternalItemSource.Dispose();
371                 //
372             }
373
374             base.Dispose(type);
375         }
376
377         private void OnItemRelayout(object sender, EventArgs e)
378         {
379             //FIXME: we need to skip the first relayout and only call size changed when real size change happen.
380             //InternalItemsLayouter.NotifyItemSizeChanged((sender as ViewItem));
381             //InternalItemsLayouter.RequestLayout(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
382         }
383
384         private void DecorateItem(RecyclerViewItem item, int index, object context)
385         {
386             item.Index = index;
387             item.ParentItemsView = this;
388             item.Template = (ItemTemplate as DataTemplateSelector)?.SelectDataTemplate(InternalItemSource.GetItem(index), this) ?? ItemTemplate;
389             item.BindingContext = context;
390             item.Relayout += OnItemRelayout;
391         }
392     }
393 }