/* Copyright (c) 2021 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ using System; using Tizen.NUI; using Tizen.NUI.BaseComponents; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; namespace Tizen.NUI.Components { /// /// ValueChangedEventArgs is a class to notify changed Picker value argument which will sent to user. /// /// 9 public class ValueChangedEventArgs : EventArgs { /// /// ValueChangedEventArgs default constructor. /// value of Picker. /// [EditorBrowsable(EditorBrowsableState.Never)] public ValueChangedEventArgs(int value) { Value = value; } /// /// ValueChangedEventArgs default constructor. /// The current value of Picker. /// /// 9 public int Value { get; } } /// /// Picker is a class which provides a function that allows the user to select /// a value through a scrolling motion by expressing the specified value as a list. /// /// 9 public partial class Picker : Control { //Tizen 6.5 base components Picker guide visible scroll item is 5. private const int scrollVisibleItems = 5; //Dummy item count for loop feature. Max value of scrolling distance in //RPI target is bigger than 20 items height. it can adjust depends on the internal logic and device env. private const int dummyItemsForLoop = 20; private int startScrollOffset; private int itemHeight; private int startScrollY; private int startY; private int pageSize; private int currentValue; private int maxValue; private int minValue; private int lastScrollPosion; private bool onAnimation; //Scroller on animation check. private bool onAlignAnimation; private bool displayedValuesUpdate; //User sets displayed value check. private bool needItemUpdate; //min, max or display value updated check. private bool loopEnabled; private ReadOnlyCollection displayedValues; private PickerScroller pickerScroller; private View upLine; private View downLine; private IList itemList; private Vector2 size; private TextLabelStyle itemTextLabel; /// /// Creates a new instance of Picker. /// /// 9 public Picker() { } /// /// Creates a new instance of Picker. /// /// Creates Picker by special style defined in UX. /// 9 public Picker(string style) : base(style) { } /// /// Creates a new instance of Picker. /// /// Creates Picker by style customized by user. /// 9 public Picker(PickerStyle pickerStyle) : base(pickerStyle) { } /// /// Dispose Picker and all children on it. /// /// Dispose type. [EditorBrowsable(EditorBrowsableState.Never)] protected override void Dispose(DisposeTypes type) { if (disposed) { return; } if (type == DisposeTypes.Explicit) { if (itemList != null) { foreach (TextLabel textLabel in itemList) { if (pickerScroller) pickerScroller.Remove(textLabel); Utility.Dispose(textLabel); } itemList = null; } if (pickerScroller != null) { Remove(pickerScroller); Utility.Dispose(pickerScroller); pickerScroller = null; } Remove(upLine); Utility.Dispose(upLine); Remove(downLine); Utility.Dispose(downLine); } base.Dispose(type); } /// /// An event emitted when Picker value changed, user can subscribe or unsubscribe to this event handler. /// /// 9 public event EventHandler ValueChanged; //TODO Fomatter here /// /// The values to be displayed instead of numbers. /// /// 9 public ReadOnlyCollection DisplayedValues { get { return displayedValues; } set { displayedValues = value; needItemUpdate = true; displayedValuesUpdate = true; UpdateValueList(); } } /// /// The Current value of Picker. /// /// 9 public int CurrentValue { get { return (int)GetValue(CurrentValueProperty); } set { SetValue(CurrentValueProperty, value); NotifyPropertyChanged(); } } private int InternalCurrentValue { get { return currentValue; } set { if (currentValue == value) return; if (currentValue < minValue) currentValue = minValue; else if (currentValue > maxValue) currentValue = maxValue; currentValue = value; UpdateCurrentValue(); } } /// /// The max value of Picker. /// /// 9 public int MaxValue { get { return (int)GetValue(MaxValueProperty); } set { SetValue(MaxValueProperty, value); NotifyPropertyChanged(); } } private int InternalMaxValue { get { return maxValue; } set { if (maxValue == value) return; if (currentValue > value) currentValue = value; maxValue = value; needItemUpdate = true; UpdateValueList(); } } /// /// The min value of Picker. /// /// 9 public int MinValue { get { return (int)GetValue(MinValueProperty); } set { SetValue(MinValueProperty, value); NotifyPropertyChanged(); } } private int InternalMinValue { get { return minValue; } set { if (minValue == value) return; if (currentValue < value) currentValue = value; minValue = value; needItemUpdate = true; UpdateValueList(); } } /// [EditorBrowsable(EditorBrowsableState.Never)] public override void OnInitialize() { base.OnInitialize(); SetAccessibilityConstructor(Role.List); Initialize(); } /// /// Applies style to Picker. /// /// The style to apply. [EditorBrowsable(EditorBrowsableState.Never)] public override void ApplyStyle(ViewStyle viewStyle) { base.ApplyStyle(viewStyle); var pickerStyle = viewStyle as PickerStyle; if (pickerStyle == null) return; pickerScroller?.SetPickerStyle(pickerStyle); //Apply StartScrollOffset style. if (pickerStyle.StartScrollOffset != null) { startScrollOffset = (int)pickerStyle.StartScrollOffset.Height; } //Apply ItemTextLabel style. if (pickerStyle.ItemTextLabel != null) { if (itemTextLabel == null) { itemTextLabel = (TextLabelStyle)pickerStyle.ItemTextLabel.Clone(); } else { itemTextLabel.MergeDirectly(pickerStyle.ItemTextLabel); } itemHeight = (int)(pickerStyle.ItemTextLabel.Size?.Height ?? 0); if (itemList != null) foreach (TextLabel textLabel in itemList) textLabel.ApplyStyle(pickerStyle.ItemTextLabel); } //Apply PickerCenterLine style. if (pickerStyle.Divider != null && upLine != null && downLine != null) { upLine.ApplyStyle(pickerStyle.Divider); downLine.ApplyStyle(pickerStyle.Divider); downLine.PositionY = (int)pickerStyle.Divider.PositionY + itemHeight; } startScrollY = (itemHeight * dummyItemsForLoop) + startScrollOffset; startY = startScrollOffset; } /// [EditorBrowsable(EditorBrowsableState.Never)] public override void OnRelayout(Vector2 size, RelayoutContainer container) { if (size == null) return; if (size.Equals(this.size)) { return; } this.size = new Vector2(size); if (pickerScroller != null && itemList != null) { pickerScroller.ScrollAvailableArea = new Vector2(0, (itemList.Count * itemHeight) - size.Height); } } private void Initialize() { HeightSpecification = LayoutParamPolicies.MatchParent; //Picker Using scroller internally. actually it is a kind of scroller which has infinity loop, //and item center align features. pickerScroller = new PickerScroller() { WidthSpecification = LayoutParamPolicies.MatchParent, HeightSpecification = LayoutParamPolicies.MatchParent, ScrollingDirection = ScrollableBase.Direction.Vertical, Layout = new LinearLayout() { LinearOrientation = LinearLayout.Orientation.Vertical, }, //FIXME: Need to expand as many as possible; // When user want to start list middle of the list item. currently confused how to create list before render. ScrollAvailableArea = new Vector2(0, 10000), Name = "pickerScroller", }; pickerScroller.Scrolling += OnScroll; pickerScroller.ScrollAnimationEnded += OnScrollAnimationEnded; pickerScroller.ScrollAnimationStarted += OnScrollAnimationStarted; itemList = new List(); minValue = maxValue = currentValue = 0; displayedValues = null; //Those many flags for min, max, value method calling sequence dependency. needItemUpdate = true; displayedValuesUpdate = false; onAnimation = false; loopEnabled = false; Add(pickerScroller); AddLine(); } private void OnValueChanged() { ValueChangedEventArgs eventArgs = new ValueChangedEventArgs(displayedValuesUpdate ? Int32.Parse(itemList[currentValue].Name) : Int32.Parse(itemList[currentValue].Text)); ValueChanged?.Invoke(this, eventArgs); } private void PageAdjust(float positionY) { //Check the scroll is going out to the dummys if so, bring it back to page. if (positionY > -(startScrollY - (itemHeight * 2))) pickerScroller.ScrollTo(-positionY + pageSize, false); else if (positionY < -(startScrollY + pageSize - (itemHeight * 2))) pickerScroller.ScrollTo(-positionY - pageSize, false); } private void OnScroll(object sender, ScrollEventArgs e) { if (!loopEnabled || onAnimation || onAlignAnimation) return; PageAdjust(e.Position.Y); } private void OnScrollAnimationStarted(object sender, ScrollEventArgs e) { onAnimation = true; } private void OnScrollAnimationEnded(object sender, ScrollEventArgs e) { //Ignore if the scroll position was not changed. (called it from this function) if (lastScrollPosion == (int)e.Position.Y) return; //Calc offset from closest item. int offset = (int)(e.Position.Y + startScrollOffset) % itemHeight; if (offset < -(itemHeight / 2)) offset += itemHeight; lastScrollPosion = (int)(-e.Position.Y + offset); onAnimation = false; if (onAlignAnimation) { onAlignAnimation = false; if (loopEnabled == true) { PageAdjust(e.Position.Y); } if (currentValue != ((int)(-e.Position.Y / itemHeight) + 2)) { currentValue = ((int)(-e.Position.Y / itemHeight) + 2); OnValueChanged(); } return; } //Item center align with animation, otherwise changed event emit. if (offset != 0) { onAlignAnimation = true; pickerScroller.ScrollTo(-e.Position.Y + offset, true); } else { if (currentValue != ((int)(-e.Position.Y / itemHeight) + 2)) { currentValue = ((int)(-e.Position.Y / itemHeight) + 2); OnValueChanged(); } } } //This is UI requirement. It helps where exactly center item is. private void AddLine() { upLine = new View(); downLine = new View(); Add(upLine); Add(downLine); } private String GetItemText(bool loopEnabled, int idx) { if (!loopEnabled) return " "; else { if (displayedValuesUpdate) { idx = idx - MinValue; if (idx <= displayedValues.Count) { return displayedValues[idx]; } return " "; } return idx.ToString(); } } //FIXME: If textVisual can add in scroller please change it to textVisual for performance [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "The items are added to itemList and are disposed in Picker.Dispose().")] private void AddPickerItem(bool loopEnabled, int idx) { TextLabel temp = new TextLabel(itemTextLabel) { WidthSpecification = LayoutParamPolicies.MatchParent, Text = GetItemText(loopEnabled, idx), Name = idx.ToString(), }; itemList.Add(temp); pickerScroller.Add(temp); } private void UpdateCurrentValue() { // -2 for center align int startItemIdx = (currentValue == 0) ? -2 : currentValue - minValue - 2; if (loopEnabled) startY = ((dummyItemsForLoop + startItemIdx) * itemHeight) + startScrollOffset; // + 2 for non loop picker center align else { startY = ((2 + startItemIdx) * itemHeight) + startScrollOffset; currentValue = currentValue - minValue + 2; } pickerScroller.ScrollTo(startY, false); } private void UpdateValueList() { if (!needItemUpdate) return; if (minValue > maxValue) return; //FIXME: This is wrong. // But scroller can't update item property after added please fix me. if (itemList.Count > 0) { itemList.Clear(); pickerScroller.RemoveAllChildren(); } if (maxValue - minValue + 1 >= scrollVisibleItems) { loopEnabled = true; //Current scroller can't add at specific index. //So need below calc. int dummyStartIdx = 0; if (maxValue - minValue >= dummyItemsForLoop) dummyStartIdx = maxValue - dummyItemsForLoop + 1; else dummyStartIdx = maxValue - (dummyItemsForLoop % (maxValue - minValue + 1)) + 1; //Start add items in scroller. first dummys for scroll anim. for (int i = 0; i < dummyItemsForLoop; i++) { if (dummyStartIdx > maxValue) dummyStartIdx = minValue; AddPickerItem(loopEnabled, dummyStartIdx++); } //Second real items. for (int i = minValue; i <= maxValue; i++) { AddPickerItem(loopEnabled, i); } //Last dummys for scroll anim. dummyStartIdx = minValue; for (int i = 0; i < dummyItemsForLoop; i++) { if (dummyStartIdx > maxValue) dummyStartIdx = minValue; AddPickerItem(loopEnabled, dummyStartIdx++); } } else { loopEnabled = false; for (int i = 0; i < 2; i++) AddPickerItem(loopEnabled, 0); for (int i = minValue; i <= maxValue; i++) AddPickerItem(!loopEnabled, i); for (int i = 0; i < 2; i++) AddPickerItem(loopEnabled, 0); } pageSize = itemHeight * (maxValue - minValue + 1); UpdateCurrentValue(); //Give a correct scroll area. if (size != null) { pickerScroller.ScrollAvailableArea = new Vector2(0, (itemList.Count * itemHeight) - size.Height); } needItemUpdate = false; } internal class PickerScroller : ScrollableBase { private int itemHeight; private int startScrollOffset; private float velocityOfLastPan = 0.0f; private float panAnimationDuration = 0.0f; private float panAnimationDelta = 0.0f; private float decelerationRate = 0.0f; private float logValueOfDeceleration = 0.0f; private delegate float UserAlphaFunctionDelegate(float progress); private UserAlphaFunctionDelegate customScrollAlphaFunction; public PickerScroller() : base() { //Default rate is 0.998. this is for reduce scroll animation length. decelerationRate = 0.991f; logValueOfDeceleration = (float)Math.Log(decelerationRate); } public void SetPickerStyle(PickerStyle pickerStyle) { if (pickerStyle.StartScrollOffset != null) { startScrollOffset = (int)pickerStyle.StartScrollOffset.Height; } if (pickerStyle.ItemTextLabel?.Size != null) { itemHeight = (int)pickerStyle.ItemTextLabel.Size.Height; } if (pickerStyle.Size != null) { Size = new Size(-1, pickerStyle.Size.Height); } } private float CustomScrollAlphaFunction(float progress) { if (panAnimationDelta == 0) { return 1.0f; } else { // Parameter "progress" is normalized value. We need to multiply target duration to calculate distance. // Can get real distance using equation of deceleration (check Decelerating function) // After get real distance, normalize it float realDuration = progress * panAnimationDuration; float realDistance = velocityOfLastPan * ((float)Math.Pow(decelerationRate, realDuration) - 1) / logValueOfDeceleration; float result = Math.Min(realDistance / Math.Abs(panAnimationDelta), 1.0f); return result; } } //Override Decelerating for Picker feature. protected override void Decelerating(float velocity, Animation animation) { //Reduce Scroll animation speed. //The picker is to select items in the scroll area, it is not correct to animate //the scroll with very high speed. velocity *= 0.5f; velocityOfLastPan = Math.Abs(velocity); float currentScrollPosition = -ContentContainer.PositionY; panAnimationDelta = (velocityOfLastPan * decelerationRate) / (1 - decelerationRate); panAnimationDelta = velocity > 0 ? -panAnimationDelta : panAnimationDelta; float destination = -(panAnimationDelta + currentScrollPosition); //Animation destination has to center of the item. float align = destination % itemHeight; destination -= align; destination -= startScrollOffset; float adjustDestination = AdjustTargetPositionOfScrollAnimation(destination); float maxPosition = ScrollAvailableArea != null ? ScrollAvailableArea.Y : 0; float minPosition = ScrollAvailableArea != null ? ScrollAvailableArea.X : 0; if (destination < -maxPosition || destination > minPosition) { panAnimationDelta = velocity > 0 ? (currentScrollPosition - minPosition) : (maxPosition - currentScrollPosition); destination = velocity > 0 ? minPosition : -maxPosition; destination = -maxPosition + itemHeight; if (panAnimationDelta == 0) { panAnimationDuration = 0.0f; } else { panAnimationDuration = (float)Math.Log((panAnimationDelta * logValueOfDeceleration / velocityOfLastPan + 1), decelerationRate); } } else { panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration; if (adjustDestination != destination) { destination = adjustDestination; panAnimationDelta = destination + currentScrollPosition; velocityOfLastPan = Math.Abs(panAnimationDelta * logValueOfDeceleration / ((float)Math.Pow(decelerationRate, panAnimationDuration) - 1)); panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration; } } customScrollAlphaFunction = new UserAlphaFunctionDelegate(CustomScrollAlphaFunction); animation.DefaultAlphaFunction = new AlphaFunction(customScrollAlphaFunction); animation.Duration = (int)panAnimationDuration; animation.AnimateTo(ContentContainer, "PositionY", (int)destination); animation.Play(); } } } }