6cd77792c922e1989b5c7fb1f84bc985c7ace6e5
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI.Components / Controls / RecyclerView / RecyclerView.cs
1 /* Copyright (c) 2020 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 Tizen.NUI.BaseComponents;
18 using System.Collections.Generic;
19 using System.ComponentModel;
20
21 namespace Tizen.NUI.Components
22 {
23     /// <summary>
24     /// [Draft] This class provides a View that can recycle items to improve performance.
25     /// </summary>
26     /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
27     [EditorBrowsable(EditorBrowsableState.Never)]
28     public class RecyclerView : ScrollableBase
29     {
30         private RecycleAdapter adapter;
31         private RecycleLayoutManager layoutManager;
32         private int totalItemCount = 15;
33         private List<PropertyNotification> notifications = new List<PropertyNotification>();
34
35         public RecyclerView() : base()
36         {
37             Initialize(new RecycleAdapter(), new RecycleLayoutManager());
38         }
39
40         /// <summary>
41         /// Default constructor.
42         /// </summary>
43         /// <param name="adapter">Recycle adapter of RecyclerView.</param>
44         /// <param name="layoutManager">Recycle layoutManager of RecyclerView.</param>
45         /// <since_tizen> 8 </since_tizen>
46         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
47         [EditorBrowsable(EditorBrowsableState.Never)]
48         public RecyclerView(RecycleAdapter adapter, RecycleLayoutManager layoutManager)
49         {
50             Initialize(adapter, layoutManager);
51         }
52
53         private void Initialize(RecycleAdapter adapter, RecycleLayoutManager layoutManager)
54         {
55             FocusGroup = true;
56             SetKeyboardNavigationSupport(true);
57             Scrolling += OnScrolling;
58
59             this.adapter = adapter;
60             this.adapter.OnDataChanged += OnAdapterDataChanged;
61
62             this.layoutManager = layoutManager;
63             this.layoutManager.Container = ContentContainer;
64             this.layoutManager.ItemSize = this.adapter.CreateRecycleItem().Size;
65             this.layoutManager.DataCount = this.adapter.Data.Count;
66
67             InitializeItems();
68         }
69
70         private void OnItemSizeChanged(object source, PropertyNotification.NotifyEventArgs args)
71         {
72             layoutManager.Layout(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
73         }
74
75         public int TotalItemCount
76         {
77             get
78             {
79                 return totalItemCount;
80             }
81             set
82             {
83                 totalItemCount = value;
84                 InitializeItems();
85             }
86         }
87
88         private void InitializeItems()
89         {
90             for (int i = Children.Count - 1; i > -1; i--)
91             {
92                 Children[i].Unparent();
93                 notifications[i].Notified -= OnItemSizeChanged;
94                 notifications.RemoveAt(i);
95             }
96
97             for (int i = 0; i < totalItemCount; i++)
98             {
99                 RecycleItem item = adapter.CreateRecycleItem();
100                 item.DataIndex = i;
101                 item.Name = "[" + i + "] recycle";
102
103                 if (i < adapter.Data.Count)
104                 {
105                     adapter.BindData(item);
106                 }
107                 Add(item);
108
109                 PropertyNotification noti = item.AddPropertyNotification("size", PropertyCondition.Step(0.1f));
110                 noti.Notified += OnItemSizeChanged;
111                 notifications.Add(noti);
112             }
113
114             layoutManager.Layout(0.0f);
115
116             if (ScrollingDirection == Direction.Horizontal)
117             {
118                 ContentContainer.SizeWidth = layoutManager.CalculateLayoutOrientationSize();
119             }
120             else
121             {
122                 ContentContainer.SizeHeight = layoutManager.CalculateLayoutOrientationSize();
123             }
124         }
125
126
127         public new Direction ScrollingDirection
128         {
129             get
130             {
131                 return base.ScrollingDirection;
132             }
133             set
134             {
135                 base.ScrollingDirection = value;
136
137                 if (ScrollingDirection == Direction.Horizontal)
138                 {
139                     ContentContainer.SizeWidth = layoutManager.CalculateLayoutOrientationSize();
140                 }
141                 else
142                 {
143                     ContentContainer.SizeHeight = layoutManager.CalculateLayoutOrientationSize();
144                 }
145             }
146         }
147
148         /// <summary>
149         /// Recycler adpater.
150         /// </summary>
151         /// <since_tizen> 8 </since_tizen>
152         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
153         [EditorBrowsable(EditorBrowsableState.Never)]
154         public RecycleAdapter Adapter
155         {
156             get
157             {
158                 return adapter;
159             }
160             set
161             {
162                 if (adapter != null)
163                 {
164                     adapter.OnDataChanged -= OnAdapterDataChanged;
165                 }
166
167                 adapter = value;
168                 adapter.OnDataChanged += OnAdapterDataChanged;
169                 layoutManager.ItemSize = adapter.CreateRecycleItem().Size;
170                 layoutManager.DataCount = adapter.Data.Count;
171                 InitializeItems();
172             }
173         }
174
175         /// <summary>
176         /// Recycler layoutManager.
177         /// </summary>
178         /// <since_tizen> 8 </since_tizen>
179         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
180         [EditorBrowsable(EditorBrowsableState.Never)]
181         public RecycleLayoutManager LayoutManager
182         {
183             get
184             {
185                 return layoutManager;
186             }
187             set
188             {
189                 layoutManager = value;
190                 layoutManager.Container = ContentContainer;
191                 layoutManager.ItemSize = adapter.CreateRecycleItem().Size;
192                 layoutManager.DataCount = adapter.Data.Count;
193                 InitializeItems();
194             }
195         }
196
197         private void OnScrolling(object source, ScrollEventArgs args)
198         {
199             layoutManager.Layout(ScrollingDirection == Direction.Horizontal ? args.Position.X : args.Position.Y);
200             List<RecycleItem> recycledItemList = layoutManager.Recycle(ScrollingDirection == Direction.Horizontal ? args.Position.X : args.Position.Y);
201             BindData(recycledItemList);
202         }
203
204         private void OnAdapterDataChanged(object source, EventArgs args)
205         {
206             List<RecycleItem> changedData = new List<RecycleItem>();
207
208             foreach (RecycleItem item in Children)
209             {
210                 changedData.Add(item);
211             }
212
213             BindData(changedData);
214         }
215
216         private void BindData(List<RecycleItem> changedData)
217         {
218             foreach (RecycleItem item in changedData)
219             {
220                 if (item.DataIndex > -1 && item.DataIndex < adapter.Data.Count)
221                 {
222                     item.Show();
223                     item.Name = "[" + item.DataIndex + "]";
224                     adapter.BindData(item);
225                 }
226                 else
227                 {
228                     item.Hide();
229                 }
230             }
231         }
232
233         /// <summary>
234         /// Adjust scrolling position by own scrolling rules.
235         /// Override this function when developer wants to change destination of flicking.(e.g. always snap to center of item)
236         /// </summary>
237         /// <param name="position">Scroll position which is calculated by ScrollableBase</param>
238         /// <returns>Adjusted scroll destination</returns>
239         /// <since_tizen> 8 </since_tizen>
240         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
241         [EditorBrowsable(EditorBrowsableState.Never)]
242         protected override float AdjustTargetPositionOfScrollAnimation(float position)
243         {
244             // Destination is depending on implementation of layout manager.
245             // Get destination from layout manager.
246             return layoutManager.CalculateCandidateScrollPosition(position);
247         }
248
249         private View focusedView;
250         private int prevFocusedDataIndex = 0;
251
252         public override View GetNextFocusableView(View currentFocusedView, View.FocusDirection direction, bool loopEnabled)
253         {
254             View nextFocusedView = null;
255
256             if (!focusedView)
257             {
258                 // If focusedView is null, find child which has previous data index
259                 if (Children.Count > 0 && Adapter.Data.Count > 0)
260                 {
261                     for (int i = 0; i < Children.Count; i++)
262                     {
263                         RecycleItem item = Children[i] as RecycleItem;
264                         if (item.DataIndex == prevFocusedDataIndex)
265                         {
266                             nextFocusedView = item;
267                             break;
268                         }
269                     }
270                 }
271             }
272             else
273             {
274                 // If this is not first focus, request next focus to LayoutManager
275                 if (LayoutManager != null)
276                 {
277                     nextFocusedView = LayoutManager.RequestNextFocusableView(currentFocusedView, direction, loopEnabled);
278                 }
279             }
280
281             if (nextFocusedView != null)
282             {
283                 // Check next focused view is inside of visible area.
284                 // If it is not, move scroll position to make it visible.
285                 Position scrollPosition = ContentContainer.CurrentPosition;
286                 float targetPosition = -(ScrollingDirection == Direction.Horizontal ? scrollPosition.X : scrollPosition.Y);
287
288                 float left = nextFocusedView.Position.X;
289                 float right = nextFocusedView.Position.X + nextFocusedView.Size.Width;
290                 float top = nextFocusedView.Position.Y;
291                 float bottom = nextFocusedView.Position.Y + nextFocusedView.Size.Height;
292
293                 float visibleRectangleLeft = -scrollPosition.X;
294                 float visibleRectangleRight = -scrollPosition.X + Size.Width;
295                 float visibleRectangleTop = -scrollPosition.Y;
296                 float visibleRectangleBottom = -scrollPosition.Y + Size.Height;
297
298                 if (ScrollingDirection == Direction.Horizontal)
299                 {
300                     if ((direction == View.FocusDirection.Left || direction == View.FocusDirection.Up) && left < visibleRectangleLeft)
301                     {
302                         targetPosition = left;
303                     }
304                     else if ((direction == View.FocusDirection.Right || direction == View.FocusDirection.Down) && right > visibleRectangleRight)
305                     {
306                         targetPosition = right - Size.Width;
307                     }
308                 }
309                 else
310                 {
311                     if ((direction == View.FocusDirection.Up || direction == View.FocusDirection.Left) && top < visibleRectangleTop)
312                     {
313                         targetPosition = top;
314                     }
315                     else if ((direction == View.FocusDirection.Down || direction == View.FocusDirection.Right) && bottom > visibleRectangleBottom)
316                     {
317                         targetPosition = bottom - Size.Height;
318                     }
319                 }
320
321                 focusedView = nextFocusedView;
322                 if ((nextFocusedView as RecycleItem) != null)
323                 {
324                     prevFocusedDataIndex = (nextFocusedView as RecycleItem).DataIndex;
325                 }
326
327                 ScrollTo(targetPosition, true);
328             }
329             else
330             {
331                 // If nextView is null, it means that we should move focus to outside of Control.
332                 // Return FocusableView depending on direction.
333                 switch (direction)
334                 {
335                     case View.FocusDirection.Left:
336                         {
337                             nextFocusedView = LeftFocusableView;
338                             break;
339                         }
340                     case View.FocusDirection.Right:
341                         {
342                             nextFocusedView = RightFocusableView;
343                             break;
344                         }
345                     case View.FocusDirection.Up:
346                         {
347                             nextFocusedView = UpFocusableView;
348                             break;
349                         }
350                     case View.FocusDirection.Down:
351                         {
352                             nextFocusedView = DownFocusableView;
353                             break;
354                         }
355                 }
356
357                 if (nextFocusedView)
358                 {
359                     focusedView = null;
360                 }
361                 else
362                 {
363                     //If FocusableView doesn't exist, not move focus.
364                     nextFocusedView = focusedView;
365                 }
366             }
367
368             return nextFocusedView;
369         }
370     }
371 }