/* 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();
}
}
}
}