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 int startScrollOffset;
64 private int itemHeight;
65 private int startScrollY;
68 private int currentValue;
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;
80 private View downLine;
81 private IList<TextLabel> itemList;
83 private TextLabelStyle itemTextLabel;
84 private bool editMode = false;
85 private View recoverIndicator = null;
86 private View editModeIndicator = null;
90 /// Creates a new instance of Picker.
92 /// <since_tizen> 9 </since_tizen>
98 /// Creates a new instance of Picker.
100 /// <param name="style">Creates Picker by special style defined in UX.</param>
101 /// <since_tizen> 9 </since_tizen>
102 public Picker(string style) : base(style)
107 /// Creates a new instance of Picker.
109 /// <param name="pickerStyle">Creates Picker by style customized by user.</param>
110 /// <since_tizen> 9 </since_tizen>
111 public Picker(PickerStyle pickerStyle) : base(pickerStyle)
116 /// Dispose Picker and all children on it.
118 /// <param name="type">Dispose type.</param>
119 [EditorBrowsable(EditorBrowsableState.Never)]
120 protected override void Dispose(DisposeTypes type)
127 if (type == DisposeTypes.Explicit)
129 if (itemList != null)
131 foreach (TextLabel textLabel in itemList)
133 if (pickerScroller) pickerScroller.Remove(textLabel);
134 Utility.Dispose(textLabel);
140 if (pickerScroller != null)
142 Remove(pickerScroller);
143 Utility.Dispose(pickerScroller);
144 pickerScroller = null;
148 Utility.Dispose(upLine);
150 Utility.Dispose(downLine);
152 recoverIndicator = null;
153 if (editModeIndicator)
155 editModeIndicator.Dispose();
156 editModeIndicator = null;
164 /// An event emitted when Picker value changed, user can subscribe or unsubscribe to this event handler.
166 /// <since_tizen> 9 </since_tizen>
167 public event EventHandler<ValueChangedEventArgs> ValueChanged;
172 /// The values to be displayed instead of numbers.
174 /// <since_tizen> 9 </since_tizen>
175 public ReadOnlyCollection<String> DisplayedValues
179 return displayedValues;
183 displayedValues = value;
185 needItemUpdate = true;
186 displayedValuesUpdate = true;
193 /// The Current value of Picker.
195 /// <since_tizen> 9 </since_tizen>
196 public int CurrentValue
200 return (int)GetValue(CurrentValueProperty);
204 SetValue(CurrentValueProperty, value);
205 NotifyPropertyChanged();
208 private int InternalCurrentValue
216 if (currentValue == value) return;
218 if (currentValue < minValue) currentValue = minValue;
219 else if (currentValue > maxValue) currentValue = maxValue;
221 currentValue = value;
223 UpdateCurrentValue();
228 /// The max value of Picker.
230 /// <since_tizen> 9 </since_tizen>
235 return (int)GetValue(MaxValueProperty);
239 SetValue(MaxValueProperty, value);
240 NotifyPropertyChanged();
243 private int InternalMaxValue
251 if (maxValue == value) return;
252 if (currentValue > value) currentValue = value;
255 needItemUpdate = true;
262 /// The min value of Picker.
264 /// <since_tizen> 9 </since_tizen>
269 return (int)GetValue(MinValueProperty);
273 SetValue(MinValueProperty, value);
274 NotifyPropertyChanged();
277 private int InternalMinValue
285 if (minValue == value) return;
286 if (currentValue < value) currentValue = value;
289 needItemUpdate = true;
296 [EditorBrowsable(EditorBrowsableState.Never)]
297 public override void OnInitialize()
300 AccessibilityRole = Role.List;
306 /// Applies style to Picker.
308 /// <param name="viewStyle">The style to apply.</param>
309 [EditorBrowsable(EditorBrowsableState.Never)]
310 public override void ApplyStyle(ViewStyle viewStyle)
312 base.ApplyStyle(viewStyle);
314 var pickerStyle = viewStyle as PickerStyle;
316 if (pickerStyle == null) return;
318 pickerScroller?.SetPickerStyle(pickerStyle);
320 //Apply StartScrollOffset style.
321 if (pickerStyle.StartScrollOffset != null)
323 startScrollOffset = (int)pickerStyle.StartScrollOffset.Height;
326 //Apply ItemTextLabel style.
327 if (pickerStyle.ItemTextLabel != null)
329 if (itemTextLabel == null)
331 itemTextLabel = (TextLabelStyle)pickerStyle.ItemTextLabel.Clone();
335 itemTextLabel.MergeDirectly(pickerStyle.ItemTextLabel);
338 itemHeight = (int)(pickerStyle.ItemTextLabel.Size?.Height ?? 0);
340 if (itemList != null)
341 foreach (TextLabel textLabel in itemList)
342 textLabel.ApplyStyle(pickerStyle.ItemTextLabel);
345 //Apply PickerCenterLine style.
346 if (pickerStyle.Divider != null && upLine != null && downLine != null)
348 upLine.ApplyStyle(pickerStyle.Divider);
349 downLine.ApplyStyle(pickerStyle.Divider);
350 downLine.PositionY = (int)pickerStyle.Divider.PositionY + itemHeight;
353 startScrollY = (itemHeight * dummyItemsForLoop) + startScrollOffset;
354 startY = startScrollOffset;
358 [EditorBrowsable(EditorBrowsableState.Never)]
359 public override void OnRelayout(Vector2 size, RelayoutContainer container)
361 if (size == null) return;
363 if (size.Equals(this.size))
368 this.size = new Vector2(size);
370 if (pickerScroller != null && itemList != null)
372 pickerScroller.ScrollAvailableArea = new Vector2(0, (itemList.Count * itemHeight) - size.Height);
376 private void Initialize()
378 HeightSpecification = LayoutParamPolicies.MatchParent;
380 //Picker Using scroller internally. actually it is a kind of scroller which has infinity loop,
381 //and item center align features.
382 pickerScroller = new PickerScroller()
384 WidthSpecification = LayoutParamPolicies.MatchParent,
385 HeightSpecification = LayoutParamPolicies.MatchParent,
386 ScrollingDirection = ScrollableBase.Direction.Vertical,
387 Layout = new LinearLayout()
389 LinearOrientation = LinearLayout.Orientation.Vertical,
391 //FIXME: Need to expand as many as possible;
392 // When user want to start list middle of the list item. currently confused how to create list before render.
393 ScrollAvailableArea = new Vector2(0, 10000),
394 Name = "pickerScroller",
397 pickerScroller.Scrolling += OnScroll;
398 pickerScroller.ScrollAnimationEnded += OnScrollAnimationEnded;
399 pickerScroller.ScrollAnimationStarted += OnScrollAnimationStarted;
401 itemList = new List<TextLabel>();
403 minValue = maxValue = currentValue = 0;
404 displayedValues = null;
405 //Those many flags for min, max, value method calling sequence dependency.
406 needItemUpdate = true;
407 displayedValuesUpdate = false;
415 KeyEvent += OnKeyEvent;
418 private void OnValueChanged()
420 ValueChangedEventArgs eventArgs =
421 new ValueChangedEventArgs(displayedValuesUpdate ? Int32.Parse(itemList[currentValue].Name) : Int32.Parse(itemList[currentValue].Text));
422 ValueChanged?.Invoke(this, eventArgs);
425 private void PageAdjust(float positionY)
427 //Check the scroll is going out to the dummys if so, bring it back to page.
428 if (positionY > -(startScrollY - (itemHeight * 2)))
429 pickerScroller.ScrollTo(-positionY + pageSize, false);
430 else if (positionY < -(startScrollY + pageSize - (itemHeight * 2)))
431 pickerScroller.ScrollTo(-positionY - pageSize, false);
434 private void OnScroll(object sender, ScrollEventArgs e)
436 if (!loopEnabled || onAnimation || onAlignAnimation) return;
438 PageAdjust(e.Position.Y);
441 private void OnScrollAnimationStarted(object sender, ScrollEventArgs e)
446 private void OnScrollAnimationEnded(object sender, ScrollEventArgs e)
448 //Ignore if the scroll position was not changed. (called it from this function)
449 if (lastScrollPosion == (int)e.Position.Y) return;
451 //Calc offset from closest item.
452 int offset = (int)(e.Position.Y + startScrollOffset) % itemHeight;
453 if (offset < -(itemHeight / 2)) offset += itemHeight;
455 lastScrollPosion = (int)(-e.Position.Y + offset);
458 if (onAlignAnimation)
460 onAlignAnimation = false;
461 if (loopEnabled == true)
463 PageAdjust(e.Position.Y);
465 if (currentValue != ((int)(-e.Position.Y / itemHeight) + 2))
467 currentValue = ((int)(-e.Position.Y / itemHeight) + 2);
474 //Item center align with animation, otherwise changed event emit.
477 onAlignAnimation = true;
478 pickerScroller.ScrollTo(-e.Position.Y + offset, true);
482 if (currentValue != ((int)(-e.Position.Y / itemHeight) + 2))
484 currentValue = ((int)(-e.Position.Y / itemHeight) + 2);
490 //This is UI requirement. It helps where exactly center item is.
491 private void AddLine()
494 downLine = new View();
500 private String GetItemText(bool loopEnabled, int idx)
502 if (!loopEnabled) return " ";
505 if (displayedValuesUpdate)
507 idx = idx - MinValue;
508 if (idx <= displayedValues.Count)
510 return displayedValues[idx];
515 return idx.ToString();
519 //FIXME: If textVisual can add in scroller please change it to textVisual for performance
520 [SuppressMessage("Microsoft.Reliability",
521 "CA2000:DisposeObjectsBeforeLosingScope",
522 Justification = "The items are added to itemList and are disposed in Picker.Dispose().")]
523 private void AddPickerItem(bool loopEnabled, int idx)
525 TextLabel temp = new TextLabel(itemTextLabel)
527 WidthSpecification = LayoutParamPolicies.MatchParent,
528 Text = GetItemText(loopEnabled, idx),
529 Name = idx.ToString(),
532 temp.AccessibilitySuppressedEvents[AccessibilityEvent.MovedOut] = true;
534 pickerScroller.Add(temp);
537 private void UpdateCurrentValue()
539 // -2 for center align
540 int startItemIdx = (currentValue == 0) ? -2 : currentValue - minValue - 2;
542 if (loopEnabled) startY = ((dummyItemsForLoop + startItemIdx) * itemHeight) + startScrollOffset;
543 // + 2 for non loop picker center align
546 startY = ((2 + startItemIdx) * itemHeight) + startScrollOffset;
547 currentValue = currentValue - minValue + 2;
549 pickerScroller.ScrollTo(startY, false);
552 private void UpdateValueList()
554 if (!needItemUpdate) return;
555 if (minValue > maxValue) return;
557 //FIXME: This is wrong.
558 // But scroller can't update item property after added please fix me.
559 if (itemList.Count > 0)
562 pickerScroller.RemoveAllChildren();
565 if (maxValue - minValue + 1 >= scrollVisibleItems)
568 //Current scroller can't add at specific index.
569 //So need below calc.
570 int dummyStartIdx = 0;
571 if (maxValue - minValue >= dummyItemsForLoop)
572 dummyStartIdx = maxValue - dummyItemsForLoop + 1;
574 dummyStartIdx = maxValue - (dummyItemsForLoop % (maxValue - minValue + 1)) + 1;
576 //Start add items in scroller. first dummys for scroll anim.
577 for (int i = 0; i < dummyItemsForLoop; i++)
579 if (dummyStartIdx > maxValue) dummyStartIdx = minValue;
580 AddPickerItem(loopEnabled, dummyStartIdx++);
583 for (int i = minValue; i <= maxValue; i++)
585 AddPickerItem(loopEnabled, i);
587 //Last dummys for scroll anim.
588 dummyStartIdx = minValue;
589 for (int i = 0; i < dummyItemsForLoop; i++)
591 if (dummyStartIdx > maxValue) dummyStartIdx = minValue;
592 AddPickerItem(loopEnabled, dummyStartIdx++);
599 for (int i = 0; i < 2; i++)
600 AddPickerItem(loopEnabled, 0);
601 for (int i = minValue; i <= maxValue; i++)
602 AddPickerItem(!loopEnabled, i);
603 for (int i = 0; i < 2; i++)
604 AddPickerItem(loopEnabled, 0);
607 pageSize = itemHeight * (maxValue - minValue + 1);
609 UpdateCurrentValue();
611 //Give a correct scroll area.
614 pickerScroller.ScrollAvailableArea = new Vector2(0, (itemList.Count * itemHeight) - size.Height);
617 needItemUpdate = false;
620 private bool OnKeyEvent(object o, View.KeyEventArgs e)
622 if (e.Key.State == Key.StateType.Down)
624 if (e.Key.KeyPressedName == "Return")
628 //Todo: sometimes this gets wrong. the currentValue is not correct. need to be fixed.
629 if (currentValue != ((int)(-pickerScroller.Position.Y / itemHeight) + 2))
631 currentValue = ((int)(-pickerScroller.Position.Y / itemHeight) + 2);
635 //set editMode false (toggle the mode)
637 FocusManager.Instance.FocusIndicator = recoverIndicator;
642 //set editMode true (toggle the mode)
644 if (editModeIndicator == null)
646 editModeIndicator = new View()
648 PositionUsesPivotPoint = true,
649 PivotPoint = new Position(0, 0, 0),
650 WidthResizePolicy = ResizePolicyType.FillToParent,
651 HeightResizePolicy = ResizePolicyType.FillToParent,
652 BorderlineColor = Color.Red,
653 BorderlineWidth = 6.0f,
654 BorderlineOffset = -1f,
655 BackgroundColor = new Color(0.2f, 0.2f, 0.2f, 0.4f),
658 recoverIndicator = FocusManager.Instance.FocusIndicator;
659 FocusManager.Instance.FocusIndicator = editModeIndicator;
663 else if (e.Key.KeyPressedName == "Up")
667 InternalCurrentValue += 1;
671 else if (e.Key.KeyPressedName == "Down")
675 InternalCurrentValue -= 1;
689 internal class PickerScroller : ScrollableBase
691 private int itemHeight;
692 private int startScrollOffset;
693 private float velocityOfLastPan = 0.0f;
694 private float panAnimationDuration = 0.0f;
695 private float panAnimationDelta = 0.0f;
696 private float decelerationRate = 0.0f;
697 private float logValueOfDeceleration = 0.0f;
698 private delegate float UserAlphaFunctionDelegate(float progress);
699 private UserAlphaFunctionDelegate customScrollAlphaFunction;
701 public PickerScroller() : base()
703 //Default rate is 0.998. this is for reduce scroll animation length.
704 decelerationRate = 0.991f;
705 logValueOfDeceleration = (float)Math.Log(decelerationRate);
708 public void SetPickerStyle(PickerStyle pickerStyle)
710 if (pickerStyle.StartScrollOffset != null)
712 startScrollOffset = (int)pickerStyle.StartScrollOffset.Height;
715 if (pickerStyle.ItemTextLabel?.Size != null)
717 itemHeight = (int)pickerStyle.ItemTextLabel.Size.Height;
720 if (pickerStyle.Size != null)
722 Size = new Size(-1, pickerStyle.Size.Height);
726 private float CustomScrollAlphaFunction(float progress)
728 if (panAnimationDelta == 0)
734 // Parameter "progress" is normalized value. We need to multiply target duration to calculate distance.
735 // Can get real distance using equation of deceleration (check Decelerating function)
736 // After get real distance, normalize it
737 float realDuration = progress * panAnimationDuration;
738 float realDistance = velocityOfLastPan * ((float)Math.Pow(decelerationRate, realDuration) - 1) / logValueOfDeceleration;
739 float result = Math.Min(realDistance / Math.Abs(panAnimationDelta), 1.0f);
745 //Override Decelerating for Picker feature.
746 protected override void Decelerating(float velocity, Animation animation)
748 //Reduce Scroll animation speed.
749 //The picker is to select items in the scroll area, it is not correct to animate
750 //the scroll with very high speed.
752 velocityOfLastPan = Math.Abs(velocity);
754 float currentScrollPosition = -ContentContainer.PositionY;
755 panAnimationDelta = (velocityOfLastPan * decelerationRate) / (1 - decelerationRate);
756 panAnimationDelta = velocity > 0 ? -panAnimationDelta : panAnimationDelta;
758 float destination = -(panAnimationDelta + currentScrollPosition);
759 //Animation destination has to center of the item.
760 float align = destination % itemHeight;
761 destination -= align;
762 destination -= startScrollOffset;
764 float adjustDestination = AdjustTargetPositionOfScrollAnimation(destination);
766 float maxPosition = ScrollAvailableArea != null ? ScrollAvailableArea.Y : 0;
767 float minPosition = ScrollAvailableArea != null ? ScrollAvailableArea.X : 0;
769 if (destination < -maxPosition || destination > minPosition)
771 panAnimationDelta = velocity > 0 ? (currentScrollPosition - minPosition) : (maxPosition - currentScrollPosition);
772 destination = velocity > 0 ? minPosition : -maxPosition;
773 destination = -maxPosition + itemHeight;
775 if (panAnimationDelta == 0)
777 panAnimationDuration = 0.0f;
781 panAnimationDuration = (float)Math.Log((panAnimationDelta * logValueOfDeceleration / velocityOfLastPan + 1), decelerationRate);
786 panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
788 if (adjustDestination != destination)
790 destination = adjustDestination;
791 panAnimationDelta = destination + currentScrollPosition;
792 velocityOfLastPan = Math.Abs(panAnimationDelta * logValueOfDeceleration / ((float)Math.Pow(decelerationRate, panAnimationDuration) - 1));
793 panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
797 customScrollAlphaFunction = new UserAlphaFunctionDelegate(CustomScrollAlphaFunction);
798 animation.DefaultAlphaFunction = new AlphaFunction(customScrollAlphaFunction);
799 animation.Duration = (int)panAnimationDuration;
800 animation.AnimateTo(ContentContainer, "PositionY", (int)destination);