using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
+using System.Runtime.InteropServices;
namespace Tizen.NUI.Components
{
Vertical
}
- /// <summary>
- /// [Draft] Configurable speed threshold that register the gestures as a flick.
- /// If the flick speed less than the threshold then will not be considered a flick.
- /// </summary>
- /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
- [EditorBrowsable(EditorBrowsableState.Never)]
- public float FlickThreshold { get; set; } = 0.2f;
-
- /// <summary>
- /// [Draft] Configurable duration modifer for the flick animation.
- /// Determines the speed of the scroll, large value results in a longer flick animation. Range (0.1 - 1.0)
- /// </summary>
- /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
- [EditorBrowsable(EditorBrowsableState.Never)]
- public float FlickAnimationSpeed { get; set; } = 0.4f;
-
- /// <summary>
- /// [Draft] Configurable modifer for the distance to be scrolled when flicked detected.
- /// It a ratio of the ScrollableBase's length. (not child's length).
- /// First value is the ratio of the distance to scroll with the weakest flick.
- /// Second value is the ratio of the distance to scroll with the strongest flick.
- /// Second > First.
- /// </summary>
- /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
- [EditorBrowsable(EditorBrowsableState.Never)]
- public Vector2 FlickDistanceMultiplierRange { get; set; } = new Vector2(0.6f, 1.8f);
-
/// <summary>
/// [Draft] Scrolling direction mode.
/// Default is Vertical scrolling.
if (mScrollEnabled)
{
mPanGestureDetector.Detected += OnPanGestureDetected;
- mTapGestureDetector.Detected += OnTapGestureDetected;
}
else
{
mPanGestureDetector.Detected -= OnPanGestureDetected;
- mTapGestureDetector.Detected -= OnTapGestureDetected;
}
}
}
}
}
+ /// <summary>
+ /// Container which has content of ScrollableBase.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public View ContentContainer { get; private set; }
+
+ /// <summary>
+ /// Set the layout on this View. Replaces any existing Layout.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public new LayoutItem Layout
+ {
+ get
+ {
+ return ContentContainer.Layout;
+ }
+ set
+ {
+ ContentContainer.Layout = value;
+ if (ContentContainer.Layout != null)
+ {
+ ContentContainer.Layout.SetPositionByLayout = false;
+ }
+ }
+ }
+
+ /// <summary>
+ /// List of children of Container.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public new List<View> Children
+ {
+ get
+ {
+ return ContentContainer.Children;
+ }
+ }
+
+ /// <summary>
+ /// Deceleration rate of scrolling by finger.
+ /// Rate should be 0 < rate < 1.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public float DecelerationRate
+ {
+ get
+ {
+ return decelerationRate;
+ }
+ set
+ {
+ decelerationRate = value;
+ logValueOfDeceleration = (float)Math.Log(value);
+ }
+ }
+
+ /// <summary>
+ /// Threashold not to go infinit at the end of scrolling animation.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public float DecelerationThreshold { get; set; } = 0.1f;
+
private bool hideScrollbar = true;
- private Animation scrollAnimation;
private float maxScrollDistance;
private float childTargetPosition = 0.0f;
private PanGestureDetector mPanGestureDetector;
- private TapGestureDetector mTapGestureDetector;
private View mInterruptTouchingChild;
private ScrollbarBase scrollBar;
- private float multiplier = 1.0f;
private bool scrolling = false;
private float ratioOfScreenWidthToCompleteScroll = 0.5f;
private float totalDisplacementForPan = 0.0f;
private bool flickWhenAnimating = false;
private PropertyNotification propertyNotification;
+ private float noticeAnimationEndBeforePosition = 0.0f;
+ private bool readyToNotice = false;
+ // Let's consider more whether this needs to be set as protected.
+ public float NoticeAnimationEndBeforePosition { get => noticeAnimationEndBeforePosition; set => noticeAnimationEndBeforePosition = value; }
+
+
// Let's consider more whether this needs to be set as protected.
private float finalTargetPosition;
+ private Animation scrollAnimation;
+ // Declare user alpha function delegate
+ [UnmanagedFunctionPointer(CallingConvention.StdCall)]
+ private delegate float UserAlphaFunctionDelegate(float progress);
+ private UserAlphaFunctionDelegate customScrollAlphaFunction;
+ private float velocityOfLastPan = 0.0f;
+ private float panAnimationDuration = 0.0f;
+ private float panAnimationDelta = 0.0f;
+ private float logValueOfDeceleration = 0.0f;
+ private float decelerationRate = 0.0f;
+
+
/// <summary>
/// [Draft] Constructor
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public ScrollableBase() : base()
{
+ DecelerationRate = 0.998f;
+
base.Layout = new ScrollableBaseCustomLayout();
mPanGestureDetector = new PanGestureDetector();
mPanGestureDetector.Attach(this);
mPanGestureDetector.AddDirection(PanGestureDetector.DirectionVertical);
mPanGestureDetector.Detected += OnPanGestureDetected;
- mTapGestureDetector = new TapGestureDetector();
- mTapGestureDetector.Attach(this);
- mTapGestureDetector.Detected += OnTapGestureDetected;
-
ClippingMode = ClippingModeType.ClipChildren;
//Default Scrolling child
{
WidthSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.MatchParent : LayoutParamPolicies.WrapContent,
HeightSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.WrapContent : LayoutParamPolicies.MatchParent,
- Layout = new AbsoluteLayout(){SetPositionByLayout = false},
+ Layout = new AbsoluteLayout() { SetPositionByLayout = false },
};
ContentContainer.Relayout += OnScrollingChildRelayout;
propertyNotification = ContentContainer.AddPropertyNotification("position", PropertyCondition.Step(1.0f));
BackgroundColor = Color.Transparent,
};
mInterruptTouchingChild.TouchEvent += OnIterruptTouchingChildTouched;
-
Scrollbar = new Scrollbar();
}
- /// <summary>
- /// Container which has content of ScrollableBase.
- /// </summary>
- [EditorBrowsable(EditorBrowsableState.Never)]
- public View ContentContainer { get; private set; }
-
- /// <summary>
- /// Set the layout on this View. Replaces any existing Layout.
- /// </summary>
- public new LayoutItem Layout
+ private bool OnIterruptTouchingChildTouched(object source, View.TouchEventArgs args)
{
- get
+ if (args.Touch.GetState(0) == PointStateType.Down)
{
- return ContentContainer.Layout;
- }
- set
- {
- ContentContainer.Layout = value;
- if(ContentContainer.Layout != null)
+ if (scrolling && !SnapToPage)
{
- ContentContainer.Layout.SetPositionByLayout = false;
+ StopScroll();
}
}
- }
-
- /// <summary>
- /// List of children of Container.
- /// </summary>
- public new List<View> Children
- {
- get
- {
- return ContentContainer.Children;
- }
- }
-
- private bool OnIterruptTouchingChildTouched(object source, View.TouchEventArgs args)
- {
return true;
}
[EditorBrowsable(EditorBrowsableState.Never)]
public override void Remove(View view)
{
- if(SnapToPage && CurrentPage == Children.IndexOf(view) && CurrentPage == Children.Count -1)
+ if (SnapToPage && CurrentPage == Children.IndexOf(view) && CurrentPage == Children.Count - 1)
{
// Target View is current page and also last child.
// CurrentPage should be changed to previous page.
- CurrentPage = Math.Max(0, CurrentPage-1);
+ CurrentPage = Math.Max(0, CurrentPage - 1);
ScrollToIndex(CurrentPage);
}
private void OnScrollAnimationEnded()
{
+ scrolling = false;
+ base.Remove(mInterruptTouchingChild);
+
ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
ScrollAnimationEnded?.Invoke(this, eventArgs);
}
- private bool readyToNotice = false;
-
- private float noticeAnimationEndBeforePosition = 0.0f;
- // Let's consider more whether this needs to be set as protected.
- public float NoticeAnimationEndBeforePosition { get => noticeAnimationEndBeforePosition; set => noticeAnimationEndBeforePosition = value; }
-
private void OnScroll()
{
ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
}
scrollAnimation.Duration = duration;
- scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.EaseOutSine);
+ scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.EaseOutSquare);
scrollAnimation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", axisPosition);
scrolling = true;
OnScrollAnimationStarted();
{
// Calculate scroll animaton duration
float scrollDistance = Math.Abs(displacement);
- int duration = (int)((320 * FlickAnimationSpeed) + (scrollDistance * FlickAnimationSpeed));
- Debug.WriteLineIf(LayoutDebugScrollableBase, "Scroll Animation Duration:" + duration + " Distance:" + scrollDistance);
-
readyToNotice = true;
- AnimateChildTo(duration, BoundScrollPosition(AdjustTargetPositionOfScrollAnimation(BoundScrollPosition(childTargetPosition))));
+ AnimateChildTo(ScrollDuration, BoundScrollPosition(AdjustTargetPositionOfScrollAnimation(BoundScrollPosition(childTargetPosition))));
}
else
{
mPanGestureDetector.Dispose();
mPanGestureDetector = null;
}
-
- if (mTapGestureDetector != null)
- {
- mTapGestureDetector.Detected -= OnTapGestureDetected;
- mTapGestureDetector.Dispose();
- mTapGestureDetector = null;
- }
}
base.Dispose(type);
}
- private float CalculateDisplacementFromVelocity(float axisVelocity)
- {
- // Map: flick speed of range (2.0 - 6.0) to flick multiplier of range (0.7 - 1.6)
- float speedMinimum = FlickThreshold;
- float speedMaximum = FlickThreshold + 6.0f;
- float multiplierMinimum = FlickDistanceMultiplierRange.X;
- float multiplierMaximum = FlickDistanceMultiplierRange.Y;
-
- float flickDisplacement = 0.0f;
-
- float speed = Math.Min(4.0f, Math.Abs(axisVelocity));
-
- Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollableBase Candidate Flick speed:" + speed);
-
- if (speed > FlickThreshold)
- {
- // Flick length is the length of the ScrollableBase.
- float flickLength = (ScrollingDirection == Direction.Horizontal) ? CurrentSize.Width : CurrentSize.Height;
-
- // Calculate multiplier by mapping speed between the multiplier minimum and maximum.
- multiplier = ((speed - speedMinimum) / ((speedMaximum - speedMinimum) * (multiplierMaximum - multiplierMinimum))) + multiplierMinimum;
-
- // flick displacement is the product of the flick length and multiplier
- flickDisplacement = ((flickLength * multiplier) * speed) / axisVelocity; // *speed and /velocity to perserve sign.
-
- Debug.WriteLineIf(LayoutDebugScrollableBase, "Calculated FlickDisplacement[" + flickDisplacement + "] from speed[" + speed + "] multiplier:"
- + multiplier);
- }
- return flickDisplacement;
- }
-
private float CalculateMaximumScrollDistance()
{
float scrollingChildLength = 0;
AnimateChildTo(ScrollDuration, destinationX);
}
- private void Flick(float flickDisplacement)
- {
- if (SnapToPage && Children.Count > 0)
- {
- if ((flickWhenAnimating && scrolling == true) || (scrolling == false))
- {
- if (flickDisplacement < 0)
- {
- CurrentPage = Math.Min(Math.Max(Children.Count - 1, 0), CurrentPage + 1);
- Debug.WriteLineIf(LayoutDebugScrollableBase, "Snap - to page:" + CurrentPage);
- }
- else
- {
- CurrentPage = Math.Max(0, CurrentPage - 1);
- Debug.WriteLineIf(LayoutDebugScrollableBase, "Snap + to page:" + CurrentPage);
- }
-
- float destinationX = -(Children[CurrentPage].Position.X + Children[CurrentPage].CurrentSize.Width / 2.0f - CurrentSize.Width / 2.0f); // set to middle of current page
- Debug.WriteLineIf(LayoutDebugScrollableBase, "Snapping to :" + destinationX);
- AnimateChildTo(ScrollDuration, destinationX);
- }
- }
- else
- {
- ScrollBy(flickDisplacement, true); // Animate flickDisplacement.
- }
- }
-
private void OnPanGestureDetected(object source, PanGestureDetector.DetectedEventArgs e)
{
if (e.PanGesture.State == Gesture.StateType.Started)
{
+ readyToNotice = false;
base.Add(mInterruptTouchingChild);
Debug.WriteLineIf(LayoutDebugScrollableBase, "Gesture Start");
if (scrolling && !SnapToPage)
}
else if (e.PanGesture.State == Gesture.StateType.Finished)
{
- float axisVelocity = (ScrollingDirection == Direction.Horizontal) ? e.PanGesture.Velocity.X : e.PanGesture.Velocity.Y;
- float flickDisplacement = CalculateDisplacementFromVelocity(axisVelocity);
-
- Debug.WriteLineIf(LayoutDebugScrollableBase, "FlickDisplacement:" + flickDisplacement + "TotalDisplacementForPan:" + totalDisplacementForPan);
OnScrollDragEnded();
+ StopScroll(); // Will replace previous animation so will stop existing one.
+
+ if (scrollAnimation == null)
+ {
+ scrollAnimation = new Animation();
+ scrollAnimation.Finished += ScrollAnimationFinished;
+ }
- if (flickDisplacement > 0 | flickDisplacement < 0)// Flick detected
+ if (SnapToPage)
{
- Flick(flickDisplacement);
+ PageSnap();
}
else
{
- // End of panning gesture but was not a flick
- if (SnapToPage && Children.Count > 0)
- {
- PageSnap();
- }
- else
- {
- ScrollBy(0, true);
- }
+ Decelerating((ScrollingDirection == Direction.Horizontal) ? e.PanGesture.Velocity.X : e.PanGesture.Velocity.Y);
}
+
totalDisplacementForPan = 0;
+ scrolling = true;
+ readyToNotice = true;
+ OnScrollAnimationStarted();
+ }
+ }
- base.Remove(mInterruptTouchingChild);
+ 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;
}
}
- private new void OnTapGestureDetected(object source, TapGestureDetector.DetectedEventArgs e)
+ private void Decelerating(float velocity)
{
- if (e.TapGesture.Type == Gesture.GestureType.Tap)
+ // Decelerating using deceleration equation ===========
+ //
+ // V : velocity (pixel per milisecond)
+ // V0 : initial velocity
+ // d : deceleration rate,
+ // t : time
+ // X : final position after decelerating
+ // log : natural logarithm
+ //
+ // V(t) = V0 * d pow t;
+ // X(t) = V0 * (d pow t - 1) / log d; <-- Integrate the velocity function
+ // X(∞) = V0 * d / (1 - d); <-- Result using inifit T can be final position because T is tending to infinity.
+ //
+ // Because of final T is tending to inifity, we should use threshold value to finish.
+ // Final T = log(-threshold * log d / |V0| ) / log d;
+
+ velocityOfLastPan = Math.Abs(velocity);
+
+ float currentScrollPosition = -(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
+ panAnimationDelta = (velocityOfLastPan * decelerationRate) / (1 - decelerationRate);
+ panAnimationDelta = velocity > 0 ? -panAnimationDelta : panAnimationDelta;
+
+ float destination = -(panAnimationDelta + currentScrollPosition);
+ float adjustDestination = AdjustTargetPositionOfScrollAnimation(destination);
+ float maxPosition = ScrollAvailableArea != null ? ScrollAvailableArea.Y : maxScrollDistance;
+ float minPosition = ScrollAvailableArea != null ? ScrollAvailableArea.X : 0;
+
+ if (destination < -maxPosition || destination > minPosition)
{
- // Stop scrolling if tap detected (press then relase).
- // Unless in Pages mode, do not want a page change to stop part way.
- if (scrolling && !SnapToPage)
+ panAnimationDelta = velocity > 0 ? (currentScrollPosition - minPosition) : (maxPosition - currentScrollPosition);
+ destination = velocity > 0 ? minPosition : -maxPosition;
+
+ if (panAnimationDelta == 0)
{
- StopScroll();
+ panAnimationDuration = 0.0f;
+ }
+ else
+ {
+ panAnimationDuration = (float)Math.Log((panAnimationDelta * logValueOfDeceleration / velocityOfLastPan + 1), decelerationRate);
+ }
+
+ Debug.WriteLineIf(LayoutDebugScrollableBase, "\n" +
+ "OverRange======================= \n" +
+ "[decelerationRate] " + decelerationRate + "\n" +
+ "[logValueOfDeceleration] " + logValueOfDeceleration + "\n" +
+ "[Velocity] " + velocityOfLastPan + "\n" +
+ "[CurrentPosition] " + currentScrollPosition + "\n" +
+ "[CandidateDelta] " + panAnimationDelta + "\n" +
+ "[Destination] " + destination + "\n" +
+ "[Duration] " + panAnimationDuration + "\n" +
+ "================================ \n"
+ );
+ }
+ 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;
}
+
+ Debug.WriteLineIf(LayoutDebugScrollableBase, "\n" +
+ "================================ \n" +
+ "[decelerationRate] " + decelerationRate + "\n" +
+ "[logValueOfDeceleration] " + logValueOfDeceleration + "\n" +
+ "[Velocity] " + velocityOfLastPan + "\n" +
+ "[CurrentPosition] " + currentScrollPosition + "\n" +
+ "[CandidateDelta] " + panAnimationDelta + "\n" +
+ "[Destination] " + destination + "\n" +
+ "[Duration] " + panAnimationDuration + "\n" +
+ "================================ \n"
+ );
}
+
+ finalTargetPosition = destination;
+
+ customScrollAlphaFunction = new UserAlphaFunctionDelegate(CustomScrollAlphaFunction);
+ scrollAnimation.DefaultAlphaFunction = new AlphaFunction(customScrollAlphaFunction);
+ GC.KeepAlive(customScrollAlphaFunction);
+ scrollAnimation.Duration = (int)panAnimationDuration;
+ scrollAnimation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", destination);
+ scrollAnimation.Play();
+ }
+
+ protected void OnTapGestureDetected(object source, TapGestureDetector.DetectedEventArgs e)
+ {
+
}
private void ScrollAnimationFinished(object sender, EventArgs e)
{
- scrolling = false;
- CheckPreReachedTargetPosition();
OnScrollAnimationEnded();
}