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