1 /* Copyright (c) 2020 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.
17 using Tizen.NUI.BaseComponents;
18 using System.Collections.Generic;
19 using System.ComponentModel;
20 using System.Diagnostics;
21 using System.Runtime.InteropServices;
23 namespace Tizen.NUI.Components
26 /// ScrollEventArgs is a class to record scroll event arguments which will sent to user.
28 /// <since_tizen> 8 </since_tizen>
29 public class ScrollEventArgs : EventArgs
31 private Position position;
34 /// Default constructor.
36 /// <param name="position">Current scroll position</param>
37 /// <since_tizen> 8 </since_tizen>
38 public ScrollEventArgs(Position position)
40 this.position = position;
44 /// Current position of ContentContainer.
46 /// <since_tizen> 8 </since_tizen>
47 public Position Position
57 /// This class provides a View that can scroll a single View with a layout. This View can be a nest of Views.
59 /// <since_tizen> 8 </since_tizen>
60 public class ScrollableBase : Control
62 static bool LayoutDebugScrollableBase = false; // Debug flag
63 private Direction mScrollingDirection = Direction.Vertical;
64 private bool mScrollEnabled = true;
65 private int mPageWidth = 0;
67 private class ScrollableBaseCustomLayout : LayoutGroup
69 protected override void OnMeasure(MeasureSpecification widthMeasureSpec, MeasureSpecification heightMeasureSpec)
71 Extents padding = Padding;
72 float totalHeight = padding.Top + padding.Bottom;
73 float totalWidth = padding.Start + padding.End;
75 MeasuredSize.StateType childWidthState = MeasuredSize.StateType.MeasuredSizeOK;
76 MeasuredSize.StateType childHeightState = MeasuredSize.StateType.MeasuredSizeOK;
78 Direction scrollingDirection = Direction.Vertical;
79 ScrollableBase scrollableBase = this.Owner as ScrollableBase;
82 scrollingDirection = scrollableBase.ScrollingDirection;
85 // measure child, should be a single scrolling child
86 foreach (LayoutItem childLayout in LayoutChildren)
88 if (childLayout != null)
91 // Use an Unspecified MeasureSpecification mode so scrolling child is not restricted to it's parents size in Height (for vertical scrolling)
92 // or Width for horizontal scrolling
93 MeasureSpecification unrestrictedMeasureSpec = new MeasureSpecification(heightMeasureSpec.Size, MeasureSpecification.ModeType.Unspecified);
95 if (scrollingDirection == Direction.Vertical)
97 MeasureChildWithMargins(childLayout, widthMeasureSpec, new LayoutLength(0), unrestrictedMeasureSpec, new LayoutLength(0)); // Height unrestricted by parent
101 MeasureChildWithMargins(childLayout, unrestrictedMeasureSpec, new LayoutLength(0), heightMeasureSpec, new LayoutLength(0)); // Width unrestricted by parent
104 float childWidth = childLayout.MeasuredWidth.Size.AsDecimal();
105 float childHeight = childLayout.MeasuredHeight.Size.AsDecimal();
107 // Determine the width and height needed by the children using their given position and size.
108 // Children could overlap so find the left most and right most child.
109 Position2D childPosition = childLayout.Owner.Position2D;
110 float childLeft = childPosition.X;
111 float childTop = childPosition.Y;
113 // Store current width and height needed to contain all children.
114 Extents childMargin = childLayout.Margin;
115 totalWidth = childWidth + childMargin.Start + childMargin.End;
116 totalHeight = childHeight + childMargin.Top + childMargin.Bottom;
118 if (childLayout.MeasuredWidth.State == MeasuredSize.StateType.MeasuredSizeTooSmall)
120 childWidthState = MeasuredSize.StateType.MeasuredSizeTooSmall;
122 if (childLayout.MeasuredHeight.State == MeasuredSize.StateType.MeasuredSizeTooSmall)
124 childHeightState = MeasuredSize.StateType.MeasuredSizeTooSmall;
130 MeasuredSize widthSizeAndState = ResolveSizeAndState(new LayoutLength(totalWidth + Padding.Start + Padding.End), widthMeasureSpec, MeasuredSize.StateType.MeasuredSizeOK);
131 MeasuredSize heightSizeAndState = ResolveSizeAndState(new LayoutLength(totalHeight + Padding.Top + Padding.Bottom), heightMeasureSpec, MeasuredSize.StateType.MeasuredSizeOK);
132 totalWidth = widthSizeAndState.Size.AsDecimal();
133 totalHeight = heightSizeAndState.Size.AsDecimal();
135 // Ensure layout respects it's given minimum size
136 totalWidth = Math.Max(totalWidth, SuggestedMinimumWidth.AsDecimal());
137 totalHeight = Math.Max(totalHeight, SuggestedMinimumHeight.AsDecimal());
139 widthSizeAndState.State = childWidthState;
140 heightSizeAndState.State = childHeightState;
142 SetMeasuredDimensions(ResolveSizeAndState(new LayoutLength(totalWidth + Padding.Start + Padding.End), widthMeasureSpec, childWidthState),
143 ResolveSizeAndState(new LayoutLength(totalHeight + Padding.Top + Padding.Bottom), heightMeasureSpec, childHeightState));
145 // Size of ScrollableBase is changed. Change Page width too.
146 scrollableBase.mPageWidth = (int)MeasuredWidth.Size.AsRoundedValue();
149 protected override void OnLayout(bool changed, LayoutLength left, LayoutLength top, LayoutLength right, LayoutLength bottom)
151 foreach (LayoutItem childLayout in LayoutChildren)
153 if (childLayout != null)
155 LayoutLength childWidth = childLayout.MeasuredWidth.Size;
156 LayoutLength childHeight = childLayout.MeasuredHeight.Size;
158 Position2D childPosition = childLayout.Owner.Position2D;
159 Extents padding = Padding;
160 Extents childMargin = childLayout.Margin;
162 LayoutLength childLeft = new LayoutLength(childPosition.X + childMargin.Start + padding.Start);
163 LayoutLength childTop = new LayoutLength(childPosition.Y + childMargin.Top + padding.Top);
165 childLayout.Layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
169 } // ScrollableBaseCustomLayout
172 /// The direction axis to scroll.
174 /// <since_tizen> 8 </since_tizen>
175 public enum Direction
180 /// <since_tizen> 8 </since_tizen>
186 /// <since_tizen> 8 </since_tizen>
191 /// Scrolling direction mode.
192 /// Default is Vertical scrolling.
194 /// <since_tizen> 8 </since_tizen>
195 public Direction ScrollingDirection
199 return mScrollingDirection;
203 if (value != mScrollingDirection)
205 mScrollingDirection = value;
206 mPanGestureDetector.RemoveDirection(value == Direction.Horizontal ?
207 PanGestureDetector.DirectionVertical : PanGestureDetector.DirectionHorizontal);
208 mPanGestureDetector.AddDirection(value == Direction.Horizontal ?
209 PanGestureDetector.DirectionHorizontal : PanGestureDetector.DirectionVertical);
211 ContentContainer.WidthSpecification = mScrollingDirection == Direction.Vertical ?
212 LayoutParamPolicies.MatchParent : LayoutParamPolicies.WrapContent;
213 ContentContainer.HeightSpecification = mScrollingDirection == Direction.Vertical ?
214 LayoutParamPolicies.WrapContent : LayoutParamPolicies.MatchParent;
220 /// Enable or disable scrolling.
222 /// <since_tizen> 8 </since_tizen>
223 public bool ScrollEnabled
227 return mScrollEnabled;
231 if (value != mScrollEnabled)
233 mScrollEnabled = value;
236 mPanGestureDetector.Detected += OnPanGestureDetected;
240 mPanGestureDetector.Detected -= OnPanGestureDetected;
247 /// Pages mode, enables moving to the next or return to current page depending on pan displacement.
248 /// Default is false.
250 /// <since_tizen> 8 </since_tizen>
251 public bool SnapToPage { set; get; } = false;
254 /// Get current page.
255 /// Working property with SnapToPage property.
257 /// <since_tizen> 8 </since_tizen>
258 public int CurrentPage { get; private set; } = 0;
261 /// Duration of scroll animation.
262 /// Default value is 125ms.
264 /// <since_tizen> 8 </since_tizen>
265 public int ScrollDuration { set; get; } = 125;
268 /// Scroll Available area.
270 /// <since_tizen> 8 </since_tizen>
271 public Vector2 ScrollAvailableArea { set; get; }
274 /// An event emitted when user starts dragging ScrollableBase, user can subscribe or unsubscribe to this event handler.<br />
276 /// <since_tizen> 8 </since_tizen>
277 public event EventHandler<ScrollEventArgs> ScrollDragStarted;
280 /// An event emitted when user stops dragging ScrollableBase, user can subscribe or unsubscribe to this event handler.<br />
282 /// <since_tizen> 8 </since_tizen>
283 public event EventHandler<ScrollEventArgs> ScrollDragEnded;
287 /// An event emitted when the scrolling slide animation starts, user can subscribe or unsubscribe to this event handler.<br />
289 /// <since_tizen> 8 </since_tizen>
290 public event EventHandler<ScrollEventArgs> ScrollAnimationStarted;
293 /// An event emitted when the scrolling slide animation ends, user can subscribe or unsubscribe to this event handler.<br />
295 /// <since_tizen> 8 </since_tizen>
296 public event EventHandler<ScrollEventArgs> ScrollAnimationEnded;
300 /// An event emitted when scrolling, user can subscribe or unsubscribe to this event handler.<br />
302 /// <since_tizen> 8 </since_tizen>
303 public event EventHandler<ScrollEventArgs> Scrolling;
307 /// Scrollbar for ScrollableBase.
309 /// <since_tizen> 8 </since_tizen>
310 public ScrollbarBase Scrollbar
320 scrollBar.Unparent();
324 scrollBar.Name = "ScrollBar";
341 /// Always hide Scrollbar.
343 /// <since_tizen> 8 </since_tizen>
344 public bool HideScrollbar
348 return hideScrollbar;
352 hideScrollbar = value;
369 /// Container which has content of ScrollableBase.
371 /// <since_tizen> 8 </since_tizen>
372 public View ContentContainer { get; private set; }
375 /// Set the layout on this View. Replaces any existing Layout.
377 /// <since_tizen> 8 </since_tizen>
378 public new LayoutItem Layout
382 return ContentContainer.Layout;
386 ContentContainer.Layout = value;
387 if (ContentContainer.Layout != null)
389 ContentContainer.Layout.SetPositionByLayout = false;
395 /// List of children of Container.
397 /// <since_tizen> 8 </since_tizen>
398 public new List<View> Children
402 return ContentContainer.Children;
407 /// Deceleration rate of scrolling by finger.
408 /// Rate should be bigger than 0 and smaller than 1.
409 /// Default value is 0.998f;
411 /// <since_tizen> 8 </since_tizen>
412 public float DecelerationRate
416 return decelerationRate;
420 decelerationRate = value;
421 logValueOfDeceleration = (float)Math.Log(value);
426 /// Threashold not to go infinit at the end of scrolling animation.
428 [EditorBrowsable(EditorBrowsableState.Never)]
429 public float DecelerationThreshold { get; set; } = 0.1f;
432 /// Page will be changed when velocity of panning is over threshold.
433 /// The unit of threshold is pixel per milisec.
435 /// <since_tizen> 8 </since_tizen>
436 public float PageFlickThreshold { get; set; } = 0.4f;
439 /// Alphafunction for scroll animation.
441 [EditorBrowsable(EditorBrowsableState.Never)]
442 public AlphaFunction ScrollAlphaFunction { get; set; } = new AlphaFunction(AlphaFunction.BuiltinFunctions.Linear);
444 private bool hideScrollbar = true;
445 private float maxScrollDistance;
446 private float childTargetPosition = 0.0f;
447 private PanGestureDetector mPanGestureDetector;
448 private View mInterruptTouchingChild;
449 private ScrollbarBase scrollBar;
450 private bool scrolling = false;
451 private float ratioOfScreenWidthToCompleteScroll = 0.5f;
452 private float totalDisplacementForPan = 0.0f;
453 private Size previousContainerSize = new Size();
454 private PropertyNotification propertyNotification;
455 private float noticeAnimationEndBeforePosition = 0.0f;
456 private bool readyToNotice = false;
459 /// Notice before animation is finished.
461 [EditorBrowsable(EditorBrowsableState.Never)]
462 // Let's consider more whether this needs to be set as protected.
463 public float NoticeAnimationEndBeforePosition { get => noticeAnimationEndBeforePosition; set => noticeAnimationEndBeforePosition = value; }
465 // Let's consider more whether this needs to be set as protected.
466 private float finalTargetPosition;
468 private Animation scrollAnimation;
469 // Declare user alpha function delegate
470 [UnmanagedFunctionPointer(CallingConvention.StdCall)]
471 private delegate float UserAlphaFunctionDelegate(float progress);
472 private UserAlphaFunctionDelegate customScrollAlphaFunction;
473 private float velocityOfLastPan = 0.0f;
474 private float panAnimationDuration = 0.0f;
475 private float panAnimationDelta = 0.0f;
476 private float logValueOfDeceleration = 0.0f;
477 private float decelerationRate = 0.0f;
480 /// Default Constructor
482 /// <since_tizen> 8 </since_tizen>
483 public ScrollableBase() : base()
485 DecelerationRate = 0.998f;
487 base.Layout = new ScrollableBaseCustomLayout();
488 mPanGestureDetector = new PanGestureDetector();
489 mPanGestureDetector.Attach(this);
490 mPanGestureDetector.AddDirection(PanGestureDetector.DirectionVertical);
491 mPanGestureDetector.Detected += OnPanGestureDetected;
493 ClippingMode = ClippingModeType.ClipChildren;
495 //Default Scrolling child
496 ContentContainer = new View()
498 WidthSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.MatchParent : LayoutParamPolicies.WrapContent,
499 HeightSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.WrapContent : LayoutParamPolicies.MatchParent,
500 Layout = new AbsoluteLayout() { SetPositionByLayout = false },
502 ContentContainer.Relayout += OnScrollingChildRelayout;
503 propertyNotification = ContentContainer.AddPropertyNotification("position", PropertyCondition.Step(1.0f));
504 propertyNotification.Notified += OnPropertyChanged;
505 base.Add(ContentContainer);
507 //Interrupt touching when panning is started
508 mInterruptTouchingChild = new View()
510 Size = new Size(Window.Instance.WindowSize),
511 BackgroundColor = Color.Transparent,
513 mInterruptTouchingChild.TouchEvent += OnIterruptTouchingChildTouched;
514 Scrollbar = new Scrollbar();
517 private bool OnIterruptTouchingChildTouched(object source, View.TouchEventArgs args)
519 if (args.Touch.GetState(0) == PointStateType.Down)
521 if (scrolling && !SnapToPage)
529 private void OnPropertyChanged(object source, PropertyNotification.NotifyEventArgs args)
535 /// Called after a child has been added to the owning view.
537 /// <param name="view">The child which has been added.</param>
538 /// <since_tizen> 8 </since_tizen>
539 public override void Add(View view)
541 ContentContainer.Add(view);
545 /// Called after a child has been removed from the owning view.
547 /// <param name="view">The child which has been removed.</param>
548 /// <since_tizen> 8 </since_tizen>
549 public override void Remove(View view)
551 if (SnapToPage && CurrentPage == Children.IndexOf(view) && CurrentPage == Children.Count - 1)
553 // Target View is current page and also last child.
554 // CurrentPage should be changed to previous page.
555 CurrentPage = Math.Max(0, CurrentPage - 1);
556 ScrollToIndex(CurrentPage);
559 ContentContainer.Remove(view);
562 private void OnScrollingChildRelayout(object source, EventArgs args)
564 // Size is changed. Calculate maxScrollDistance.
565 bool isSizeChanged = previousContainerSize.Width != ContentContainer.Size.Width || previousContainerSize.Height != ContentContainer.Size.Height;
569 maxScrollDistance = CalculateMaximumScrollDistance();
573 previousContainerSize = ContentContainer.Size;
577 /// The composition of a Scrollbar can vary depending on how you use ScrollableBase.
578 /// Set the composition that will go into the ScrollableBase according to your ScrollableBase.
580 /// <since_tizen> 8 </since_tizen>
581 [EditorBrowsable(EditorBrowsableState.Never)]
582 protected virtual void SetScrollbar()
586 bool isHorizontal = ScrollingDirection == Direction.Horizontal;
587 float contentLength = isHorizontal ? ContentContainer.Size.Width : ContentContainer.Size.Height;
588 float viewportLength = isHorizontal ? Size.Width : Size.Height;
589 float currentPosition = isHorizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y;
590 Scrollbar.Initialize(contentLength, viewportLength, currentPosition, isHorizontal);
595 /// Scrolls to the item at the specified index.
597 /// <param name="index">Index of item.</param>
598 /// <since_tizen> 8 </since_tizen>
599 public void ScrollToIndex(int index)
601 if (ContentContainer.ChildCount - 1 < index || index < 0)
611 float targetPosition = Math.Min(ScrollingDirection == Direction.Vertical ? Children[index].Position.Y : Children[index].Position.X, maxScrollDistance);
612 AnimateChildTo(ScrollDuration, -targetPosition);
615 private void OnScrollDragStarted()
617 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
618 ScrollDragStarted?.Invoke(this, eventArgs);
621 private void OnScrollDragEnded()
623 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
624 ScrollDragEnded?.Invoke(this, eventArgs);
627 private void OnScrollAnimationStarted()
629 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
630 ScrollAnimationStarted?.Invoke(this, eventArgs);
633 private void OnScrollAnimationEnded()
636 base.Remove(mInterruptTouchingChild);
638 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
639 ScrollAnimationEnded?.Invoke(this, eventArgs);
642 private void OnScroll()
644 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
645 Scrolling?.Invoke(this, eventArgs);
647 bool isHorizontal = ScrollingDirection == Direction.Horizontal;
648 float contentLength = isHorizontal ? ContentContainer.Size.Width : ContentContainer.Size.Height;
649 float currentPosition = isHorizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y;
651 scrollBar.Update(contentLength, Math.Abs(currentPosition));
652 CheckPreReachedTargetPosition();
655 private void CheckPreReachedTargetPosition()
657 // Check whether we reached pre-reached target position
659 ContentContainer.CurrentPosition.Y <= finalTargetPosition + NoticeAnimationEndBeforePosition &&
660 ContentContainer.CurrentPosition.Y >= finalTargetPosition - NoticeAnimationEndBeforePosition)
663 readyToNotice = false;
664 OnPreReachedTargetPosition(finalTargetPosition);
669 /// This helps developer who wants to know before scroll is reaching target position.
671 /// <param name="targetPosition">Index of item.</param>
672 /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
673 [EditorBrowsable(EditorBrowsableState.Never)]
674 protected virtual void OnPreReachedTargetPosition(float targetPosition)
679 private void StopScroll()
681 if (scrollAnimation != null)
683 if (scrollAnimation.State == Animation.States.Playing)
685 Debug.WriteLineIf(LayoutDebugScrollableBase, "StopScroll Animation Playing");
686 scrollAnimation.Stop(Animation.EndActions.Cancel);
687 OnScrollAnimationEnded();
689 scrollAnimation.Clear();
693 private void AnimateChildTo(int duration, float axisPosition)
695 Debug.WriteLineIf(LayoutDebugScrollableBase, "AnimationTo Animation Duration:" + duration + " Destination:" + axisPosition);
696 finalTargetPosition = axisPosition;
698 StopScroll(); // Will replace previous animation so will stop existing one.
700 if (scrollAnimation == null)
702 scrollAnimation = new Animation();
703 scrollAnimation.Finished += ScrollAnimationFinished;
706 scrollAnimation.Duration = duration;
707 scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.EaseOutSquare);
708 scrollAnimation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", axisPosition, ScrollAlphaFunction);
710 OnScrollAnimationStarted();
711 scrollAnimation.Play();
715 /// Scroll to specific position with or without animation.
717 /// <param name="position">Destination.</param>
718 /// <param name="animate">Scroll with or without animation</param>
719 /// <since_tizen> 8 </since_tizen>
720 public void ScrollTo(float position, bool animate)
722 float currentPositionX = ContentContainer.CurrentPosition.X != 0 ? ContentContainer.CurrentPosition.X : ContentContainer.Position.X;
723 float currentPositionY = ContentContainer.CurrentPosition.Y != 0 ? ContentContainer.CurrentPosition.Y : ContentContainer.Position.Y;
724 float delta = ScrollingDirection == Direction.Horizontal ? currentPositionX : currentPositionY;
725 // The argument position is the new pan position. So the new position of ScrollableBase becomes (-position).
726 // To move ScrollableBase's position to (-position), it moves by (-position - currentPosition).
727 delta = -position - delta;
729 ScrollBy(delta, animate);
732 private float BoundScrollPosition(float targetPosition)
734 if (ScrollAvailableArea != null)
736 float minScrollPosition = ScrollAvailableArea.X;
737 float maxScrollPosition = ScrollAvailableArea.Y;
739 targetPosition = Math.Min(-minScrollPosition, targetPosition);
740 targetPosition = Math.Max(-maxScrollPosition, targetPosition);
744 targetPosition = Math.Min(0, targetPosition);
745 targetPosition = Math.Max(-maxScrollDistance, targetPosition);
748 return targetPosition;
751 private void ScrollBy(float displacement, bool animate)
753 if (GetChildCount() == 0 || maxScrollDistance < 0)
758 float childCurrentPosition = (ScrollingDirection == Direction.Horizontal) ? ContentContainer.PositionX : ContentContainer.PositionY;
760 Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy childCurrentPosition:" + childCurrentPosition +
761 " displacement:" + displacement,
762 " maxScrollDistance:" + maxScrollDistance);
764 childTargetPosition = childCurrentPosition + displacement; // child current position + gesture displacement
767 Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy currentAxisPosition:" + childCurrentPosition + "childTargetPosition:" + childTargetPosition);
771 // Calculate scroll animaton duration
772 float scrollDistance = Math.Abs(displacement);
773 readyToNotice = true;
775 AnimateChildTo(ScrollDuration, BoundScrollPosition(AdjustTargetPositionOfScrollAnimation(BoundScrollPosition(childTargetPosition))));
779 finalTargetPosition = BoundScrollPosition(childTargetPosition);
781 // Set position of scrolling child without an animation
782 if (ScrollingDirection == Direction.Horizontal)
784 ContentContainer.PositionX = finalTargetPosition;
788 ContentContainer.PositionY = finalTargetPosition;
795 /// you can override it to clean-up your own resources.
797 /// <param name="type">DisposeTypes</param>
798 /// This will be public opened in tizen_5.5 after ACR done. Before ACR, need to be hidden as inhouse API.
799 [EditorBrowsable(EditorBrowsableState.Never)]
800 protected override void Dispose(DisposeTypes type)
807 if (type == DisposeTypes.Explicit)
811 if (mPanGestureDetector != null)
813 mPanGestureDetector.Detected -= OnPanGestureDetected;
814 mPanGestureDetector.Dispose();
815 mPanGestureDetector = null;
821 private float CalculateMaximumScrollDistance()
823 float scrollingChildLength = 0;
824 float scrollerLength = 0;
825 if (ScrollingDirection == Direction.Horizontal)
827 Debug.WriteLineIf(LayoutDebugScrollableBase, "Horizontal");
829 scrollingChildLength = ContentContainer.Size.Width;
830 scrollerLength = Size.Width;
834 Debug.WriteLineIf(LayoutDebugScrollableBase, "Vertical");
835 scrollingChildLength = ContentContainer.Size.Height;
836 scrollerLength = Size.Height;
839 Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy maxScrollDistance:" + (scrollingChildLength - scrollerLength) +
840 " parent length:" + scrollerLength +
841 " scrolling child length:" + scrollingChildLength);
843 return Math.Max(scrollingChildLength - scrollerLength, 0);
846 private void PageSnap(float velocity)
848 Debug.WriteLineIf(LayoutDebugScrollableBase, "PageSnap with pan candidate totalDisplacement:" + totalDisplacementForPan +
849 " currentPage[" + CurrentPage + "]");
851 //Increment current page if total displacement enough to warrant a page change.
852 if (Math.Abs(totalDisplacementForPan) > (mPageWidth * ratioOfScreenWidthToCompleteScroll))
854 if (totalDisplacementForPan < 0)
856 CurrentPage = Math.Min(Math.Max(Children.Count - 1, 0), ++CurrentPage);
860 CurrentPage = Math.Max(0, --CurrentPage);
863 else if (Math.Abs(velocity) > PageFlickThreshold)
867 CurrentPage = Math.Min(Math.Max(Children.Count - 1, 0), ++CurrentPage);
871 CurrentPage = Math.Max(0, --CurrentPage);
875 // Animate to new page or reposition to current page
876 float destinationX = -(Children[CurrentPage].Position.X + Children[CurrentPage].CurrentSize.Width / 2 - CurrentSize.Width / 2); // set to middle of current page
877 Debug.WriteLineIf(LayoutDebugScrollableBase, "Snapping to page[" + CurrentPage + "] to:" + destinationX + " from:" + ContentContainer.PositionX);
878 AnimateChildTo(ScrollDuration, destinationX);
881 private void OnPanGestureDetected(object source, PanGestureDetector.DetectedEventArgs e)
883 if (SnapToPage && scrollAnimation != null && scrollAnimation.State == Animation.States.Playing)
888 if (e.PanGesture.State == Gesture.StateType.Started)
890 readyToNotice = false;
891 base.Add(mInterruptTouchingChild);
892 Debug.WriteLineIf(LayoutDebugScrollableBase, "Gesture Start");
893 if (scrolling && !SnapToPage)
897 totalDisplacementForPan = 0.0f;
898 OnScrollDragStarted();
900 else if (e.PanGesture.State == Gesture.StateType.Continuing)
902 if (ScrollingDirection == Direction.Horizontal)
904 ScrollBy(e.PanGesture.Displacement.X, false);
905 totalDisplacementForPan += e.PanGesture.Displacement.X;
909 ScrollBy(e.PanGesture.Displacement.Y, false);
910 totalDisplacementForPan += e.PanGesture.Displacement.Y;
912 Debug.WriteLineIf(LayoutDebugScrollableBase, "OnPanGestureDetected Continue totalDisplacementForPan:" + totalDisplacementForPan);
914 else if (e.PanGesture.State == Gesture.StateType.Finished)
917 StopScroll(); // Will replace previous animation so will stop existing one.
919 if (scrollAnimation == null)
921 scrollAnimation = new Animation();
922 scrollAnimation.Finished += ScrollAnimationFinished;
925 float panVelocity = (ScrollingDirection == Direction.Horizontal) ? e.PanGesture.Velocity.X : e.PanGesture.Velocity.Y;
929 PageSnap(panVelocity);
935 float currentScrollPosition = (ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
936 scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.Linear);
937 scrollAnimation.Duration = 0;
938 scrollAnimation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", currentScrollPosition);
939 scrollAnimation.Play();
943 Decelerating(panVelocity, scrollAnimation);
947 totalDisplacementForPan = 0;
949 readyToNotice = true;
950 OnScrollAnimationStarted();
954 private float CustomScrollAlphaFunction(float progress)
956 if (panAnimationDelta == 0)
962 // Parameter "progress" is normalized value. We need to multiply target duration to calculate distance.
963 // Can get real distance using equation of deceleration (check Decelerating function)
964 // After get real distance, normalize it
965 float realDuration = progress * panAnimationDuration;
966 float realDistance = velocityOfLastPan * ((float)Math.Pow(decelerationRate, realDuration) - 1) / logValueOfDeceleration;
967 float result = Math.Min(realDistance / Math.Abs(panAnimationDelta), 1.0f);
973 /// you can override it to custom your decelerating
975 /// <param name="velocity">Velocity of current pan.</param>
976 /// <param name="animation">Scroll animation.</param>
977 [EditorBrowsable(EditorBrowsableState.Never)]
978 protected virtual void Decelerating(float velocity, Animation animation)
980 // Decelerating using deceleration equation ===========
982 // V : velocity (pixel per milisecond)
983 // V0 : initial velocity
984 // d : deceleration rate,
986 // X : final position after decelerating
987 // log : natural logarithm
989 // V(t) = V0 * d pow t;
990 // X(t) = V0 * (d pow t - 1) / log d; <-- Integrate the velocity function
991 // X(∞) = V0 * d / (1 - d); <-- Result using inifit T can be final position because T is tending to infinity.
993 // Because of final T is tending to inifity, we should use threshold value to finish.
994 // Final T = log(-threshold * log d / |V0| ) / log d;
996 velocityOfLastPan = Math.Abs(velocity);
998 float currentScrollPosition = -(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
999 panAnimationDelta = (velocityOfLastPan * decelerationRate) / (1 - decelerationRate);
1000 panAnimationDelta = velocity > 0 ? -panAnimationDelta : panAnimationDelta;
1002 float destination = -(panAnimationDelta + currentScrollPosition);
1003 float adjustDestination = AdjustTargetPositionOfScrollAnimation(destination);
1004 float maxPosition = ScrollAvailableArea != null ? ScrollAvailableArea.Y : maxScrollDistance;
1005 float minPosition = ScrollAvailableArea != null ? ScrollAvailableArea.X : 0;
1007 if (destination < -maxPosition || destination > minPosition)
1009 panAnimationDelta = velocity > 0 ? (currentScrollPosition - minPosition) : (maxPosition - currentScrollPosition);
1010 destination = velocity > 0 ? minPosition : -maxPosition;
1012 if (panAnimationDelta == 0)
1014 panAnimationDuration = 0.0f;
1018 panAnimationDuration = (float)Math.Log((panAnimationDelta * logValueOfDeceleration / velocityOfLastPan + 1), decelerationRate);
1021 Debug.WriteLineIf(LayoutDebugScrollableBase, "\n" +
1022 "OverRange======================= \n" +
1023 "[decelerationRate] " + decelerationRate + "\n" +
1024 "[logValueOfDeceleration] " + logValueOfDeceleration + "\n" +
1025 "[Velocity] " + velocityOfLastPan + "\n" +
1026 "[CurrentPosition] " + currentScrollPosition + "\n" +
1027 "[CandidateDelta] " + panAnimationDelta + "\n" +
1028 "[Destination] " + destination + "\n" +
1029 "[Duration] " + panAnimationDuration + "\n" +
1030 "================================ \n"
1035 panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
1037 if (adjustDestination != destination)
1039 destination = adjustDestination;
1040 panAnimationDelta = destination + currentScrollPosition;
1041 velocityOfLastPan = Math.Abs(panAnimationDelta * logValueOfDeceleration / ((float)Math.Pow(decelerationRate, panAnimationDuration) - 1));
1042 panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
1045 Debug.WriteLineIf(LayoutDebugScrollableBase, "\n" +
1046 "================================ \n" +
1047 "[decelerationRate] " + decelerationRate + "\n" +
1048 "[logValueOfDeceleration] " + logValueOfDeceleration + "\n" +
1049 "[Velocity] " + velocityOfLastPan + "\n" +
1050 "[CurrentPosition] " + currentScrollPosition + "\n" +
1051 "[CandidateDelta] " + panAnimationDelta + "\n" +
1052 "[Destination] " + destination + "\n" +
1053 "[Duration] " + panAnimationDuration + "\n" +
1054 "================================ \n"
1058 finalTargetPosition = destination;
1060 customScrollAlphaFunction = new UserAlphaFunctionDelegate(CustomScrollAlphaFunction);
1061 animation.DefaultAlphaFunction = new AlphaFunction(customScrollAlphaFunction);
1062 GC.KeepAlive(customScrollAlphaFunction);
1063 animation.Duration = (int)panAnimationDuration;
1064 animation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", destination);
1068 private void ScrollAnimationFinished(object sender, EventArgs e)
1070 OnScrollAnimationEnded();
1074 /// Adjust scrolling position by own scrolling rules.
1075 /// Override this function when developer wants to change destination of flicking.(e.g. always snap to center of item)
1077 /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
1078 [EditorBrowsable(EditorBrowsableState.Never)]
1079 protected virtual float AdjustTargetPositionOfScrollAnimation(float position)