9035521997a4066044db728ea5d105e7f9934b0b
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI.Components / Controls / Picker.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 Tizen.NUI;
18 using Tizen.NUI.BaseComponents;
19 using System.Collections.Generic;
20 using System.Collections.ObjectModel;
21 using System.ComponentModel;
22 using System.Diagnostics.CodeAnalysis;
23
24 namespace Tizen.NUI.Components
25 {
26     /// <summary>
27     /// ValueChangedEventArgs is a class to notify changed Picker value argument which will sent to user.
28     /// </summary>
29     [EditorBrowsable(EditorBrowsableState.Never)]
30     public class ValueChangedEventArgs : EventArgs
31     {
32         /// <summary>
33         /// ValueChangedEventArgs default constructor.
34         /// <param name="value">value of Picker.</param>
35         /// </summary>
36         [EditorBrowsable(EditorBrowsableState.Never)]   
37         public ValueChangedEventArgs(int value)
38         {
39             Value = value;
40         }
41
42         /// <summary>
43         /// ValueChangedEventArgs default constructor.
44         /// <returns>The current value of Picker.</returns>
45         /// </summary>
46         [EditorBrowsable(EditorBrowsableState.Never)]   
47         public int Value { get; }
48         
49     }
50
51     /// <summary>
52     /// Picker is a class which provides a function that allows the user to select 
53     /// a value through a scrolling motion by expressing the specified value as a list.
54     /// </summary>
55     [EditorBrowsable(EditorBrowsableState.Never)]
56     public class Picker : Control
57     {
58         //Tizen 6.5 base components Picker guide visible scroll item is 5.
59         private const int scrollVisibleItems = 5;
60         //Dummy item count for loop feature. Max value of scrolling distance in 
61         //RPI target is bigger than 20 items height. it can adjust depends on the internal logic and device env.
62         private const int dummyItemsForLoop = 20;             
63         private int startScrollOffset;
64         private int itemHeight;
65         private int startScrollY;
66         private int startY;
67         private int pageSize;
68         private int currentValue;
69         private int maxValue;
70         private int minValue;
71         private int lastScrollPosion;
72         private bool onAnimation; //Scroller on animation check.
73         private bool onAlignAnimation;
74         private bool displayedValuesUpdate; //User sets displayed value check.
75         private bool needItemUpdate; //min, max or display value updated check.
76         private bool loopEnabled;
77         private ReadOnlyCollection<string> displayedValues;
78         private PickerScroller pickerScroller;
79         private View upLine;
80         private View downLine;
81         private IList<TextLabel> itemList;
82         private PickerStyle pickerStyle => ViewStyle as PickerStyle;
83
84         /// <summary>
85         /// Creates a new instance of Picker.
86         /// </summary>
87         [EditorBrowsable(EditorBrowsableState.Never)]
88         public Picker()
89         {
90             Initialize();
91         }
92
93         /// <summary>
94         /// Creates a new instance of Picker.
95         /// </summary>
96         /// <param name="style">Creates Picker by special style defined in UX.</param>
97         [EditorBrowsable(EditorBrowsableState.Never)]
98         public Picker(string style) : base(style)
99         {
100             Initialize();
101         }
102
103         /// <summary>
104         /// Creates a new instance of Picker.
105         /// </summary>
106         /// <param name="pickerStyle">Creates Picker by style customized by user.</param>
107         [EditorBrowsable(EditorBrowsableState.Never)]
108         public Picker(PickerStyle pickerStyle) : base(pickerStyle)
109         {
110             Initialize();
111         }
112
113         /// <summary>
114         /// Dispose Picker and all children on it.
115         /// </summary>
116         /// <param name="type">Dispose type.</param>
117         [EditorBrowsable(EditorBrowsableState.Never)]
118         protected override void Dispose(DisposeTypes type)
119         {
120             if (disposed)
121             {
122                 return;
123             }
124
125             if (type == DisposeTypes.Explicit)
126             {
127                 if (itemList != null)
128                 {
129                     foreach (TextLabel textLabel in itemList)
130                     {
131                         if (pickerScroller) pickerScroller.Remove(textLabel);
132                         Utility.Dispose(textLabel);
133                     }
134
135                     itemList = null;
136                 }
137
138                 if (pickerScroller != null)
139                 {
140                     Remove(pickerScroller);
141                     Utility.Dispose(pickerScroller);
142                     pickerScroller = null;
143                 }
144
145                 Remove(upLine);
146                 Utility.Dispose(upLine);
147                 Remove(downLine);
148                 Utility.Dispose(downLine);
149             }
150
151             base.Dispose(type);
152         }
153
154         /// <summary>
155         /// An event emitted when Picker value changed, user can subscribe or unsubscribe to this event handler.
156         /// </summary>
157         [EditorBrowsable(EditorBrowsableState.Never)]
158         public event EventHandler<ValueChangedEventArgs> ValueChanged;
159
160         //TODO Fomatter here
161
162         /// <summary>
163         /// The values to be displayed instead of numbers.
164         /// </summary>
165         [EditorBrowsable(EditorBrowsableState.Never)]
166         public ReadOnlyCollection<String> DisplayedValues
167         {
168             get
169             {
170                 return displayedValues;
171             }
172             set
173             {
174                 displayedValues = value;
175
176                 needItemUpdate = true;
177                 displayedValuesUpdate = true;
178
179                 UpdateValueList();
180             }
181         }
182         
183         /// <summary>
184         /// The Current value of Picker.
185         /// </summary>
186         [EditorBrowsable(EditorBrowsableState.Never)]
187         public int CurrentValue
188         {
189             get
190             {
191                 return currentValue;
192             }
193             set
194             {
195                 if (currentValue == value) return;
196
197                 if (currentValue < minValue) currentValue = minValue;
198                 else if (currentValue > maxValue) currentValue = maxValue;
199
200                 currentValue = value;
201
202                 UpdateCurrentValue();
203             }
204         }
205
206         /// <summary>
207         /// The max value of Picker.
208         /// </summary>
209         [EditorBrowsable(EditorBrowsableState.Never)]
210         public int MaxValue
211         {
212             get
213             {
214                 return maxValue;
215             }
216             set
217             {
218                 if (maxValue == value) return;
219                 if (currentValue > value) currentValue = value;
220                 
221                 maxValue = value;
222                 needItemUpdate = true;
223
224                 UpdateValueList();
225             }
226         }
227
228         /// <summary>
229         /// The min value of Picker.
230         /// </summary>
231         [EditorBrowsable(EditorBrowsableState.Never)]
232         public int MinValue
233         {
234             get
235             {
236                 return minValue;
237             }
238             set
239             {
240                 if (minValue == value) return;
241                 if (currentValue < value) currentValue = value;
242                 
243                 minValue = value;
244                 needItemUpdate = true;
245
246                 UpdateValueList();
247             }
248         }
249
250         /// <inheritdoc/>
251         [EditorBrowsable(EditorBrowsableState.Never)]
252         public override void OnInitialize()
253         {
254             base.OnInitialize();
255             SetAccessibilityConstructor(Role.List, AccessibilityInterface.Value);
256         }
257
258         /// <summary>
259         /// Applies style to Picker.
260         /// </summary>
261         /// <param name="viewStyle">The style to apply.</param>
262         [EditorBrowsable(EditorBrowsableState.Never)]
263         public override void ApplyStyle(ViewStyle viewStyle)
264         {
265             base.ApplyStyle(viewStyle);
266
267             //Apply StartScrollOffset style.
268             if (pickerStyle?.StartScrollOffset != null)
269                 startScrollOffset = (int)pickerStyle.StartScrollOffset.Height;
270
271             //Apply ItemTextLabel style.
272             if (pickerStyle?.ItemTextLabel != null)
273             {
274                 itemHeight = (int)pickerStyle.ItemTextLabel.Size.Height;
275
276                 if (itemList != null)
277                     foreach (TextLabel textLabel in itemList)
278                         textLabel.ApplyStyle(pickerStyle.ItemTextLabel);
279             }
280
281             //Apply PickerCenterLine style.
282             if (pickerStyle?.Divider != null && upLine != null && downLine != null)
283             {
284                 upLine.ApplyStyle(pickerStyle.Divider);
285                 downLine.ApplyStyle(pickerStyle.Divider);
286                 downLine.PositionY = (int)pickerStyle.Divider.PositionY + itemHeight;
287             }
288         }
289                 
290         private void Initialize()
291         {
292             AccessibilityHighlightable = true;
293             HeightSpecification = LayoutParamPolicies.MatchParent;
294
295             //Picker Using scroller internally. actually it is a kind of scroller which has infinity loop,
296             //and item center align features.
297             pickerScroller = new PickerScroller(pickerStyle)
298             {
299                 Size = new Size(-1, pickerStyle.Size.Height),
300                 ScrollingDirection = ScrollableBase.Direction.Vertical,
301                 Layout = new LinearLayout()
302                 {
303                     LinearOrientation = LinearLayout.Orientation.Vertical,
304                 },
305                 //FIXME: Need to expand as many as possible;
306                 //       When user want to start list middle of the list item. currently confused how to create list before render.
307                 ScrollAvailableArea = new Vector2(0, 10000),
308                 Name = "pickerScroller",
309             };
310             pickerScroller.Scrolling += OnScroll;
311             pickerScroller.ScrollAnimationEnded += OnScrollAnimationEnded;
312             pickerScroller.ScrollAnimationStarted += OnScrollAnimationStarted;
313
314             itemList = new List<TextLabel>();
315             
316             minValue = maxValue = currentValue = 0;
317             displayedValues = null;
318             //Those many flags for min, max, value method calling sequence dependency.
319             needItemUpdate = true;
320             displayedValuesUpdate = false;
321             onAnimation = false;
322             loopEnabled = false;
323
324             startScrollOffset = (int)pickerStyle.StartScrollOffset.Height;
325             itemHeight = (int)pickerStyle.ItemTextLabel.Size.Height;
326             startScrollY = (itemHeight * dummyItemsForLoop) + startScrollOffset;
327             startY = startScrollOffset;
328
329             Add(pickerScroller);
330             AddLine();
331         }
332
333         private void OnValueChanged()
334         { 
335             ValueChangedEventArgs eventArgs =
336                 new ValueChangedEventArgs(displayedValuesUpdate ? Int32.Parse(itemList[currentValue].Name) : Int32.Parse(itemList[currentValue].Text));
337             ValueChanged?.Invoke(this, eventArgs);
338         }
339
340         private void PageAdjust(float positionY)
341         {
342             //Check the scroll is going out to the dummys if so, bring it back to page.
343             if (positionY > -(startScrollY - (itemHeight * 2)))
344                 pickerScroller.ScrollTo(-positionY + pageSize, false);
345             else if (positionY < -(startScrollY + pageSize - (itemHeight * 2)))
346                 pickerScroller.ScrollTo(-positionY - pageSize, false);
347         }
348
349         private void OnScroll(object sender, ScrollEventArgs e)
350         {
351             if (!loopEnabled || onAnimation || onAlignAnimation) return;
352             
353             PageAdjust(e.Position.Y);
354         }
355
356         private void OnScrollAnimationStarted(object sender, ScrollEventArgs e)
357         {
358             onAnimation = true;
359         }
360
361         private void OnScrollAnimationEnded(object sender, ScrollEventArgs e)
362         {
363             //Ignore if the scroll position was not changed. (called it from this function)
364             if (lastScrollPosion == (int)e.Position.Y) return;
365
366             //Calc offset from closest item.
367             int offset = (int)(e.Position.Y + startScrollOffset) % itemHeight;
368             if (offset < -(itemHeight / 2)) offset += itemHeight;
369
370             lastScrollPosion = (int)(-e.Position.Y + offset);
371
372             onAnimation = false;
373             if (onAlignAnimation) {
374                 onAlignAnimation = false;
375                 if (loopEnabled == true)
376                 {
377                     PageAdjust(e.Position.Y);
378                 }
379                 if (currentValue != ((int)(-e.Position.Y / itemHeight) + 2))
380                 {
381                     currentValue = ((int)(-e.Position.Y / itemHeight) + 2);
382                     OnValueChanged();
383                 }
384
385                 return;
386             }
387
388             //Item center align with animation, otherwise changed event emit.
389             if (offset != 0) {
390                 onAlignAnimation = true;
391                 pickerScroller.ScrollTo(-e.Position.Y + offset, true);
392             }
393             else {
394                 if (currentValue != ((int)(-e.Position.Y / itemHeight) + 2))
395                 {
396                     currentValue = ((int)(-e.Position.Y / itemHeight) + 2);
397                     OnValueChanged();
398                 }
399             }
400         }
401
402         //This is UI requirement. It helps where exactly center item is.
403         private void AddLine()
404         {
405             upLine = new View(pickerStyle.Divider);
406             downLine = new View(pickerStyle.Divider)
407             {
408                 Position = new Position(0, (int)pickerStyle.Divider.PositionY + itemHeight),
409             };
410
411             Add(upLine);
412             Add(downLine);
413         }
414
415         private String GetItemText(bool loopEnabled, int idx)
416         {
417             if (!loopEnabled) return " ";
418             else {
419                 if (displayedValuesUpdate) {
420                     idx = idx - MinValue;
421                     if (idx <= displayedValues.Count) {
422                         return displayedValues[idx];
423                     }
424                     return " ";
425                 }
426
427                 return idx.ToString();
428             }
429         }
430
431         //FIXME: If textVisual can add in scroller please change it to textVisual for performance
432         [SuppressMessage("Microsoft.Reliability",
433                          "CA2000:DisposeObjectsBeforeLosingScope",
434                          Justification = "The items are added to itemList and are disposed in Picker.Dispose().")]
435         private void AddPickerItem(bool loopEnabled, int idx)
436         {
437             TextLabel temp = new TextLabel(pickerStyle.ItemTextLabel)
438             {
439                 WidthSpecification = LayoutParamPolicies.MatchParent,
440                 Text = GetItemText(loopEnabled, idx),
441                 Name = idx.ToString(),
442             };
443
444             itemList.Add(temp);
445             pickerScroller.Add(temp);
446         }
447
448         private void UpdateCurrentValue()
449         {
450             // -2 for center align
451             int startItemIdx = (currentValue == 0) ? -2 : currentValue - minValue - 2;
452
453             if (loopEnabled) startY = ((dummyItemsForLoop + startItemIdx) * itemHeight) + startScrollOffset;
454             // + 2 for non loop picker center align
455             else startY = ((2 + startItemIdx) * itemHeight) + startScrollOffset;
456             pickerScroller.ScrollTo(startY, false);
457         }
458
459         private void UpdateValueList()
460         {
461             if (!needItemUpdate) return;
462             if (minValue > maxValue) return;
463
464             //FIXME: This is wrong.
465             //       But scroller can't update item property after added please fix me.
466             if (itemList.Count > 0) {
467                 itemList.Clear();
468                 pickerScroller.RemoveAllChildren();
469             }
470
471             if (maxValue - minValue + 1 >= scrollVisibleItems)
472             {
473                 loopEnabled = true;
474                 //Current scroller can't add at specific index.
475                 //So need below calc.
476                 int dummyStartIdx = 0;
477                 if (maxValue - minValue >= dummyItemsForLoop)
478                   dummyStartIdx = maxValue - dummyItemsForLoop + 1;
479                 else
480                   dummyStartIdx = maxValue - (dummyItemsForLoop % (maxValue - minValue + 1)) + 1;
481
482                 //Start add items in scroller. first dummys for scroll anim.
483                 for (int i = 0; i < dummyItemsForLoop; i++)
484                 {
485                     if (dummyStartIdx > maxValue) dummyStartIdx = minValue;
486                     AddPickerItem(loopEnabled, dummyStartIdx++);
487                 }
488                 //Second real items.
489                 for (int i = minValue; i <= maxValue; i++)
490                 {
491                     AddPickerItem(loopEnabled, i);
492                 }
493                 //Last dummys for scroll anim.
494                 dummyStartIdx = minValue;
495                 for (int i = 0; i < dummyItemsForLoop; i++)
496                 {
497                     if (dummyStartIdx > maxValue) dummyStartIdx = minValue;
498                     AddPickerItem(loopEnabled, dummyStartIdx++);
499                 }
500             }
501             else
502             {
503                 loopEnabled = false;
504
505                 for (int i = 0; i < 2; i++)
506                     AddPickerItem(loopEnabled, 0);
507                 for (int i = minValue; i <= maxValue; i++)
508                     AddPickerItem(!loopEnabled, i);
509                 for (int i = 0; i < 2; i++)
510                     AddPickerItem(loopEnabled, 0);
511
512             }
513             pageSize = itemHeight * (maxValue - minValue + 1);
514
515             UpdateCurrentValue();
516
517             //Give a correct scroll area.
518             pickerScroller.ScrollAvailableArea = new Vector2(0, (itemList.Count * itemHeight) - pickerStyle.Size.Height);
519
520             needItemUpdate = false;
521         }
522
523         internal class PickerScroller : ScrollableBase
524         {
525             private int itemHeight;
526             private int startScrollOffset;
527             private float velocityOfLastPan = 0.0f;
528             private float panAnimationDuration = 0.0f;
529             private float panAnimationDelta = 0.0f;
530             private float decelerationRate = 0.0f;
531             private float logValueOfDeceleration = 0.0f;
532             private delegate float UserAlphaFunctionDelegate(float progress);
533             private UserAlphaFunctionDelegate customScrollAlphaFunction;
534
535             public PickerScroller(PickerStyle pickerStyle) : base()
536             {
537                 //Default rate is 0.998. this is for reduce scroll animation length.
538                 decelerationRate = 0.991f;
539                 startScrollOffset = (int)pickerStyle.StartScrollOffset.Height;
540                 itemHeight = (int)pickerStyle.ItemTextLabel.Size.Height;
541                 logValueOfDeceleration = (float)Math.Log(decelerationRate);
542             }
543
544             private float CustomScrollAlphaFunction(float progress)
545             {
546                 if (panAnimationDelta == 0)
547                 {
548                     return 1.0f;
549                 }
550                 else
551                 {
552                     // Parameter "progress" is normalized value. We need to multiply target duration to calculate distance.
553                     // Can get real distance using equation of deceleration (check Decelerating function)
554                     // After get real distance, normalize it
555                     float realDuration = progress * panAnimationDuration;
556                     float realDistance = velocityOfLastPan * ((float)Math.Pow(decelerationRate, realDuration) - 1) / logValueOfDeceleration;
557                     float result = Math.Min(realDistance / Math.Abs(panAnimationDelta), 1.0f);
558
559                     return result;
560                 }
561             }
562
563             //Override Decelerating for Picker feature.
564             protected override void Decelerating(float velocity, Animation animation)
565             {
566                 //Reduce Scroll animation speed.
567                 //The picker is to select items in the scroll area, it is not correct to animate
568                 //the scroll with very high speed.
569                 velocity *= 0.5f;
570                 velocityOfLastPan = Math.Abs(velocity);
571
572                 float currentScrollPosition = -ContentContainer.PositionY;
573                 panAnimationDelta = (velocityOfLastPan * decelerationRate) / (1 - decelerationRate);
574                 panAnimationDelta = velocity > 0 ? -panAnimationDelta : panAnimationDelta;
575
576                 float destination = -(panAnimationDelta + currentScrollPosition);
577                 //Animation destination has to center of the item.
578                 float align = destination % itemHeight;
579                 destination -= align;
580                 destination -= startScrollOffset;
581
582                 float adjustDestination = AdjustTargetPositionOfScrollAnimation(destination);
583
584                 float maxPosition = ScrollAvailableArea != null ? ScrollAvailableArea.Y : 0;
585                 float minPosition = ScrollAvailableArea != null ? ScrollAvailableArea.X : 0;
586
587                 if (destination < -maxPosition || destination > minPosition)
588                 {
589                     panAnimationDelta = velocity > 0 ? (currentScrollPosition - minPosition) : (maxPosition - currentScrollPosition);
590                     destination = velocity > 0 ? minPosition : -maxPosition;
591                     destination = -maxPosition + itemHeight;
592
593                     if (panAnimationDelta == 0)
594                     {
595                         panAnimationDuration = 0.0f;
596                     }
597                     else
598                     {
599                         panAnimationDuration = (float)Math.Log((panAnimationDelta * logValueOfDeceleration / velocityOfLastPan + 1), decelerationRate);
600                     }
601                 }
602                 else
603                 {
604                     panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
605
606                     if (adjustDestination != destination)
607                     {
608                         destination = adjustDestination;
609                         panAnimationDelta = destination + currentScrollPosition;
610                         velocityOfLastPan = Math.Abs(panAnimationDelta * logValueOfDeceleration / ((float)Math.Pow(decelerationRate, panAnimationDuration) - 1));
611                         panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
612                     }
613                 }
614
615                 customScrollAlphaFunction = new UserAlphaFunctionDelegate(CustomScrollAlphaFunction);
616                 animation.DefaultAlphaFunction = new AlphaFunction(customScrollAlphaFunction);
617                 animation.Duration = (int)panAnimationDuration;
618                 animation.AnimateTo(ContentContainer, "PositionY", (int)destination);
619                 animation.Play();
620             }
621         }
622     }
623 }