b29f64e5a98a55a42adebd4e0e1f3566e7cacad9
[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 const int middleItemIdx = 2;
64         private int startScrollOffset;
65         private int itemHeight;
66         private int startScrollY;
67         private int startY;
68         private int pageSize;
69         private int currentValue;
70         private int maxValue;
71         private int minValue;
72         private int lastScrollPosion;
73         private int accessibilityHiddenStartIdx;
74         private bool onAnimation; //Scroller on animation check.
75         private bool onAlignAnimation;
76         private bool displayedValuesUpdate; //User sets displayed value check.
77         private bool needItemUpdate; //min, max or display value updated check.
78         private bool loopEnabled;
79         private bool isScreenReaderEnabled;
80         private bool isAtspiEnabled;
81         private ReadOnlyCollection<string> displayedValues;
82         private PickerScroller pickerScroller;
83         private View upLine;
84         private View downLine;
85         private IList<TextLabel> itemList;
86         private Vector2 size;
87         private TextLabelStyle itemTextLabel;
88         private bool editMode = false;
89         private View recoverIndicator = null;
90         private View editModeIndicator = null;
91
92
93         /// <summary>
94         /// Creates a new instance of Picker.
95         /// </summary>
96         /// <since_tizen> 9 </since_tizen>
97         public Picker()
98         {
99         }
100
101         /// <summary>
102         /// Creates a new instance of Picker.
103         /// </summary>
104         /// <param name="style">Creates Picker by special style defined in UX.</param>
105         /// <since_tizen> 9 </since_tizen>
106         public Picker(string style) : base(style)
107         {
108         }
109
110         /// <summary>
111         /// Creates a new instance of Picker.
112         /// </summary>
113         /// <param name="pickerStyle">Creates Picker by style customized by user.</param>
114         /// <since_tizen> 9 </since_tizen>
115         public Picker(PickerStyle pickerStyle) : base(pickerStyle)
116         {
117         }
118
119
120         /// <inheritdoc/>
121         [EditorBrowsable(EditorBrowsableState.Never)]
122         protected override void OnEnabled(bool enabled)
123         {
124             base.OnEnabled(enabled);
125
126             pickerScroller.IsEnabled = enabled;
127         }
128
129         /// <summary>
130         /// Dispose Picker and all children on it.
131         /// </summary>
132         /// <param name="type">Dispose type.</param>
133         [EditorBrowsable(EditorBrowsableState.Never)]
134         protected override void Dispose(DisposeTypes type)
135         {
136             if (disposed)
137             {
138                 return;
139             }
140
141             if (type == DisposeTypes.Explicit)
142             {
143                 if (itemList != null)
144                 {
145                     foreach (TextLabel textLabel in itemList)
146                     {
147                         if (pickerScroller) pickerScroller.Remove(textLabel);
148                         Utility.Dispose(textLabel);
149                     }
150
151                     itemList = null;
152                 }
153
154                 if (pickerScroller != null)
155                 {
156                     Remove(pickerScroller);
157                     Utility.Dispose(pickerScroller);
158                     pickerScroller = null;
159                 }
160
161                 Remove(upLine);
162                 Utility.Dispose(upLine);
163                 Remove(downLine);
164                 Utility.Dispose(downLine);
165
166                 recoverIndicator = null;
167                 if (editModeIndicator)
168                 {
169                     editModeIndicator.Dispose();
170                     editModeIndicator = null;
171                 }
172             }
173
174             base.Dispose(type);
175         }
176
177         /// <summary>
178         /// An event emitted when Picker value changed, user can subscribe or unsubscribe to this event handler.
179         /// </summary>
180         /// <since_tizen> 9 </since_tizen>
181         public event EventHandler<ValueChangedEventArgs> ValueChanged;
182
183         //TODO Fomatter here
184
185         /// <summary>
186         /// The values to be displayed instead of numbers.
187         /// </summary>
188         /// <since_tizen> 9 </since_tizen>
189         public ReadOnlyCollection<String> DisplayedValues
190         {
191             get
192             {
193                 return displayedValues;
194             }
195             set
196             {
197                 displayedValues = value;
198
199                 needItemUpdate = true;
200                 displayedValuesUpdate = true;
201
202                 UpdateValueList();
203             }
204         }
205
206         /// <summary>
207         /// The Current value of Picker.
208         /// </summary>
209         /// <since_tizen> 9 </since_tizen>
210         public int CurrentValue
211         {
212             get
213             {
214                 return (int)GetValue(CurrentValueProperty);
215             }
216             set
217             {
218                 SetValue(CurrentValueProperty, value);
219                 NotifyPropertyChanged();
220             }
221         }
222         private int InternalCurrentValue
223         {
224             get
225             {
226                 return currentValue;
227             }
228             set
229             {
230                 if (currentValue == value) return;
231
232                 if (currentValue < minValue) currentValue = minValue;
233                 else if (currentValue > maxValue) currentValue = maxValue;
234
235                 currentValue = value;
236
237                 UpdateCurrentValue();
238             }
239         }
240
241         /// <summary>
242         /// The max value of Picker.
243         /// </summary>
244         /// <since_tizen> 9 </since_tizen>
245         public int MaxValue
246         {
247             get
248             {
249                 return (int)GetValue(MaxValueProperty);
250             }
251             set
252             {
253                 SetValue(MaxValueProperty, value);
254                 NotifyPropertyChanged();
255             }
256         }
257         private int InternalMaxValue
258         {
259             get
260             {
261                 return maxValue;
262             }
263             set
264             {
265                 if (maxValue == value) return;
266                 if (currentValue > value) currentValue = value;
267
268                 maxValue = value;
269                 needItemUpdate = true;
270
271                 UpdateValueList();
272             }
273         }
274
275         /// <summary>
276         /// The min value of Picker.
277         /// </summary>
278         /// <since_tizen> 9 </since_tizen>
279         public int MinValue
280         {
281             get
282             {
283                 return (int)GetValue(MinValueProperty);
284             }
285             set
286             {
287                 SetValue(MinValueProperty, value);
288                 NotifyPropertyChanged();
289             }
290         }
291         private int InternalMinValue
292         {
293             get
294             {
295                 return minValue;
296             }
297             set
298             {
299                 if (minValue == value) return;
300                 if (currentValue < value) currentValue = value;
301
302                 minValue = value;
303                 needItemUpdate = true;
304
305                 UpdateValueList();
306             }
307         }
308
309         /// <inheritdoc/>
310         [EditorBrowsable(EditorBrowsableState.Never)]
311         public override void OnInitialize()
312         {
313             base.OnInitialize();
314             AccessibilityRole = Role.List;
315
316             Initialize();
317         }
318
319         /// <summary>
320         /// Applies style to Picker.
321         /// </summary>
322         /// <param name="viewStyle">The style to apply.</param>
323         [EditorBrowsable(EditorBrowsableState.Never)]
324         public override void ApplyStyle(ViewStyle viewStyle)
325         {
326             base.ApplyStyle(viewStyle);
327
328             var pickerStyle = viewStyle as PickerStyle;
329
330             if (pickerStyle == null) return;
331
332             pickerScroller?.SetPickerStyle(pickerStyle);
333
334             //Apply StartScrollOffset style.
335             if (pickerStyle.StartScrollOffset != null)
336             {
337                 startScrollOffset = (int)pickerStyle.StartScrollOffset.Height;
338             }
339
340             //Apply ItemTextLabel style.
341             if (pickerStyle.ItemTextLabel != null)
342             {
343                 if (itemTextLabel == null)
344                 {
345                     itemTextLabel = (TextLabelStyle)pickerStyle.ItemTextLabel.Clone();
346                 }
347                 else
348                 {
349                     itemTextLabel.MergeDirectly(pickerStyle.ItemTextLabel);
350                 }
351
352                 itemHeight = (int)(pickerStyle.ItemTextLabel.Size?.Height ?? 0);
353
354                 if (itemList != null)
355                     foreach (TextLabel textLabel in itemList)
356                         textLabel.ApplyStyle(pickerStyle.ItemTextLabel);
357             }
358
359             //Apply PickerCenterLine style.
360             if (pickerStyle.Divider != null && upLine != null && downLine != null)
361             {
362                 upLine.ApplyStyle(pickerStyle.Divider);
363                 downLine.ApplyStyle(pickerStyle.Divider);
364                 downLine.PositionY = (int)pickerStyle.Divider.PositionY + itemHeight;
365             }
366
367             startScrollY = (itemHeight * dummyItemsForLoop) + startScrollOffset;
368             startY = startScrollOffset;
369         }
370
371         /// <inheritdoc/>
372         [EditorBrowsable(EditorBrowsableState.Never)]
373         public override void OnRelayout(Vector2 size, RelayoutContainer container)
374         {
375             if (size == null) return;
376
377             if (size.Equals(this.size))
378             {
379                 return;
380             }
381
382             this.size = new Vector2(size);
383
384             if (pickerScroller != null && itemList != null)
385             {
386                 pickerScroller.ScrollAvailableArea = new Vector2(0, (itemList.Count * itemHeight) - size.Height);
387             }
388         }
389
390         private void AccessibilityEnabled()
391         {
392             if (loopEnabled) ShowItemsForAccessibility(currentValue - middleItemIdx);
393             else
394             {
395                 //Exception case handling condition state.
396                 //If user sets 4 items it can scroll but not loop.
397                 if (currentValue > (middleItemIdx * 2)) ShowItemsForAccessibility(middleItemIdx + 1);
398                 else ShowItemsForAccessibility(middleItemIdx);
399             }
400         }
401
402         private void Initialize()
403         {
404             HeightSpecification = LayoutParamPolicies.MatchParent;
405
406             //Picker Using scroller internally. actually it is a kind of scroller which has infinity loop,
407             //and item center align features.
408             pickerScroller = new PickerScroller()
409             {
410                 WidthSpecification = LayoutParamPolicies.MatchParent,
411                 HeightSpecification = LayoutParamPolicies.MatchParent,
412                 ScrollingDirection = ScrollableBase.Direction.Vertical,
413                 Layout = new LinearLayout()
414                 {
415                     LinearOrientation = LinearLayout.Orientation.Vertical,
416                 },
417                 //FIXME: Need to expand as many as possible;
418                 //       When user want to start list middle of the list item. currently confused how to create list before render.
419                 ScrollAvailableArea = new Vector2(0, 10000),
420                 Name = "pickerScroller",
421             };
422
423             pickerScroller.Scrolling += OnScroll;
424             pickerScroller.ScrollAnimationEnded += OnScrollAnimationEnded;
425             pickerScroller.ScrollAnimationStarted += OnScrollAnimationStarted;
426
427             itemList = new List<TextLabel>();
428
429             minValue = maxValue = currentValue = 0;
430             displayedValues = null;
431             //Those many flags for min, max, value method calling sequence dependency.
432             needItemUpdate = true;
433             displayedValuesUpdate = false;
434             onAnimation = false;
435             loopEnabled = false;
436
437             Add(pickerScroller);
438             AddLine();
439
440             Focusable = true;
441             KeyEvent += OnKeyEvent;
442
443             Accessibility.Accessibility.Enabled += (s, e) => {
444                 isAtspiEnabled = true;
445                 AccessibilityEnabled();
446             };
447             Accessibility.Accessibility.Disabled += (s, e) => {
448                 isAtspiEnabled = false;
449                 HideItemsForAccessibility();
450             };
451             Accessibility.Accessibility.ScreenReaderEnabled += (s, e) => {
452                 isScreenReaderEnabled = true;
453             };
454             Accessibility.Accessibility.ScreenReaderDisabled += (s, e) => {
455                 isScreenReaderEnabled = false;
456             };
457
458             isAtspiEnabled = Accessibility.Accessibility.IsEnabled;
459             isScreenReaderEnabled = Accessibility.Accessibility.IsScreenReaderEnabled;
460
461         }
462
463         private void HideItemsForAccessibility()
464         {
465             if (loopEnabled)
466                 for (int i = accessibilityHiddenStartIdx; i < (accessibilityHiddenStartIdx + scrollVisibleItems); i++)
467                     itemList[i].AccessibilityHidden = true;
468             else
469             {
470                 if (currentValue > (middleItemIdx * 2))
471                     for (int i = accessibilityHiddenStartIdx; i < (accessibilityHiddenStartIdx + (maxValue - minValue)); i++)
472                         itemList[i].AccessibilityHidden = true;
473                 else
474                     for (int i = accessibilityHiddenStartIdx; i < (accessibilityHiddenStartIdx + (maxValue - minValue + 1)); i++)
475                         itemList[i].AccessibilityHidden = true;
476             }
477         }
478
479         private void ShowItemsForAccessibility(int startIdx)
480         {
481             if (startIdx < 0) {
482                 Tizen.Log.Error("NUI", "ScreenReaderEnabled signal emitted before picker value initialize");
483                 return;
484             }
485
486             accessibilityHiddenStartIdx = startIdx;
487             if (loopEnabled)
488                 for (int i = accessibilityHiddenStartIdx; i < (accessibilityHiddenStartIdx + scrollVisibleItems); i++)
489                     itemList[i].AccessibilityHidden = false;
490             else
491             {
492                 if (currentValue > (middleItemIdx * 2))
493                     for (int i = accessibilityHiddenStartIdx; i < (accessibilityHiddenStartIdx + (maxValue - minValue)); i++)
494                         itemList[i].AccessibilityHidden = false;
495                 else
496                     for (int i = accessibilityHiddenStartIdx; i < (accessibilityHiddenStartIdx + (maxValue - minValue + 1)); i++)
497                         itemList[i].AccessibilityHidden = false;
498             }
499         }
500
501         private void OnValueChanged()
502         {
503             if (isAtspiEnabled)
504             {
505                 if (loopEnabled) {
506                     HideItemsForAccessibility();
507                     ShowItemsForAccessibility(currentValue - middleItemIdx);
508                 }
509                 if (isScreenReaderEnabled) itemList[currentValue].GrabAccessibilityHighlight();
510             }
511
512             ValueChangedEventArgs eventArgs =
513                 new ValueChangedEventArgs(displayedValuesUpdate ? Int32.Parse(itemList[currentValue].Name) : Int32.Parse(itemList[currentValue].Text));
514             ValueChanged?.Invoke(this, eventArgs);
515         }
516
517         private void PageAdjust(float positionY)
518         {
519             //Check the scroll is going out to the dummys if so, bring it back to page.
520             if (positionY > -(startScrollY - (itemHeight * middleItemIdx)))
521                 pickerScroller.ScrollTo(-positionY + pageSize, false);
522             else if (positionY < -(startScrollY + pageSize - (itemHeight * middleItemIdx)))
523                 pickerScroller.ScrollTo(-positionY - pageSize, false);
524         }
525
526         private void OnScroll(object sender, ScrollEventArgs e)
527         {
528             if (!loopEnabled || onAnimation || onAlignAnimation) return;
529
530             if (isAtspiEnabled) Accessibility.Accessibility.ClearCurrentlyHighlightedView();
531
532             PageAdjust(e.Position.Y);
533         }
534
535         private void OnScrollAnimationStarted(object sender, ScrollEventArgs e)
536         {
537             onAnimation = true;
538         }
539
540         private void OnScrollAnimationEnded(object sender, ScrollEventArgs e)
541         {
542             //Ignore if the scroll position was not changed. (called it from this function)
543             if (lastScrollPosion == (int)e.Position.Y) return;
544
545             //Calc offset from closest item.
546             int offset = (int)(e.Position.Y + startScrollOffset) % itemHeight;
547             if (offset < -(itemHeight / 2)) offset += itemHeight;
548
549             lastScrollPosion = (int)(-e.Position.Y + offset);
550
551             onAnimation = false;
552             if (onAlignAnimation)
553             {
554                 onAlignAnimation = false;
555                 if (loopEnabled == true)
556                 {
557                     PageAdjust(e.Position.Y);
558                 }
559                 if (currentValue != ((int)(-e.Position.Y / itemHeight) + middleItemIdx))
560                 {
561                     currentValue = ((int)(-e.Position.Y / itemHeight) + middleItemIdx);
562                     OnValueChanged();
563                 }
564
565                 return;
566             }
567
568             //Item center align with animation, otherwise changed event emit.
569             if (offset != 0)
570             {
571                 onAlignAnimation = true;
572                 pickerScroller.ScrollTo(-e.Position.Y + offset, true);
573             }
574             else
575             {
576                 if (currentValue != ((int)(-e.Position.Y / itemHeight) + middleItemIdx))
577                 {
578                     currentValue = ((int)(-e.Position.Y / itemHeight) + middleItemIdx);
579                     OnValueChanged();
580                 }
581             }
582         }
583
584         //This is UI requirement. It helps where exactly center item is.
585         private void AddLine()
586         {
587             upLine = new View();
588             downLine = new View();
589
590             Add(upLine);
591             Add(downLine);
592         }
593
594         private String GetItemText(bool loopEnabled, int idx)
595         {
596             if (!loopEnabled) return " ";
597             else
598             {
599                 if (displayedValuesUpdate)
600                 {
601                     idx = idx - MinValue;
602                     if (idx <= displayedValues.Count)
603                     {
604                         return displayedValues[idx];
605                     }
606                     return " ";
607                 }
608
609                 return idx.ToString();
610             }
611         }
612
613         //FIXME: If textVisual can add in scroller please change it to textVisual for performance
614         [SuppressMessage("Microsoft.Reliability",
615                          "CA2000:DisposeObjectsBeforeLosingScope",
616                          Justification = "The items are added to itemList and are disposed in Picker.Dispose().")]
617         private void AddPickerItem(bool loopEnabled, int idx)
618         {
619             TextLabel temp = new TextLabel(itemTextLabel)
620             {
621                 WidthSpecification = LayoutParamPolicies.MatchParent,
622                 Text = GetItemText(loopEnabled, idx),
623                 Name = idx.ToString(),
624             };
625
626             temp.AccessibilitySuppressedEvents[AccessibilityEvent.MovedOut] = true;
627             // Hide on Accessibility tree
628             if (isAtspiEnabled) temp.AccessibilityHidden = true;
629             itemList.Add(temp);
630             pickerScroller.Add(temp);
631         }
632
633         private void UpdateCurrentValue()
634         {
635             // -2 for center align
636             int startItemIdx = (currentValue == 0) ? -middleItemIdx : currentValue - minValue - middleItemIdx;
637
638             if (loopEnabled)
639             {
640                 startY = ((dummyItemsForLoop + startItemIdx) * itemHeight) + startScrollOffset;
641
642                 if (isAtspiEnabled)
643                 {
644                     HideItemsForAccessibility();
645                     ShowItemsForAccessibility(dummyItemsForLoop + startItemIdx);
646                 }
647             }
648             // + 2 for non loop picker center align
649             else
650             {
651                 startY = ((middleItemIdx + startItemIdx) * itemHeight) + startScrollOffset;
652                 currentValue = currentValue - minValue + middleItemIdx;
653
654                 if (isAtspiEnabled)
655                 {
656                     HideItemsForAccessibility();
657                     AccessibilityEnabled();
658                 }
659             }
660             pickerScroller.ScrollTo(startY, false);
661         }
662
663         private void UpdateValueList()
664         {
665             if (!needItemUpdate) return;
666             if (minValue > maxValue) return;
667
668             //FIXME: This is wrong.
669             //       But scroller can't update item property after added please fix me.
670             if (itemList.Count > 0)
671             {
672                 itemList.Clear();
673                 pickerScroller.RemoveAllChildren();
674             }
675
676             if (maxValue - minValue + 1 >= scrollVisibleItems)
677             {
678                 loopEnabled = true;
679                 //Current scroller can't add at specific index.
680                 //So need below calc.
681                 int dummyStartIdx = 0;
682                 if (maxValue - minValue >= dummyItemsForLoop)
683                     dummyStartIdx = maxValue - dummyItemsForLoop + 1;
684                 else
685                     dummyStartIdx = maxValue - (dummyItemsForLoop % (maxValue - minValue + 1)) + 1;
686
687                 //Start add items in scroller. first dummys for scroll anim.
688                 for (int i = 0; i < dummyItemsForLoop; i++)
689                 {
690                     if (dummyStartIdx > maxValue) dummyStartIdx = minValue;
691                     AddPickerItem(loopEnabled, dummyStartIdx++);
692                 }
693                 //Second real items.
694                 for (int i = minValue; i <= maxValue; i++)
695                 {
696                     AddPickerItem(loopEnabled, i);
697                 }
698                 //Last dummys for scroll anim.
699                 dummyStartIdx = minValue;
700                 for (int i = 0; i < dummyItemsForLoop; i++)
701                 {
702                     if (dummyStartIdx > maxValue) dummyStartIdx = minValue;
703                     AddPickerItem(loopEnabled, dummyStartIdx++);
704                 }
705             }
706             else
707             {
708                 loopEnabled = false;
709
710                 for (int i = 0; i < middleItemIdx; i++)
711                     AddPickerItem(loopEnabled, 0);
712                 for (int i = minValue; i <= maxValue; i++)
713                     AddPickerItem(!loopEnabled, i);
714                 for (int i = 0; i < middleItemIdx; i++)
715                     AddPickerItem(loopEnabled, 0);
716
717             }
718             pageSize = itemHeight * (maxValue - minValue + 1);
719
720             UpdateCurrentValue();
721
722             //Give a correct scroll area.
723             if (size != null)
724             {
725                 pickerScroller.ScrollAvailableArea = new Vector2(0, (itemList.Count * itemHeight) - size.Height);
726             }
727
728             needItemUpdate = false;
729         }
730
731         private bool OnKeyEvent(object o, View.KeyEventArgs e)
732         {
733             if (e.Key.State == Key.StateType.Down)
734             {
735                 if (e.Key.KeyPressedName == "Return")
736                 {
737                     if (editMode)
738                     {
739                         //Todo: sometimes this gets wrong. the currentValue is not correct. need to be fixed.
740                         if (currentValue != ((int)(-pickerScroller.Position.Y / itemHeight) + middleItemIdx))
741                         {
742                             currentValue = ((int)(-pickerScroller.Position.Y / itemHeight) + middleItemIdx);
743                             OnValueChanged();
744                         }
745
746                         //set editMode false (toggle the mode)
747                         editMode = false;
748                         FocusManager.Instance.FocusIndicator = recoverIndicator;
749                         return true;
750                     }
751                     else
752                     {
753                         //set editMode true (toggle the mode)
754                         editMode = true;
755                         if (editModeIndicator == null)
756                         {
757                             editModeIndicator = new View()
758                             {
759                                 PositionUsesPivotPoint = true,
760                                 PivotPoint = new Position(0, 0, 0),
761                                 WidthResizePolicy = ResizePolicyType.FillToParent,
762                                 HeightResizePolicy = ResizePolicyType.FillToParent,
763                                 BorderlineColor = Color.Red,
764                                 BorderlineWidth = 6.0f,
765                                 BorderlineOffset = -1f,
766                                 BackgroundColor = new Color(0.2f, 0.2f, 0.2f, 0.4f),
767                             };
768                         }
769                         recoverIndicator = FocusManager.Instance.FocusIndicator;
770                         FocusManager.Instance.FocusIndicator = editModeIndicator;
771                         return true;
772                     }
773                 }
774                 else if (e.Key.KeyPressedName == "Up")
775                 {
776                     if (editMode)
777                     {
778                         InternalCurrentValue += 1;
779                         return true;
780                     }
781                 }
782                 else if (e.Key.KeyPressedName == "Down")
783                 {
784                     if (editMode)
785                     {
786                         InternalCurrentValue -= 1;
787                         return true;
788                     }
789                 }
790
791                 if (editMode)
792                 {
793                     return true;
794                 }
795             }
796             return false;
797         }
798
799
800         internal class PickerScroller : ScrollableBase
801         {
802             private int itemHeight;
803             private int startScrollOffset;
804             private float velocityOfLastPan = 0.0f;
805             private float panAnimationDuration = 0.0f;
806             private float panAnimationDelta = 0.0f;
807             private float decelerationRate = 0.0f;
808             private float logValueOfDeceleration = 0.0f;
809             private delegate float UserAlphaFunctionDelegate(float progress);
810             private UserAlphaFunctionDelegate customScrollAlphaFunction;
811
812             public PickerScroller() : base()
813             {
814                 //Default rate is 0.998. this is for reduce scroll animation length.
815                 decelerationRate = 0.991f;
816                 logValueOfDeceleration = (float)Math.Log(decelerationRate);
817             }
818
819             public void SetPickerStyle(PickerStyle pickerStyle)
820             {
821                 if (pickerStyle.StartScrollOffset != null)
822                 {
823                     startScrollOffset = (int)pickerStyle.StartScrollOffset.Height;
824                 }
825
826                 if (pickerStyle.ItemTextLabel?.Size != null)
827                 {
828                     itemHeight = (int)pickerStyle.ItemTextLabel.Size.Height;
829                 }
830
831                 if (pickerStyle.Size != null)
832                 {
833                     Size = new Size(-1, pickerStyle.Size.Height);
834                 }
835             }
836
837             private float CustomScrollAlphaFunction(float progress)
838             {
839                 if (panAnimationDelta == 0)
840                 {
841                     return 1.0f;
842                 }
843                 else
844                 {
845                     // Parameter "progress" is normalized value. We need to multiply target duration to calculate distance.
846                     // Can get real distance using equation of deceleration (check Decelerating function)
847                     // After get real distance, normalize it
848                     float realDuration = progress * panAnimationDuration;
849                     float realDistance = velocityOfLastPan * ((float)Math.Pow(decelerationRate, realDuration) - 1) / logValueOfDeceleration;
850                     float result = Math.Min(realDistance / Math.Abs(panAnimationDelta), 1.0f);
851
852                     return result;
853                 }
854             }
855
856             //Override Decelerating for Picker feature.
857             protected override void Decelerating(float velocity, Animation animation)
858             {
859                 //Reduce Scroll animation speed.
860                 //The picker is to select items in the scroll area, it is not correct to animate
861                 //the scroll with very high speed.
862                 velocity *= 0.5f;
863                 velocityOfLastPan = Math.Abs(velocity);
864
865                 float currentScrollPosition = -ContentContainer.PositionY;
866                 panAnimationDelta = (velocityOfLastPan * decelerationRate) / (1 - decelerationRate);
867                 panAnimationDelta = velocity > 0 ? -panAnimationDelta : panAnimationDelta;
868
869                 float destination = -(panAnimationDelta + currentScrollPosition);
870                 //Animation destination has to center of the item.
871                 float align = destination % itemHeight;
872                 destination -= align;
873                 destination -= startScrollOffset;
874
875                 float adjustDestination = AdjustTargetPositionOfScrollAnimation(destination);
876
877                 float maxPosition = ScrollAvailableArea != null ? ScrollAvailableArea.Y : 0;
878                 float minPosition = ScrollAvailableArea != null ? ScrollAvailableArea.X : 0;
879
880                 if (destination < -maxPosition || destination > minPosition)
881                 {
882                     panAnimationDelta = velocity > 0 ? (currentScrollPosition - minPosition) : (maxPosition - currentScrollPosition);
883                     destination = velocity > 0 ? minPosition : -maxPosition;
884                     destination = -maxPosition + itemHeight;
885
886                     if (panAnimationDelta == 0)
887                     {
888                         panAnimationDuration = 0.0f;
889                     }
890                     else
891                     {
892                         panAnimationDuration = (float)Math.Log((panAnimationDelta * logValueOfDeceleration / velocityOfLastPan + 1), decelerationRate);
893                     }
894                 }
895                 else
896                 {
897                     panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
898
899                     if (adjustDestination != destination)
900                     {
901                         destination = adjustDestination;
902                         panAnimationDelta = destination + currentScrollPosition;
903                         velocityOfLastPan = Math.Abs(panAnimationDelta * logValueOfDeceleration / ((float)Math.Pow(decelerationRate, panAnimationDuration) - 1));
904                         panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
905                     }
906                 }
907
908                 customScrollAlphaFunction = new UserAlphaFunctionDelegate(CustomScrollAlphaFunction);
909                 animation.DefaultAlphaFunction = new AlphaFunction(customScrollAlphaFunction);
910                 animation.Duration = (int)panAnimationDuration;
911                 animation.AnimateTo(ContentContainer, "PositionY", (int)destination);
912                 animation.Play();
913             }
914         }
915     }
916 }