1 /* Copyright (c) 2021 Samsung Electronics Co., Ltd.
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
7 * http://www.apache.org/licenses/LICENSE-2.0
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.
18 using Tizen.NUI.BaseComponents;
19 using System.Collections.Generic;
20 using System.Collections.ObjectModel;
21 using System.ComponentModel;
22 using System.Diagnostics.CodeAnalysis;
24 namespace Tizen.NUI.Components
27 /// ValueChangedEventArgs is a class to notify changed Picker value argument which will sent to user.
29 /// <since_tizen> 9 </since_tizen>
30 public class ValueChangedEventArgs : EventArgs
33 /// ValueChangedEventArgs default constructor.
34 /// <param name="value">value of Picker.</param>
36 [EditorBrowsable(EditorBrowsableState.Never)]
37 public ValueChangedEventArgs(int value)
43 /// ValueChangedEventArgs default constructor.
44 /// <returns>The current value of Picker.</returns>
46 /// <since_tizen> 9 </since_tizen>
47 public int Value { get; }
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.
55 /// <since_tizen> 9 </since_tizen>
56 public partial class Picker : Control
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;
69 private int currentValue;
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;
84 private View downLine;
85 private IList<TextLabel> itemList;
87 private TextLabelStyle itemTextLabel;
88 private bool editMode = false;
89 private View recoverIndicator = null;
90 private View editModeIndicator = null;
94 /// Creates a new instance of Picker.
96 /// <since_tizen> 9 </since_tizen>
102 /// Creates a new instance of Picker.
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)
111 /// Creates a new instance of Picker.
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)
121 [EditorBrowsable(EditorBrowsableState.Never)]
122 protected override void OnEnabled(bool enabled)
124 base.OnEnabled(enabled);
126 pickerScroller.IsEnabled = enabled;
130 /// Dispose Picker and all children on it.
132 /// <param name="type">Dispose type.</param>
133 [EditorBrowsable(EditorBrowsableState.Never)]
134 protected override void Dispose(DisposeTypes type)
141 if (type == DisposeTypes.Explicit)
143 if (itemList != null)
145 foreach (TextLabel textLabel in itemList)
147 if (pickerScroller) pickerScroller.Remove(textLabel);
148 Utility.Dispose(textLabel);
154 if (pickerScroller != null)
156 Remove(pickerScroller);
157 Utility.Dispose(pickerScroller);
158 pickerScroller = null;
162 Utility.Dispose(upLine);
164 Utility.Dispose(downLine);
166 recoverIndicator = null;
167 if (editModeIndicator)
169 editModeIndicator.Dispose();
170 editModeIndicator = null;
178 /// An event emitted when Picker value changed, user can subscribe or unsubscribe to this event handler.
180 /// <since_tizen> 9 </since_tizen>
181 public event EventHandler<ValueChangedEventArgs> ValueChanged;
186 /// The values to be displayed instead of numbers.
188 /// <since_tizen> 9 </since_tizen>
189 public ReadOnlyCollection<String> DisplayedValues
193 return displayedValues;
197 displayedValues = value;
199 needItemUpdate = true;
200 displayedValuesUpdate = true;
207 /// The Current value of Picker.
209 /// <since_tizen> 9 </since_tizen>
210 public int CurrentValue
214 return (int)GetValue(CurrentValueProperty);
218 SetValue(CurrentValueProperty, value);
219 NotifyPropertyChanged();
222 private int InternalCurrentValue
230 if (currentValue == value) return;
232 if (currentValue < minValue) currentValue = minValue;
233 else if (currentValue > maxValue) currentValue = maxValue;
235 currentValue = value;
237 UpdateCurrentValue();
242 /// The max value of Picker.
244 /// <since_tizen> 9 </since_tizen>
249 return (int)GetValue(MaxValueProperty);
253 SetValue(MaxValueProperty, value);
254 NotifyPropertyChanged();
257 private int InternalMaxValue
265 if (maxValue == value) return;
266 if (currentValue > value) currentValue = value;
269 needItemUpdate = true;
276 /// The min value of Picker.
278 /// <since_tizen> 9 </since_tizen>
283 return (int)GetValue(MinValueProperty);
287 SetValue(MinValueProperty, value);
288 NotifyPropertyChanged();
291 private int InternalMinValue
299 if (minValue == value) return;
300 if (currentValue < value) currentValue = value;
303 needItemUpdate = true;
310 [EditorBrowsable(EditorBrowsableState.Never)]
311 public override void OnInitialize()
314 AccessibilityRole = Role.List;
320 /// Applies style to Picker.
322 /// <param name="viewStyle">The style to apply.</param>
323 [EditorBrowsable(EditorBrowsableState.Never)]
324 public override void ApplyStyle(ViewStyle viewStyle)
326 base.ApplyStyle(viewStyle);
328 var pickerStyle = viewStyle as PickerStyle;
330 if (pickerStyle == null) return;
332 pickerScroller?.SetPickerStyle(pickerStyle);
334 //Apply StartScrollOffset style.
335 if (pickerStyle.StartScrollOffset != null)
337 startScrollOffset = (int)pickerStyle.StartScrollOffset.Height;
340 //Apply ItemTextLabel style.
341 if (pickerStyle.ItemTextLabel != null)
343 if (itemTextLabel == null)
345 itemTextLabel = (TextLabelStyle)pickerStyle.ItemTextLabel.Clone();
349 itemTextLabel.MergeDirectly(pickerStyle.ItemTextLabel);
352 itemHeight = (int)(pickerStyle.ItemTextLabel.Size?.Height ?? 0);
354 if (itemList != null)
355 foreach (TextLabel textLabel in itemList)
356 textLabel.ApplyStyle(pickerStyle.ItemTextLabel);
359 //Apply PickerCenterLine style.
360 if (pickerStyle.Divider != null && upLine != null && downLine != null)
362 upLine.ApplyStyle(pickerStyle.Divider);
363 downLine.ApplyStyle(pickerStyle.Divider);
364 downLine.PositionY = (int)pickerStyle.Divider.PositionY + itemHeight;
367 startScrollY = (itemHeight * dummyItemsForLoop) + startScrollOffset;
368 startY = startScrollOffset;
372 [EditorBrowsable(EditorBrowsableState.Never)]
373 public override void OnRelayout(Vector2 size, RelayoutContainer container)
375 if (size == null) return;
377 if (size.Equals(this.size))
382 this.size = new Vector2(size);
384 if (pickerScroller != null && itemList != null)
386 pickerScroller.ScrollAvailableArea = new Vector2(0, (itemList.Count * itemHeight) - size.Height);
390 private void AccessibilityEnabled()
392 if (loopEnabled) ShowItemsForAccessibility(currentValue - middleItemIdx);
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);
402 private void Initialize()
404 HeightSpecification = LayoutParamPolicies.MatchParent;
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()
410 WidthSpecification = LayoutParamPolicies.MatchParent,
411 HeightSpecification = LayoutParamPolicies.MatchParent,
412 ScrollingDirection = ScrollableBase.Direction.Vertical,
413 Layout = new LinearLayout()
415 LinearOrientation = LinearLayout.Orientation.Vertical,
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",
423 pickerScroller.Scrolling += OnScroll;
424 pickerScroller.ScrollAnimationEnded += OnScrollAnimationEnded;
425 pickerScroller.ScrollAnimationStarted += OnScrollAnimationStarted;
427 itemList = new List<TextLabel>();
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;
441 KeyEvent += OnKeyEvent;
443 Accessibility.Accessibility.Enabled += (s, e) => {
444 isAtspiEnabled = true;
445 AccessibilityEnabled();
447 Accessibility.Accessibility.Disabled += (s, e) => {
448 isAtspiEnabled = false;
449 HideItemsForAccessibility();
451 Accessibility.Accessibility.ScreenReaderEnabled += (s, e) => {
452 isScreenReaderEnabled = true;
454 Accessibility.Accessibility.ScreenReaderDisabled += (s, e) => {
455 isScreenReaderEnabled = false;
458 isAtspiEnabled = Accessibility.Accessibility.IsEnabled;
459 isScreenReaderEnabled = Accessibility.Accessibility.IsScreenReaderEnabled;
463 private void HideItemsForAccessibility()
466 for (int i = accessibilityHiddenStartIdx; i < (accessibilityHiddenStartIdx + scrollVisibleItems); i++)
467 itemList[i].AccessibilityHidden = true;
470 if (currentValue > (middleItemIdx * 2))
471 for (int i = accessibilityHiddenStartIdx; i < (accessibilityHiddenStartIdx + (maxValue - minValue)); i++)
472 itemList[i].AccessibilityHidden = true;
474 for (int i = accessibilityHiddenStartIdx; i < (accessibilityHiddenStartIdx + (maxValue - minValue + 1)); i++)
475 itemList[i].AccessibilityHidden = true;
479 private void ShowItemsForAccessibility(int startIdx)
482 Tizen.Log.Error("NUI", "ScreenReaderEnabled signal emitted before picker value initialize");
486 accessibilityHiddenStartIdx = startIdx;
488 for (int i = accessibilityHiddenStartIdx; i < (accessibilityHiddenStartIdx + scrollVisibleItems); i++)
489 itemList[i].AccessibilityHidden = false;
492 if (currentValue > (middleItemIdx * 2))
493 for (int i = accessibilityHiddenStartIdx; i < (accessibilityHiddenStartIdx + (maxValue - minValue)); i++)
494 itemList[i].AccessibilityHidden = false;
496 for (int i = accessibilityHiddenStartIdx; i < (accessibilityHiddenStartIdx + (maxValue - minValue + 1)); i++)
497 itemList[i].AccessibilityHidden = false;
501 private void OnValueChanged()
506 HideItemsForAccessibility();
507 ShowItemsForAccessibility(currentValue - middleItemIdx);
509 if (isScreenReaderEnabled) itemList[currentValue].GrabAccessibilityHighlight();
512 ValueChangedEventArgs eventArgs =
513 new ValueChangedEventArgs(displayedValuesUpdate ? Int32.Parse(itemList[currentValue].Name) : Int32.Parse(itemList[currentValue].Text));
514 ValueChanged?.Invoke(this, eventArgs);
517 private void PageAdjust(float positionY)
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);
526 private void OnScroll(object sender, ScrollEventArgs e)
528 if (!loopEnabled || onAnimation || onAlignAnimation) return;
530 if (isAtspiEnabled) Accessibility.Accessibility.ClearCurrentlyHighlightedView();
532 PageAdjust(e.Position.Y);
535 private void OnScrollAnimationStarted(object sender, ScrollEventArgs e)
540 private void OnScrollAnimationEnded(object sender, ScrollEventArgs e)
542 //Ignore if the scroll position was not changed. (called it from this function)
543 if (lastScrollPosion == (int)e.Position.Y) return;
545 //Calc offset from closest item.
546 int offset = (int)(e.Position.Y + startScrollOffset) % itemHeight;
547 if (offset < -(itemHeight / 2)) offset += itemHeight;
549 lastScrollPosion = (int)(-e.Position.Y + offset);
552 if (onAlignAnimation)
554 onAlignAnimation = false;
555 if (loopEnabled == true)
557 PageAdjust(e.Position.Y);
559 if (currentValue != ((int)(-e.Position.Y / itemHeight) + middleItemIdx))
561 currentValue = ((int)(-e.Position.Y / itemHeight) + middleItemIdx);
568 //Item center align with animation, otherwise changed event emit.
571 onAlignAnimation = true;
572 pickerScroller.ScrollTo(-e.Position.Y + offset, true);
576 if (currentValue != ((int)(-e.Position.Y / itemHeight) + middleItemIdx))
578 currentValue = ((int)(-e.Position.Y / itemHeight) + middleItemIdx);
584 //This is UI requirement. It helps where exactly center item is.
585 private void AddLine()
588 downLine = new View();
594 private String GetItemText(bool loopEnabled, int idx)
596 if (!loopEnabled) return " ";
599 if (displayedValuesUpdate)
601 idx = idx - MinValue;
602 if (idx <= displayedValues.Count)
604 return displayedValues[idx];
609 return idx.ToString();
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)
619 TextLabel temp = new TextLabel(itemTextLabel)
621 WidthSpecification = LayoutParamPolicies.MatchParent,
622 Text = GetItemText(loopEnabled, idx),
623 Name = idx.ToString(),
626 temp.AccessibilitySuppressedEvents[AccessibilityEvent.MovedOut] = true;
627 // Hide on Accessibility tree
628 if (isAtspiEnabled) temp.AccessibilityHidden = true;
630 pickerScroller.Add(temp);
633 private void UpdateCurrentValue()
635 // -2 for center align
636 int startItemIdx = (currentValue == 0) ? -middleItemIdx : currentValue - minValue - middleItemIdx;
640 startY = ((dummyItemsForLoop + startItemIdx) * itemHeight) + startScrollOffset;
644 HideItemsForAccessibility();
645 ShowItemsForAccessibility(dummyItemsForLoop + startItemIdx);
648 // + 2 for non loop picker center align
651 startY = ((middleItemIdx + startItemIdx) * itemHeight) + startScrollOffset;
652 currentValue = currentValue - minValue + middleItemIdx;
656 HideItemsForAccessibility();
657 AccessibilityEnabled();
660 pickerScroller.ScrollTo(startY, false);
663 private void UpdateValueList()
665 if (!needItemUpdate) return;
666 if (minValue > maxValue) return;
668 //FIXME: This is wrong.
669 // But scroller can't update item property after added please fix me.
670 if (itemList.Count > 0)
673 pickerScroller.RemoveAllChildren();
676 if (maxValue - minValue + 1 >= scrollVisibleItems)
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;
685 dummyStartIdx = maxValue - (dummyItemsForLoop % (maxValue - minValue + 1)) + 1;
687 //Start add items in scroller. first dummys for scroll anim.
688 for (int i = 0; i < dummyItemsForLoop; i++)
690 if (dummyStartIdx > maxValue) dummyStartIdx = minValue;
691 AddPickerItem(loopEnabled, dummyStartIdx++);
694 for (int i = minValue; i <= maxValue; i++)
696 AddPickerItem(loopEnabled, i);
698 //Last dummys for scroll anim.
699 dummyStartIdx = minValue;
700 for (int i = 0; i < dummyItemsForLoop; i++)
702 if (dummyStartIdx > maxValue) dummyStartIdx = minValue;
703 AddPickerItem(loopEnabled, dummyStartIdx++);
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);
718 pageSize = itemHeight * (maxValue - minValue + 1);
720 UpdateCurrentValue();
722 //Give a correct scroll area.
725 pickerScroller.ScrollAvailableArea = new Vector2(0, (itemList.Count * itemHeight) - size.Height);
728 needItemUpdate = false;
731 private bool OnKeyEvent(object o, View.KeyEventArgs e)
733 if (e.Key.State == Key.StateType.Down)
735 if (e.Key.KeyPressedName == "Return")
739 //Todo: sometimes this gets wrong. the currentValue is not correct. need to be fixed.
740 if (currentValue != ((int)(-pickerScroller.Position.Y / itemHeight) + middleItemIdx))
742 currentValue = ((int)(-pickerScroller.Position.Y / itemHeight) + middleItemIdx);
746 //set editMode false (toggle the mode)
748 FocusManager.Instance.FocusIndicator = recoverIndicator;
753 //set editMode true (toggle the mode)
755 if (editModeIndicator == null)
757 editModeIndicator = new View()
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),
769 recoverIndicator = FocusManager.Instance.FocusIndicator;
770 FocusManager.Instance.FocusIndicator = editModeIndicator;
774 else if (e.Key.KeyPressedName == "Up")
778 InternalCurrentValue += 1;
782 else if (e.Key.KeyPressedName == "Down")
786 InternalCurrentValue -= 1;
800 internal class PickerScroller : ScrollableBase
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;
812 public PickerScroller() : base()
814 //Default rate is 0.998. this is for reduce scroll animation length.
815 decelerationRate = 0.991f;
816 logValueOfDeceleration = (float)Math.Log(decelerationRate);
819 public void SetPickerStyle(PickerStyle pickerStyle)
821 if (pickerStyle.StartScrollOffset != null)
823 startScrollOffset = (int)pickerStyle.StartScrollOffset.Height;
826 if (pickerStyle.ItemTextLabel?.Size != null)
828 itemHeight = (int)pickerStyle.ItemTextLabel.Size.Height;
831 if (pickerStyle.Size != null)
833 Size = new Size(-1, pickerStyle.Size.Height);
837 private float CustomScrollAlphaFunction(float progress)
839 if (panAnimationDelta == 0)
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);
856 //Override Decelerating for Picker feature.
857 protected override void Decelerating(float velocity, Animation animation)
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.
863 velocityOfLastPan = Math.Abs(velocity);
865 float currentScrollPosition = -ContentContainer.PositionY;
866 panAnimationDelta = (velocityOfLastPan * decelerationRate) / (1 - decelerationRate);
867 panAnimationDelta = velocity > 0 ? -panAnimationDelta : panAnimationDelta;
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;
875 float adjustDestination = AdjustTargetPositionOfScrollAnimation(destination);
877 float maxPosition = ScrollAvailableArea != null ? ScrollAvailableArea.Y : 0;
878 float minPosition = ScrollAvailableArea != null ? ScrollAvailableArea.X : 0;
880 if (destination < -maxPosition || destination > minPosition)
882 panAnimationDelta = velocity > 0 ? (currentScrollPosition - minPosition) : (maxPosition - currentScrollPosition);
883 destination = velocity > 0 ? minPosition : -maxPosition;
884 destination = -maxPosition + itemHeight;
886 if (panAnimationDelta == 0)
888 panAnimationDuration = 0.0f;
892 panAnimationDuration = (float)Math.Log((panAnimationDelta * logValueOfDeceleration / velocityOfLastPan + 1), decelerationRate);
897 panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
899 if (adjustDestination != destination)
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;
908 customScrollAlphaFunction = new UserAlphaFunctionDelegate(CustomScrollAlphaFunction);
909 animation.DefaultAlphaFunction = new AlphaFunction(customScrollAlphaFunction);
910 animation.Duration = (int)panAnimationDuration;
911 animation.AnimateTo(ContentContainer, "PositionY", (int)destination);