Merge remote-tracking branch 'origin/master' into tizen
[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     /// <since_tizen> 9 </since_tizen>
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         /// <since_tizen> 9 </since_tizen>
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     /// <since_tizen> 9 </since_tizen>
56     public partial 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 Vector2 size;
83         private TextLabelStyle itemTextLabel;
84
85         /// <summary>
86         /// Creates a new instance of Picker.
87         /// </summary>
88         /// <since_tizen> 9 </since_tizen>
89         public Picker()
90         {
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         /// <since_tizen> 9 </since_tizen>
98         public Picker(string style) : base(style)
99         {
100         }
101
102         /// <summary>
103         /// Creates a new instance of Picker.
104         /// </summary>
105         /// <param name="pickerStyle">Creates Picker by style customized by user.</param>
106         /// <since_tizen> 9 </since_tizen>
107         public Picker(PickerStyle pickerStyle) : base(pickerStyle)
108         {
109         }
110
111         /// <summary>
112         /// Dispose Picker and all children on it.
113         /// </summary>
114         /// <param name="type">Dispose type.</param>
115         [EditorBrowsable(EditorBrowsableState.Never)]
116         protected override void Dispose(DisposeTypes type)
117         {
118             if (disposed)
119             {
120                 return;
121             }
122
123             if (type == DisposeTypes.Explicit)
124             {
125                 if (itemList != null)
126                 {
127                     foreach (TextLabel textLabel in itemList)
128                     {
129                         if (pickerScroller) pickerScroller.Remove(textLabel);
130                         Utility.Dispose(textLabel);
131                     }
132
133                     itemList = null;
134                 }
135
136                 if (pickerScroller != null)
137                 {
138                     Remove(pickerScroller);
139                     Utility.Dispose(pickerScroller);
140                     pickerScroller = null;
141                 }
142
143                 Remove(upLine);
144                 Utility.Dispose(upLine);
145                 Remove(downLine);
146                 Utility.Dispose(downLine);
147             }
148
149             base.Dispose(type);
150         }
151
152         /// <summary>
153         /// An event emitted when Picker value changed, user can subscribe or unsubscribe to this event handler.
154         /// </summary>
155         /// <since_tizen> 9 </since_tizen>
156         public event EventHandler<ValueChangedEventArgs> ValueChanged;
157
158         //TODO Fomatter here
159
160         /// <summary>
161         /// The values to be displayed instead of numbers.
162         /// </summary>
163         /// <since_tizen> 9 </since_tizen>
164         public ReadOnlyCollection<String> DisplayedValues
165         {
166             get
167             {
168                 return displayedValues;
169             }
170             set
171             {
172                 displayedValues = value;
173
174                 needItemUpdate = true;
175                 displayedValuesUpdate = true;
176
177                 UpdateValueList();
178             }
179         }
180         
181         /// <summary>
182         /// The Current value of Picker.
183         /// </summary>
184         /// <since_tizen> 9 </since_tizen>
185         public int CurrentValue
186         {
187             get
188             {
189                 return (int)GetValue(CurrentValueProperty);
190             }
191             set
192             {
193                 SetValue(CurrentValueProperty, value);
194                 NotifyPropertyChanged();
195             }
196         }
197         private int InternalCurrentValue
198         {
199             get
200             {
201                 return currentValue;
202             }
203             set
204             {
205                 if (currentValue == value) return;
206
207                 if (currentValue < minValue) currentValue = minValue;
208                 else if (currentValue > maxValue) currentValue = maxValue;
209
210                 currentValue = value;
211
212                 UpdateCurrentValue();
213             }
214         }
215
216         /// <summary>
217         /// The max value of Picker.
218         /// </summary>
219         /// <since_tizen> 9 </since_tizen>
220         public int MaxValue
221         {
222             get
223             {
224                 return (int)GetValue(MaxValueProperty);
225             }
226             set
227             {
228                 SetValue(MaxValueProperty, value);
229                 NotifyPropertyChanged();
230             }
231         }
232         private int InternalMaxValue
233         {
234             get
235             {
236                 return maxValue;
237             }
238             set
239             {
240                 if (maxValue == value) return;
241                 if (currentValue > value) currentValue = value;
242                 
243                 maxValue = value;
244                 needItemUpdate = true;
245
246                 UpdateValueList();
247             }
248         }
249
250         /// <summary>
251         /// The min value of Picker.
252         /// </summary>
253         /// <since_tizen> 9 </since_tizen>
254         public int MinValue
255         {
256             get
257             {
258                 return (int)GetValue(MinValueProperty);
259             }
260             set
261             {
262                 SetValue(MinValueProperty, value);
263                 NotifyPropertyChanged();
264             }
265         }
266         private int InternalMinValue
267         {
268             get
269             {
270                 return minValue;
271             }
272             set
273             {
274                 if (minValue == value) return;
275                 if (currentValue < value) currentValue = value;
276                 
277                 minValue = value;
278                 needItemUpdate = true;
279
280                 UpdateValueList();
281             }
282         }
283
284         /// <inheritdoc/>
285         [EditorBrowsable(EditorBrowsableState.Never)]
286         public override void OnInitialize()
287         {
288             base.OnInitialize();
289             SetAccessibilityConstructor(Role.List);
290
291             Initialize();
292         }
293
294         /// <summary>
295         /// Applies style to Picker.
296         /// </summary>
297         /// <param name="viewStyle">The style to apply.</param>
298         [EditorBrowsable(EditorBrowsableState.Never)]
299         public override void ApplyStyle(ViewStyle viewStyle)
300         {
301             base.ApplyStyle(viewStyle);
302
303             var pickerStyle = viewStyle as PickerStyle;
304
305             if (pickerStyle == null) return;
306
307             pickerScroller?.SetPickerStyle(pickerStyle);
308
309             //Apply StartScrollOffset style.
310             if (pickerStyle.StartScrollOffset != null)
311             {
312                 startScrollOffset = (int)pickerStyle.StartScrollOffset.Height;
313             }
314
315             //Apply ItemTextLabel style.
316             if (pickerStyle.ItemTextLabel != null)
317             {
318                 if (itemTextLabel == null)
319                 {
320                     itemTextLabel = (TextLabelStyle)pickerStyle.ItemTextLabel.Clone();
321                 }
322                 else
323                 {
324                     itemTextLabel.MergeDirectly(pickerStyle.ItemTextLabel);
325                 }
326
327                 itemHeight = (int)(pickerStyle.ItemTextLabel.Size?.Height ?? 0);
328
329                 if (itemList != null)
330                     foreach (TextLabel textLabel in itemList)
331                         textLabel.ApplyStyle(pickerStyle.ItemTextLabel);
332             }
333
334             //Apply PickerCenterLine style.
335             if (pickerStyle.Divider != null && upLine != null && downLine != null)
336             {
337                 upLine.ApplyStyle(pickerStyle.Divider);
338                 downLine.ApplyStyle(pickerStyle.Divider);
339                 downLine.PositionY = (int)pickerStyle.Divider.PositionY + itemHeight;
340             }
341
342             startScrollY = (itemHeight * dummyItemsForLoop) + startScrollOffset;
343             startY = startScrollOffset;
344         }
345
346         /// <inheritdoc/>
347         [EditorBrowsable(EditorBrowsableState.Never)]
348         public override void OnRelayout(Vector2 size, RelayoutContainer container)
349         {
350             if (size == null) return;
351
352             if (size.Equals(this.size))
353             {
354                 return;
355             }
356
357             this.size = new Vector2(size);
358
359             if (pickerScroller != null && itemList != null)
360             {
361                 pickerScroller.ScrollAvailableArea = new Vector2(0, (itemList.Count * itemHeight) - size.Height);
362             }
363         }
364                 
365         private void Initialize()
366         {
367             HeightSpecification = LayoutParamPolicies.MatchParent;
368
369             //Picker Using scroller internally. actually it is a kind of scroller which has infinity loop,
370             //and item center align features.
371             pickerScroller = new PickerScroller()
372             {
373                 WidthSpecification = LayoutParamPolicies.MatchParent,
374                 HeightSpecification = LayoutParamPolicies.MatchParent,
375                 ScrollingDirection = ScrollableBase.Direction.Vertical,
376                 Layout = new LinearLayout()
377                 {
378                     LinearOrientation = LinearLayout.Orientation.Vertical,
379                 },
380                 //FIXME: Need to expand as many as possible;
381                 //       When user want to start list middle of the list item. currently confused how to create list before render.
382                 ScrollAvailableArea = new Vector2(0, 10000),
383                 Name = "pickerScroller",
384             };
385
386             pickerScroller.Scrolling += OnScroll;
387             pickerScroller.ScrollAnimationEnded += OnScrollAnimationEnded;
388             pickerScroller.ScrollAnimationStarted += OnScrollAnimationStarted;
389
390             itemList = new List<TextLabel>();
391             
392             minValue = maxValue = currentValue = 0;
393             displayedValues = null;
394             //Those many flags for min, max, value method calling sequence dependency.
395             needItemUpdate = true;
396             displayedValuesUpdate = false;
397             onAnimation = false;
398             loopEnabled = false;
399
400             Add(pickerScroller);
401             AddLine();
402         }
403
404         private void OnValueChanged()
405         { 
406             ValueChangedEventArgs eventArgs =
407                 new ValueChangedEventArgs(displayedValuesUpdate ? Int32.Parse(itemList[currentValue].Name) : Int32.Parse(itemList[currentValue].Text));
408             ValueChanged?.Invoke(this, eventArgs);
409         }
410
411         private void PageAdjust(float positionY)
412         {
413             //Check the scroll is going out to the dummys if so, bring it back to page.
414             if (positionY > -(startScrollY - (itemHeight * 2)))
415                 pickerScroller.ScrollTo(-positionY + pageSize, false);
416             else if (positionY < -(startScrollY + pageSize - (itemHeight * 2)))
417                 pickerScroller.ScrollTo(-positionY - pageSize, false);
418         }
419
420         private void OnScroll(object sender, ScrollEventArgs e)
421         {
422             if (!loopEnabled || onAnimation || onAlignAnimation) return;
423             
424             PageAdjust(e.Position.Y);
425         }
426
427         private void OnScrollAnimationStarted(object sender, ScrollEventArgs e)
428         {
429             onAnimation = true;
430         }
431
432         private void OnScrollAnimationEnded(object sender, ScrollEventArgs e)
433         {
434             //Ignore if the scroll position was not changed. (called it from this function)
435             if (lastScrollPosion == (int)e.Position.Y) return;
436
437             //Calc offset from closest item.
438             int offset = (int)(e.Position.Y + startScrollOffset) % itemHeight;
439             if (offset < -(itemHeight / 2)) offset += itemHeight;
440
441             lastScrollPosion = (int)(-e.Position.Y + offset);
442
443             onAnimation = false;
444             if (onAlignAnimation) {
445                 onAlignAnimation = false;
446                 if (loopEnabled == true)
447                 {
448                     PageAdjust(e.Position.Y);
449                 }
450                 if (currentValue != ((int)(-e.Position.Y / itemHeight) + 2))
451                 {
452                     currentValue = ((int)(-e.Position.Y / itemHeight) + 2);
453                     OnValueChanged();
454                 }
455
456                 return;
457             }
458
459             //Item center align with animation, otherwise changed event emit.
460             if (offset != 0) {
461                 onAlignAnimation = true;
462                 pickerScroller.ScrollTo(-e.Position.Y + offset, true);
463             }
464             else {
465                 if (currentValue != ((int)(-e.Position.Y / itemHeight) + 2))
466                 {
467                     currentValue = ((int)(-e.Position.Y / itemHeight) + 2);
468                     OnValueChanged();
469                 }
470             }
471         }
472
473         //This is UI requirement. It helps where exactly center item is.
474         private void AddLine()
475         {
476             upLine = new View();
477             downLine = new View();
478
479             Add(upLine);
480             Add(downLine);
481         }
482
483         private String GetItemText(bool loopEnabled, int idx)
484         {
485             if (!loopEnabled) return " ";
486             else {
487                 if (displayedValuesUpdate) {
488                     idx = idx - MinValue;
489                     if (idx <= displayedValues.Count) {
490                         return displayedValues[idx];
491                     }
492                     return " ";
493                 }
494
495                 return idx.ToString();
496             }
497         }
498
499         //FIXME: If textVisual can add in scroller please change it to textVisual for performance
500         [SuppressMessage("Microsoft.Reliability",
501                          "CA2000:DisposeObjectsBeforeLosingScope",
502                          Justification = "The items are added to itemList and are disposed in Picker.Dispose().")]
503         private void AddPickerItem(bool loopEnabled, int idx)
504         {
505             TextLabel temp = new TextLabel(itemTextLabel)
506             {
507                 WidthSpecification = LayoutParamPolicies.MatchParent,
508                 Text = GetItemText(loopEnabled, idx),
509                 Name = idx.ToString(),
510             };
511
512             itemList.Add(temp);
513             pickerScroller.Add(temp);
514         }
515
516         private void UpdateCurrentValue()
517         {
518             // -2 for center align
519             int startItemIdx = (currentValue == 0) ? -2 : currentValue - minValue - 2;
520
521             if (loopEnabled) startY = ((dummyItemsForLoop + startItemIdx) * itemHeight) + startScrollOffset;
522             // + 2 for non loop picker center align
523             else
524             {
525                 startY = ((2 + startItemIdx) * itemHeight) + startScrollOffset;
526                 currentValue = currentValue - minValue + 2;
527             }
528             pickerScroller.ScrollTo(startY, false);
529         }
530
531         private void UpdateValueList()
532         {
533             if (!needItemUpdate) return;
534             if (minValue > maxValue) return;
535
536             //FIXME: This is wrong.
537             //       But scroller can't update item property after added please fix me.
538             if (itemList.Count > 0) {
539                 itemList.Clear();
540                 pickerScroller.RemoveAllChildren();
541             }
542
543             if (maxValue - minValue + 1 >= scrollVisibleItems)
544             {
545                 loopEnabled = true;
546                 //Current scroller can't add at specific index.
547                 //So need below calc.
548                 int dummyStartIdx = 0;
549                 if (maxValue - minValue >= dummyItemsForLoop)
550                   dummyStartIdx = maxValue - dummyItemsForLoop + 1;
551                 else
552                   dummyStartIdx = maxValue - (dummyItemsForLoop % (maxValue - minValue + 1)) + 1;
553
554                 //Start add items in scroller. first dummys for scroll anim.
555                 for (int i = 0; i < dummyItemsForLoop; i++)
556                 {
557                     if (dummyStartIdx > maxValue) dummyStartIdx = minValue;
558                     AddPickerItem(loopEnabled, dummyStartIdx++);
559                 }
560                 //Second real items.
561                 for (int i = minValue; i <= maxValue; i++)
562                 {
563                     AddPickerItem(loopEnabled, i);
564                 }
565                 //Last dummys for scroll anim.
566                 dummyStartIdx = minValue;
567                 for (int i = 0; i < dummyItemsForLoop; i++)
568                 {
569                     if (dummyStartIdx > maxValue) dummyStartIdx = minValue;
570                     AddPickerItem(loopEnabled, dummyStartIdx++);
571                 }
572             }
573             else
574             {
575                 loopEnabled = false;
576
577                 for (int i = 0; i < 2; i++)
578                     AddPickerItem(loopEnabled, 0);
579                 for (int i = minValue; i <= maxValue; i++)
580                     AddPickerItem(!loopEnabled, i);
581                 for (int i = 0; i < 2; i++)
582                     AddPickerItem(loopEnabled, 0);
583
584             }
585             pageSize = itemHeight * (maxValue - minValue + 1);
586
587             UpdateCurrentValue();
588
589             //Give a correct scroll area.
590             if (size != null)
591             {
592                 pickerScroller.ScrollAvailableArea = new Vector2(0, (itemList.Count * itemHeight) - size.Height);
593             }
594
595             needItemUpdate = false;
596         }
597
598         internal class PickerScroller : ScrollableBase
599         {
600             private int itemHeight;
601             private int startScrollOffset;
602             private float velocityOfLastPan = 0.0f;
603             private float panAnimationDuration = 0.0f;
604             private float panAnimationDelta = 0.0f;
605             private float decelerationRate = 0.0f;
606             private float logValueOfDeceleration = 0.0f;
607             private delegate float UserAlphaFunctionDelegate(float progress);
608             private UserAlphaFunctionDelegate customScrollAlphaFunction;
609
610             public PickerScroller() : base()
611             {
612                 //Default rate is 0.998. this is for reduce scroll animation length.
613                 decelerationRate = 0.991f;
614                 logValueOfDeceleration = (float)Math.Log(decelerationRate);
615             }
616
617             public void SetPickerStyle(PickerStyle pickerStyle)
618             {
619                 if (pickerStyle.StartScrollOffset != null)
620                 {
621                     startScrollOffset = (int)pickerStyle.StartScrollOffset.Height;
622                 }
623
624                 if (pickerStyle.ItemTextLabel?.Size != null)
625                 {
626                     itemHeight = (int)pickerStyle.ItemTextLabel.Size.Height;
627                 }
628
629                 if (pickerStyle.Size != null)
630                 {
631                     Size = new Size(-1, pickerStyle.Size.Height);
632                 }
633             }
634
635             private float CustomScrollAlphaFunction(float progress)
636             {
637                 if (panAnimationDelta == 0)
638                 {
639                     return 1.0f;
640                 }
641                 else
642                 {
643                     // Parameter "progress" is normalized value. We need to multiply target duration to calculate distance.
644                     // Can get real distance using equation of deceleration (check Decelerating function)
645                     // After get real distance, normalize it
646                     float realDuration = progress * panAnimationDuration;
647                     float realDistance = velocityOfLastPan * ((float)Math.Pow(decelerationRate, realDuration) - 1) / logValueOfDeceleration;
648                     float result = Math.Min(realDistance / Math.Abs(panAnimationDelta), 1.0f);
649
650                     return result;
651                 }
652             }
653
654             //Override Decelerating for Picker feature.
655             protected override void Decelerating(float velocity, Animation animation)
656             {
657                 //Reduce Scroll animation speed.
658                 //The picker is to select items in the scroll area, it is not correct to animate
659                 //the scroll with very high speed.
660                 velocity *= 0.5f;
661                 velocityOfLastPan = Math.Abs(velocity);
662
663                 float currentScrollPosition = -ContentContainer.PositionY;
664                 panAnimationDelta = (velocityOfLastPan * decelerationRate) / (1 - decelerationRate);
665                 panAnimationDelta = velocity > 0 ? -panAnimationDelta : panAnimationDelta;
666
667                 float destination = -(panAnimationDelta + currentScrollPosition);
668                 //Animation destination has to center of the item.
669                 float align = destination % itemHeight;
670                 destination -= align;
671                 destination -= startScrollOffset;
672
673                 float adjustDestination = AdjustTargetPositionOfScrollAnimation(destination);
674
675                 float maxPosition = ScrollAvailableArea != null ? ScrollAvailableArea.Y : 0;
676                 float minPosition = ScrollAvailableArea != null ? ScrollAvailableArea.X : 0;
677
678                 if (destination < -maxPosition || destination > minPosition)
679                 {
680                     panAnimationDelta = velocity > 0 ? (currentScrollPosition - minPosition) : (maxPosition - currentScrollPosition);
681                     destination = velocity > 0 ? minPosition : -maxPosition;
682                     destination = -maxPosition + itemHeight;
683
684                     if (panAnimationDelta == 0)
685                     {
686                         panAnimationDuration = 0.0f;
687                     }
688                     else
689                     {
690                         panAnimationDuration = (float)Math.Log((panAnimationDelta * logValueOfDeceleration / velocityOfLastPan + 1), decelerationRate);
691                     }
692                 }
693                 else
694                 {
695                     panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
696
697                     if (adjustDestination != destination)
698                     {
699                         destination = adjustDestination;
700                         panAnimationDelta = destination + currentScrollPosition;
701                         velocityOfLastPan = Math.Abs(panAnimationDelta * logValueOfDeceleration / ((float)Math.Pow(decelerationRate, panAnimationDuration) - 1));
702                         panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
703                     }
704                 }
705
706                 customScrollAlphaFunction = new UserAlphaFunctionDelegate(CustomScrollAlphaFunction);
707                 animation.DefaultAlphaFunction = new AlphaFunction(customScrollAlphaFunction);
708                 animation.Duration = (int)panAnimationDuration;
709                 animation.AnimateTo(ContentContainer, "PositionY", (int)destination);
710                 animation.Play();
711             }
712         }
713     }
714 }