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