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 [EditorBrowsable(EditorBrowsableState.Never)]
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 [EditorBrowsable(EditorBrowsableState.Never)]
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 [EditorBrowsable(EditorBrowsableState.Never)]
56 public 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;
82 private PickerStyle pickerStyle => ViewStyle as PickerStyle;
85 /// Creates a new instance of Picker.
87 [EditorBrowsable(EditorBrowsableState.Never)]
94 /// Creates a new instance of Picker.
96 /// <param name="style">Creates Picker by special style defined in UX.</param>
97 [EditorBrowsable(EditorBrowsableState.Never)]
98 public Picker(string style) : base(style)
104 /// Creates a new instance of Picker.
106 /// <param name="pickerStyle">Creates Picker by style customized by user.</param>
107 [EditorBrowsable(EditorBrowsableState.Never)]
108 public Picker(PickerStyle pickerStyle) : base(pickerStyle)
114 /// Dispose Picker and all children on it.
116 /// <param name="type">Dispose type.</param>
117 [EditorBrowsable(EditorBrowsableState.Never)]
118 protected override void Dispose(DisposeTypes type)
125 if (type == DisposeTypes.Explicit)
127 if (itemList != null)
129 foreach (TextLabel textLabel in itemList)
131 if (pickerScroller) pickerScroller.Remove(textLabel);
132 Utility.Dispose(textLabel);
138 if (pickerScroller != null)
140 Remove(pickerScroller);
141 Utility.Dispose(pickerScroller);
142 pickerScroller = null;
146 Utility.Dispose(upLine);
148 Utility.Dispose(downLine);
155 /// An event emitted when Picker value changed, user can subscribe or unsubscribe to this event handler.
157 [EditorBrowsable(EditorBrowsableState.Never)]
158 public event EventHandler<ValueChangedEventArgs> ValueChanged;
163 /// The values to be displayed instead of numbers.
165 [EditorBrowsable(EditorBrowsableState.Never)]
166 public ReadOnlyCollection<String> DisplayedValues
170 return displayedValues;
174 displayedValues = value;
176 needItemUpdate = true;
177 displayedValuesUpdate = true;
184 /// The Current value of Picker.
186 [EditorBrowsable(EditorBrowsableState.Never)]
187 public int CurrentValue
195 if (currentValue == value) return;
197 if (currentValue < minValue) currentValue = minValue;
198 else if (currentValue > maxValue) currentValue = maxValue;
200 currentValue = value;
202 UpdateCurrentValue();
207 /// The max value of Picker.
209 [EditorBrowsable(EditorBrowsableState.Never)]
218 if (maxValue == value) return;
219 if (currentValue > value) currentValue = value;
222 needItemUpdate = true;
229 /// The min value of Picker.
231 [EditorBrowsable(EditorBrowsableState.Never)]
240 if (minValue == value) return;
241 if (currentValue < value) currentValue = value;
244 needItemUpdate = true;
251 [EditorBrowsable(EditorBrowsableState.Never)]
252 public override void OnInitialize()
255 SetAccessibilityConstructor(Role.List, AccessibilityInterface.Value);
259 /// Applies style to Picker.
261 /// <param name="viewStyle">The style to apply.</param>
262 [EditorBrowsable(EditorBrowsableState.Never)]
263 public override void ApplyStyle(ViewStyle viewStyle)
265 base.ApplyStyle(viewStyle);
267 //Apply StartScrollOffset style.
268 if (pickerStyle?.StartScrollOffset != null)
269 startScrollOffset = (int)pickerStyle.StartScrollOffset.Height;
271 //Apply ItemTextLabel style.
272 if (pickerStyle?.ItemTextLabel != null)
274 itemHeight = (int)pickerStyle.ItemTextLabel.Size.Height;
276 if (itemList != null)
277 foreach (TextLabel textLabel in itemList)
278 textLabel.ApplyStyle(pickerStyle.ItemTextLabel);
281 //Apply PickerCenterLine style.
282 if (pickerStyle?.Divider != null && upLine != null && downLine != null)
284 upLine.ApplyStyle(pickerStyle.Divider);
285 downLine.ApplyStyle(pickerStyle.Divider);
286 downLine.PositionY = (int)pickerStyle.Divider.PositionY + itemHeight;
290 private void Initialize()
292 AccessibilityHighlightable = true;
293 HeightSpecification = LayoutParamPolicies.MatchParent;
295 //Picker Using scroller internally. actually it is a kind of scroller which has infinity loop,
296 //and item center align features.
297 pickerScroller = new PickerScroller(pickerStyle)
299 Size = new Size(-1, pickerStyle.Size.Height),
300 ScrollingDirection = ScrollableBase.Direction.Vertical,
301 Layout = new LinearLayout()
303 LinearOrientation = LinearLayout.Orientation.Vertical,
305 //FIXME: Need to expand as many as possible;
306 // When user want to start list middle of the list item. currently confused how to create list before render.
307 ScrollAvailableArea = new Vector2(0, 10000),
308 Name = "pickerScroller",
310 pickerScroller.Scrolling += OnScroll;
311 pickerScroller.ScrollAnimationEnded += OnScrollAnimationEnded;
312 pickerScroller.ScrollAnimationStarted += OnScrollAnimationStarted;
314 itemList = new List<TextLabel>();
316 minValue = maxValue = currentValue = 0;
317 displayedValues = null;
318 //Those many flags for min, max, value method calling sequence dependency.
319 needItemUpdate = true;
320 displayedValuesUpdate = false;
324 startScrollOffset = (int)pickerStyle.StartScrollOffset.Height;
325 itemHeight = (int)pickerStyle.ItemTextLabel.Size.Height;
326 startScrollY = (itemHeight * dummyItemsForLoop) + startScrollOffset;
327 startY = startScrollOffset;
333 private void OnValueChanged()
335 ValueChangedEventArgs eventArgs =
336 new ValueChangedEventArgs(displayedValuesUpdate ? Int32.Parse(itemList[currentValue].Name) : Int32.Parse(itemList[currentValue].Text));
337 ValueChanged?.Invoke(this, eventArgs);
340 private void PageAdjust(float positionY)
342 //Check the scroll is going out to the dummys if so, bring it back to page.
343 if (positionY > -(startScrollY - (itemHeight * 2)))
344 pickerScroller.ScrollTo(-positionY + pageSize, false);
345 else if (positionY < -(startScrollY + pageSize - (itemHeight * 2)))
346 pickerScroller.ScrollTo(-positionY - pageSize, false);
349 private void OnScroll(object sender, ScrollEventArgs e)
351 if (!loopEnabled || onAnimation || onAlignAnimation) return;
353 PageAdjust(e.Position.Y);
356 private void OnScrollAnimationStarted(object sender, ScrollEventArgs e)
361 private void OnScrollAnimationEnded(object sender, ScrollEventArgs e)
363 //Ignore if the scroll position was not changed. (called it from this function)
364 if (lastScrollPosion == (int)e.Position.Y) return;
366 //Calc offset from closest item.
367 int offset = (int)(e.Position.Y + startScrollOffset) % itemHeight;
368 if (offset < -(itemHeight / 2)) offset += itemHeight;
370 lastScrollPosion = (int)(-e.Position.Y + offset);
373 if (onAlignAnimation) {
374 onAlignAnimation = false;
375 if (loopEnabled == true)
377 PageAdjust(e.Position.Y);
379 if (currentValue != ((int)(-e.Position.Y / itemHeight) + 2))
381 currentValue = ((int)(-e.Position.Y / itemHeight) + 2);
388 //Item center align with animation, otherwise changed event emit.
390 onAlignAnimation = true;
391 pickerScroller.ScrollTo(-e.Position.Y + offset, true);
394 if (currentValue != ((int)(-e.Position.Y / itemHeight) + 2))
396 currentValue = ((int)(-e.Position.Y / itemHeight) + 2);
402 //This is UI requirement. It helps where exactly center item is.
403 private void AddLine()
405 upLine = new View(pickerStyle.Divider);
406 downLine = new View(pickerStyle.Divider)
408 Position = new Position(0, (int)pickerStyle.Divider.PositionY + itemHeight),
415 private String GetItemText(bool loopEnabled, int idx)
417 if (!loopEnabled) return " ";
419 if (displayedValuesUpdate) {
420 idx = idx - MinValue;
421 if (idx <= displayedValues.Count) {
422 return displayedValues[idx];
427 return idx.ToString();
431 //FIXME: If textVisual can add in scroller please change it to textVisual for performance
432 [SuppressMessage("Microsoft.Reliability",
433 "CA2000:DisposeObjectsBeforeLosingScope",
434 Justification = "The items are added to itemList and are disposed in Picker.Dispose().")]
435 private void AddPickerItem(bool loopEnabled, int idx)
437 TextLabel temp = new TextLabel(pickerStyle.ItemTextLabel)
439 WidthSpecification = LayoutParamPolicies.MatchParent,
440 Text = GetItemText(loopEnabled, idx),
441 Name = idx.ToString(),
445 pickerScroller.Add(temp);
448 private void UpdateCurrentValue()
450 // -2 for center align
451 int startItemIdx = (currentValue == 0) ? -2 : currentValue - minValue - 2;
453 if (loopEnabled) startY = ((dummyItemsForLoop + startItemIdx) * itemHeight) + startScrollOffset;
454 // + 2 for non loop picker center align
455 else startY = ((2 + startItemIdx) * itemHeight) + startScrollOffset;
456 pickerScroller.ScrollTo(startY, false);
459 private void UpdateValueList()
461 if (!needItemUpdate) return;
462 if (minValue > maxValue) return;
464 //FIXME: This is wrong.
465 // But scroller can't update item property after added please fix me.
466 if (itemList.Count > 0) {
468 pickerScroller.RemoveAllChildren();
471 if (maxValue - minValue + 1 >= scrollVisibleItems)
474 //Current scroller can't add at specific index.
475 //So need below calc.
476 int dummyStartIdx = 0;
477 if (maxValue - minValue >= dummyItemsForLoop)
478 dummyStartIdx = maxValue - dummyItemsForLoop + 1;
480 dummyStartIdx = maxValue - (dummyItemsForLoop % (maxValue - minValue + 1)) + 1;
482 //Start add items in scroller. first dummys for scroll anim.
483 for (int i = 0; i < dummyItemsForLoop; i++)
485 if (dummyStartIdx > maxValue) dummyStartIdx = minValue;
486 AddPickerItem(loopEnabled, dummyStartIdx++);
489 for (int i = minValue; i <= maxValue; i++)
491 AddPickerItem(loopEnabled, i);
493 //Last dummys for scroll anim.
494 dummyStartIdx = minValue;
495 for (int i = 0; i < dummyItemsForLoop; i++)
497 if (dummyStartIdx > maxValue) dummyStartIdx = minValue;
498 AddPickerItem(loopEnabled, dummyStartIdx++);
505 for (int i = 0; i < 2; i++)
506 AddPickerItem(loopEnabled, 0);
507 for (int i = minValue; i <= maxValue; i++)
508 AddPickerItem(!loopEnabled, i);
509 for (int i = 0; i < 2; i++)
510 AddPickerItem(loopEnabled, 0);
513 pageSize = itemHeight * (maxValue - minValue + 1);
515 UpdateCurrentValue();
517 //Give a correct scroll area.
518 pickerScroller.ScrollAvailableArea = new Vector2(0, (itemList.Count * itemHeight) - pickerStyle.Size.Height);
520 needItemUpdate = false;
523 internal class PickerScroller : ScrollableBase
525 private int itemHeight;
526 private int startScrollOffset;
527 private float velocityOfLastPan = 0.0f;
528 private float panAnimationDuration = 0.0f;
529 private float panAnimationDelta = 0.0f;
530 private float decelerationRate = 0.0f;
531 private float logValueOfDeceleration = 0.0f;
532 private delegate float UserAlphaFunctionDelegate(float progress);
533 private UserAlphaFunctionDelegate customScrollAlphaFunction;
535 public PickerScroller(PickerStyle pickerStyle) : base()
537 //Default rate is 0.998. this is for reduce scroll animation length.
538 decelerationRate = 0.991f;
539 startScrollOffset = (int)pickerStyle.StartScrollOffset.Height;
540 itemHeight = (int)pickerStyle.ItemTextLabel.Size.Height;
541 logValueOfDeceleration = (float)Math.Log(decelerationRate);
544 private float CustomScrollAlphaFunction(float progress)
546 if (panAnimationDelta == 0)
552 // Parameter "progress" is normalized value. We need to multiply target duration to calculate distance.
553 // Can get real distance using equation of deceleration (check Decelerating function)
554 // After get real distance, normalize it
555 float realDuration = progress * panAnimationDuration;
556 float realDistance = velocityOfLastPan * ((float)Math.Pow(decelerationRate, realDuration) - 1) / logValueOfDeceleration;
557 float result = Math.Min(realDistance / Math.Abs(panAnimationDelta), 1.0f);
563 //Override Decelerating for Picker feature.
564 protected override void Decelerating(float velocity, Animation animation)
566 //Reduce Scroll animation speed.
567 //The picker is to select items in the scroll area, it is not correct to animate
568 //the scroll with very high speed.
570 velocityOfLastPan = Math.Abs(velocity);
572 float currentScrollPosition = -ContentContainer.PositionY;
573 panAnimationDelta = (velocityOfLastPan * decelerationRate) / (1 - decelerationRate);
574 panAnimationDelta = velocity > 0 ? -panAnimationDelta : panAnimationDelta;
576 float destination = -(panAnimationDelta + currentScrollPosition);
577 //Animation destination has to center of the item.
578 float align = destination % itemHeight;
579 destination -= align;
580 destination -= startScrollOffset;
582 float adjustDestination = AdjustTargetPositionOfScrollAnimation(destination);
584 float maxPosition = ScrollAvailableArea != null ? ScrollAvailableArea.Y : 0;
585 float minPosition = ScrollAvailableArea != null ? ScrollAvailableArea.X : 0;
587 if (destination < -maxPosition || destination > minPosition)
589 panAnimationDelta = velocity > 0 ? (currentScrollPosition - minPosition) : (maxPosition - currentScrollPosition);
590 destination = velocity > 0 ? minPosition : -maxPosition;
591 destination = -maxPosition + itemHeight;
593 if (panAnimationDelta == 0)
595 panAnimationDuration = 0.0f;
599 panAnimationDuration = (float)Math.Log((panAnimationDelta * logValueOfDeceleration / velocityOfLastPan + 1), decelerationRate);
604 panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
606 if (adjustDestination != destination)
608 destination = adjustDestination;
609 panAnimationDelta = destination + currentScrollPosition;
610 velocityOfLastPan = Math.Abs(panAnimationDelta * logValueOfDeceleration / ((float)Math.Pow(decelerationRate, panAnimationDuration) - 1));
611 panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
615 customScrollAlphaFunction = new UserAlphaFunctionDelegate(CustomScrollAlphaFunction);
616 animation.DefaultAlphaFunction = new AlphaFunction(customScrollAlphaFunction);
617 animation.Duration = (int)panAnimationDuration;
618 animation.AnimateTo(ContentContainer, "PositionY", (int)destination);