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.
18 using Tizen.NUI.BaseComponents;
19 using System.Collections.Generic;
20 using System.ComponentModel;
21 using System.Diagnostics;
22 using System.Runtime.InteropServices;
23 using Tizen.NUI.Accessibility;
25 namespace Tizen.NUI.Components
28 /// ScrollEventArgs is a class to record scroll event arguments which will sent to user.
30 /// <since_tizen> 8 </since_tizen>
31 public class ScrollEventArgs : EventArgs
33 private Position position;
36 /// Default constructor.
38 /// <param name="position">Current scroll position</param>
39 /// <since_tizen> 8 </since_tizen>
40 public ScrollEventArgs(Position position)
42 this.position = position;
46 /// Current position of ContentContainer.
48 /// <since_tizen> 8 </since_tizen>
49 public Position Position
59 /// ScrollOutofBoundEventArgs is to record scroll out-of-bound event arguments which will be sent to user.
61 [EditorBrowsable(EditorBrowsableState.Never)]
62 public class ScrollOutOfBoundEventArgs : EventArgs
65 /// The bound to be scrolled out of.
67 [EditorBrowsable(EditorBrowsableState.Never)]
73 [EditorBrowsable(EditorBrowsableState.Never)]
79 [EditorBrowsable(EditorBrowsableState.Never)]
84 /// Default constructor.
86 /// <param name="bound">Current scrollable bound</param>
87 [EditorBrowsable(EditorBrowsableState.Never)]
88 public ScrollOutOfBoundEventArgs(Bound bound)
90 ScrollableBound = bound;
94 /// Current position of ContentContainer.
96 [EditorBrowsable(EditorBrowsableState.Never)]
97 public Bound ScrollableBound
104 /// This class provides a View that can scroll a single View with a layout. This View can be a nest of Views.
106 /// <since_tizen> 8 </since_tizen>
107 public class ScrollableBase : Control
109 static bool LayoutDebugScrollableBase = false; // Debug flag
110 private Direction mScrollingDirection = Direction.Vertical;
111 private bool mScrollEnabled = true;
112 private int mScrollDuration = 125;
113 private int mPageWidth = 0;
114 private float mPageFlickThreshold = 0.4f;
115 private float mScrollingEventThreshold = 0.00001f;
117 private class ScrollableBaseCustomLayout : LayoutGroup
119 protected override void OnMeasure(MeasureSpecification widthMeasureSpec, MeasureSpecification heightMeasureSpec)
121 Extents padding = Padding;
122 float totalHeight = padding.Top + padding.Bottom;
123 float totalWidth = padding.Start + padding.End;
125 MeasuredSize.StateType childWidthState = MeasuredSize.StateType.MeasuredSizeOK;
126 MeasuredSize.StateType childHeightState = MeasuredSize.StateType.MeasuredSizeOK;
128 Direction scrollingDirection = Direction.Vertical;
129 ScrollableBase scrollableBase = this.Owner as ScrollableBase;
132 scrollingDirection = scrollableBase.ScrollingDirection;
135 // measure child, should be a single scrolling child
136 foreach (LayoutItem childLayout in LayoutChildren)
138 if (childLayout != null)
141 // Use an Unspecified MeasureSpecification mode so scrolling child is not restricted to it's parents size in Height (for vertical scrolling)
142 // or Width for horizontal scrolling
143 MeasureSpecification unrestrictedMeasureSpec = new MeasureSpecification(heightMeasureSpec.Size, MeasureSpecification.ModeType.Unspecified);
145 if (scrollingDirection == Direction.Vertical)
147 MeasureChildWithMargins(childLayout, widthMeasureSpec, new LayoutLength(0), unrestrictedMeasureSpec, new LayoutLength(0)); // Height unrestricted by parent
151 MeasureChildWithMargins(childLayout, unrestrictedMeasureSpec, new LayoutLength(0), heightMeasureSpec, new LayoutLength(0)); // Width unrestricted by parent
154 float childWidth = childLayout.MeasuredWidth.Size.AsDecimal();
155 float childHeight = childLayout.MeasuredHeight.Size.AsDecimal();
157 // Determine the width and height needed by the children using their given position and size.
158 // Children could overlap so find the left most and right most child.
159 Position2D childPosition = childLayout.Owner.Position2D;
160 float childLeft = childPosition.X;
161 float childTop = childPosition.Y;
163 // Store current width and height needed to contain all children.
164 Extents childMargin = childLayout.Margin;
165 totalWidth = childWidth + childMargin.Start + childMargin.End;
166 totalHeight = childHeight + childMargin.Top + childMargin.Bottom;
168 if (childLayout.MeasuredWidth.State == MeasuredSize.StateType.MeasuredSizeTooSmall)
170 childWidthState = MeasuredSize.StateType.MeasuredSizeTooSmall;
172 if (childLayout.MeasuredHeight.State == MeasuredSize.StateType.MeasuredSizeTooSmall)
174 childHeightState = MeasuredSize.StateType.MeasuredSizeTooSmall;
180 MeasuredSize widthSizeAndState = ResolveSizeAndState(new LayoutLength(totalWidth + Padding.Start + Padding.End), widthMeasureSpec, MeasuredSize.StateType.MeasuredSizeOK);
181 MeasuredSize heightSizeAndState = ResolveSizeAndState(new LayoutLength(totalHeight + Padding.Top + Padding.Bottom), heightMeasureSpec, MeasuredSize.StateType.MeasuredSizeOK);
182 totalWidth = widthSizeAndState.Size.AsDecimal();
183 totalHeight = heightSizeAndState.Size.AsDecimal();
185 // Ensure layout respects it's given minimum size
186 totalWidth = Math.Max(totalWidth, SuggestedMinimumWidth.AsDecimal());
187 totalHeight = Math.Max(totalHeight, SuggestedMinimumHeight.AsDecimal());
189 widthSizeAndState.State = childWidthState;
190 heightSizeAndState.State = childHeightState;
192 SetMeasuredDimensions(ResolveSizeAndState(new LayoutLength(totalWidth + Padding.Start + Padding.End), widthMeasureSpec, childWidthState),
193 ResolveSizeAndState(new LayoutLength(totalHeight + Padding.Top + Padding.Bottom), heightMeasureSpec, childHeightState));
195 // Size of ScrollableBase is changed. Change Page width too.
196 scrollableBase.mPageWidth = (int)MeasuredWidth.Size.AsRoundedValue();
197 scrollableBase.OnScrollingChildRelayout(null , null);
200 protected override void OnLayout(bool changed, LayoutLength left, LayoutLength top, LayoutLength right, LayoutLength bottom)
202 foreach (LayoutItem childLayout in LayoutChildren)
204 if (childLayout != null)
206 LayoutLength childWidth = childLayout.MeasuredWidth.Size;
207 LayoutLength childHeight = childLayout.MeasuredHeight.Size;
209 Position2D childPosition = childLayout.Owner.Position2D;
210 Extents padding = Padding;
211 Extents childMargin = childLayout.Margin;
213 LayoutLength childLeft = new LayoutLength(childPosition.X + childMargin.Start + padding.Start);
214 LayoutLength childTop = new LayoutLength(childPosition.Y + childMargin.Top + padding.Top);
216 childLayout.Layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
220 } // ScrollableBaseCustomLayout
223 /// The direction axis to scroll.
225 /// <since_tizen> 8 </since_tizen>
226 public enum Direction
231 /// <since_tizen> 8 </since_tizen>
237 /// <since_tizen> 8 </since_tizen>
242 /// Scrolling direction mode.
243 /// Default is Vertical scrolling.
245 /// <since_tizen> 8 </since_tizen>
246 public Direction ScrollingDirection
250 return mScrollingDirection;
254 if (value != mScrollingDirection)
256 mScrollingDirection = value;
257 mPanGestureDetector.RemoveDirection(value == Direction.Horizontal ?
258 PanGestureDetector.DirectionVertical : PanGestureDetector.DirectionHorizontal);
259 mPanGestureDetector.AddDirection(value == Direction.Horizontal ?
260 PanGestureDetector.DirectionHorizontal : PanGestureDetector.DirectionVertical);
262 ContentContainer.WidthSpecification = mScrollingDirection == Direction.Vertical ?
263 LayoutParamPolicies.MatchParent : LayoutParamPolicies.WrapContent;
264 ContentContainer.HeightSpecification = mScrollingDirection == Direction.Vertical ?
265 LayoutParamPolicies.WrapContent : LayoutParamPolicies.MatchParent;
271 /// Enable or disable scrolling.
273 /// <since_tizen> 8 </since_tizen>
274 public bool ScrollEnabled
278 return mScrollEnabled;
282 if (value != mScrollEnabled)
284 mScrollEnabled = value;
287 mPanGestureDetector.Detected += OnPanGestureDetected;
291 mPanGestureDetector.Detected -= OnPanGestureDetected;
298 /// Pages mode, enables moving to the next or return to current page depending on pan displacement.
299 /// Default is false.
301 /// <since_tizen> 8 </since_tizen>
302 public bool SnapToPage { set; get; } = false;
305 /// Get current page.
306 /// Working property with SnapToPage property.
308 /// <since_tizen> 8 </since_tizen>
309 public int CurrentPage { get; private set; } = 0;
312 /// Duration of scroll animation.
313 /// Default value is 125ms.
315 /// <since_tizen> 8 </since_tizen>
316 public int ScrollDuration
320 mScrollDuration = value >= 0 ? value : mScrollDuration;
324 return mScrollDuration;
329 /// Scroll Available area.
331 /// <since_tizen> 8 </since_tizen>
332 public Vector2 ScrollAvailableArea { set; get; }
335 /// An event emitted when user starts dragging ScrollableBase, user can subscribe or unsubscribe to this event handler.<br />
337 /// <since_tizen> 8 </since_tizen>
338 public event EventHandler<ScrollEventArgs> ScrollDragStarted;
341 /// An event emitted when user stops dragging ScrollableBase, user can subscribe or unsubscribe to this event handler.<br />
343 /// <since_tizen> 8 </since_tizen>
344 public event EventHandler<ScrollEventArgs> ScrollDragEnded;
347 /// An event emitted when the scrolling slide animation starts, user can subscribe or unsubscribe to this event handler.<br />
349 /// <since_tizen> 8 </since_tizen>
350 public event EventHandler<ScrollEventArgs> ScrollAnimationStarted;
353 /// An event emitted when the scrolling slide animation ends, user can subscribe or unsubscribe to this event handler.<br />
355 /// <since_tizen> 8 </since_tizen>
356 public event EventHandler<ScrollEventArgs> ScrollAnimationEnded;
359 /// An event emitted when scrolling, user can subscribe or unsubscribe to this event handler.<br />
361 /// <since_tizen> 8 </since_tizen>
362 public event EventHandler<ScrollEventArgs> Scrolling;
365 /// An event emitted when scrolling out of bound, user can subscribe or unsubscribe to this event handler.<br />
367 [EditorBrowsable(EditorBrowsableState.Never)]
368 public event EventHandler<ScrollOutOfBoundEventArgs> ScrollOutOfBound;
371 /// Scrollbar for ScrollableBase.
373 /// <since_tizen> 8 </since_tizen>
374 public ScrollbarBase Scrollbar
384 scrollBar.Unparent();
388 if (scrollBar != null)
390 scrollBar.Name = "ScrollBar";
408 /// Always hide Scrollbar.
410 /// <since_tizen> 8 </since_tizen>
411 public bool HideScrollbar
415 return hideScrollbar;
419 hideScrollbar = value;
436 /// Container which has content of ScrollableBase.
438 /// <since_tizen> 8 </since_tizen>
439 public View ContentContainer { get; private set; }
442 /// Set the layout on this View. Replaces any existing Layout.
444 /// <since_tizen> 8 </since_tizen>
445 public new LayoutItem Layout
449 return ContentContainer.Layout;
453 ContentContainer.Layout = value;
454 if (ContentContainer.Layout != null)
456 ContentContainer.Layout.SetPositionByLayout = false;
462 /// List of children of Container.
464 /// <since_tizen> 8 </since_tizen>
465 public new List<View> Children
469 return ContentContainer.Children;
474 /// Deceleration rate of scrolling by finger.
475 /// Rate should be bigger than 0 and smaller than 1.
476 /// Default value is 0.998f;
478 /// <since_tizen> 8 </since_tizen>
479 public float DecelerationRate
483 return decelerationRate;
487 decelerationRate = (value < 1 && value > 0) ? value : decelerationRate;
488 logValueOfDeceleration = (float)Math.Log(value);
493 /// Threashold not to go infinit at the end of scrolling animation.
495 [EditorBrowsable(EditorBrowsableState.Never)]
496 public float DecelerationThreshold { get; set; } = 0.1f;
499 /// Scrolling event will be thrown when this amount of scroll positino is changed.
501 [EditorBrowsable(EditorBrowsableState.Never)]
502 public float ScrollingEventThreshold
506 return mScrollingEventThreshold;
510 if (mScrollingEventThreshold != value && value > 0)
512 ContentContainer.RemovePropertyNotification(propertyNotification);
513 propertyNotification = ContentContainer.AddPropertyNotification("position", PropertyCondition.Step(value));
514 propertyNotification.Notified += OnPropertyChanged;
515 mScrollingEventThreshold = value;
522 /// Page will be changed when velocity of panning is over threshold.
523 /// The unit of threshold is pixel per milisec.
525 /// <since_tizen> 8 </since_tizen>
526 public float PageFlickThreshold
530 return mPageFlickThreshold;
534 mPageFlickThreshold = value >= 0f ? value : mPageFlickThreshold;
539 /// Alphafunction for scroll animation.
541 [EditorBrowsable(EditorBrowsableState.Never)]
542 public AlphaFunction ScrollAlphaFunction { get; set; } = new AlphaFunction(AlphaFunction.BuiltinFunctions.Linear);
544 private bool hideScrollbar = true;
545 private float maxScrollDistance;
546 private float childTargetPosition = 0.0f;
547 private PanGestureDetector mPanGestureDetector;
548 private View mInterruptTouchingChild;
549 private ScrollbarBase scrollBar;
550 private bool scrolling = false;
551 private float ratioOfScreenWidthToCompleteScroll = 0.5f;
552 private float totalDisplacementForPan = 0.0f;
553 private Size previousContainerSize = new Size();
554 private Size previousSize = new Size();
555 private PropertyNotification propertyNotification;
556 private float noticeAnimationEndBeforePosition = 0.0f;
557 private bool readyToNotice = false;
560 /// Notice before animation is finished.
562 [EditorBrowsable(EditorBrowsableState.Never)]
563 // Let's consider more whether this needs to be set as protected.
564 public float NoticeAnimationEndBeforePosition { get => noticeAnimationEndBeforePosition; set => noticeAnimationEndBeforePosition = value; }
566 // Let's consider more whether this needs to be set as protected.
567 private float finalTargetPosition;
569 private Animation scrollAnimation;
570 // Declare user alpha function delegate
571 [UnmanagedFunctionPointer(CallingConvention.StdCall)]
572 private delegate float UserAlphaFunctionDelegate(float progress);
573 private UserAlphaFunctionDelegate customScrollAlphaFunction;
574 private float velocityOfLastPan = 0.0f;
575 private float panAnimationDuration = 0.0f;
576 private float panAnimationDelta = 0.0f;
577 private float logValueOfDeceleration = 0.0f;
578 private float decelerationRate = 0.0f;
580 private View verticalTopShadowView;
581 private View verticalBottomShadowView;
582 private const int verticalShadowScaleHeightLimit = 64 * 3;
583 private const int verticalShadowAnimationDuration = 300;
584 private Animation verticalShadowAnimation;
585 private bool isVerticalShadowShown = false;
586 private float startShowShadowDisplacement;
589 /// Default Constructor
591 /// <since_tizen> 8 </since_tizen>
592 public ScrollableBase() : base()
594 DecelerationRate = 0.998f;
596 base.Layout = new ScrollableBaseCustomLayout();
597 mPanGestureDetector = new PanGestureDetector();
598 mPanGestureDetector.Attach(this);
599 mPanGestureDetector.AddDirection(PanGestureDetector.DirectionVertical);
600 mPanGestureDetector.Detected += OnPanGestureDetected;
602 ClippingMode = ClippingModeType.ClipChildren;
604 //Default Scrolling child
605 ContentContainer = new View()
607 WidthSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.MatchParent : LayoutParamPolicies.WrapContent,
608 HeightSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.WrapContent : LayoutParamPolicies.MatchParent,
609 Layout = new AbsoluteLayout() { SetPositionByLayout = false },
611 ContentContainer.Relayout += OnScrollingChildRelayout;
612 propertyNotification = ContentContainer.AddPropertyNotification("position", PropertyCondition.Step(mScrollingEventThreshold));
613 propertyNotification.Notified += OnPropertyChanged;
614 base.Add(ContentContainer);
616 //Interrupt touching when panning is started
617 mInterruptTouchingChild = new View()
619 Size = new Size(Window.Instance.WindowSize),
620 BackgroundColor = Color.Transparent,
622 mInterruptTouchingChild.TouchEvent += OnIterruptTouchingChildTouched;
623 Scrollbar = new Scrollbar();
625 //Show vertical shadow on the top (or bottom) of the scrollable when panning down (or up).
626 verticalTopShadowView = new View
628 BackgroundImage = Tizen.NUI.StyleManager.FrameworkResourcePath + "nui_component_default_scroll_over_shooting_top.png",
631 PositionUsesPivotPoint = true,
632 ParentOrigin = NUI.ParentOrigin.TopCenter,
633 PivotPoint = NUI.PivotPoint.TopCenter,
635 verticalBottomShadowView = new View
637 BackgroundImage = Tizen.NUI.StyleManager.FrameworkResourcePath + "nui_component_default_scroll_over_shooting_bottom.png",
640 PositionUsesPivotPoint = true,
641 ParentOrigin = NUI.ParentOrigin.BottomCenter,
642 PivotPoint = NUI.PivotPoint.BottomCenter,
645 AccessibilityManager.Instance.SetAccessibilityAttribute(this, AccessibilityManager.AccessibilityAttribute.Trait, "ScrollableBase");
648 private bool OnIterruptTouchingChildTouched(object source, View.TouchEventArgs args)
650 if (args.Touch.GetState(0) == PointStateType.Down)
652 if (scrolling && !SnapToPage)
660 private void OnPropertyChanged(object source, PropertyNotification.NotifyEventArgs args)
666 /// Called after a child has been added to the owning view.
668 /// <param name="view">The child which has been added.</param>
669 /// <since_tizen> 8 </since_tizen>
670 public override void Add(View view)
672 ContentContainer.Add(view);
676 /// Called after a child has been removed from the owning view.
678 /// <param name="view">The child which has been removed.</param>
679 /// <since_tizen> 8 </since_tizen>
680 public override void Remove(View view)
682 if (SnapToPage && CurrentPage == Children.IndexOf(view) && CurrentPage == Children.Count - 1)
684 // Target View is current page and also last child.
685 // CurrentPage should be changed to previous page.
686 CurrentPage = Math.Max(0, CurrentPage - 1);
687 ScrollToIndex(CurrentPage);
690 ContentContainer.Remove(view);
693 private void OnScrollingChildRelayout(object source, EventArgs args)
695 // Size is changed. Calculate maxScrollDistance.
696 bool isSizeChanged = previousContainerSize.Width != ContentContainer.Size.Width || previousContainerSize.Height != ContentContainer.Size.Height
697 || previousSize.Width != Size.Width || previousSize.Height != Size.Height;
701 maxScrollDistance = CalculateMaximumScrollDistance();
705 previousContainerSize = ContentContainer.Size;
710 /// The composition of a Scrollbar can vary depending on how you use ScrollableBase.
711 /// Set the composition that will go into the ScrollableBase according to your ScrollableBase.
713 /// <since_tizen> 8 </since_tizen>
714 [EditorBrowsable(EditorBrowsableState.Never)]
715 protected virtual void SetScrollbar()
719 bool isHorizontal = ScrollingDirection == Direction.Horizontal;
720 float contentLength = isHorizontal ? ContentContainer.Size.Width : ContentContainer.Size.Height;
721 float viewportLength = isHorizontal ? Size.Width : Size.Height;
722 float currentPosition = isHorizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y;
723 Scrollbar.Initialize(contentLength, viewportLength, currentPosition, isHorizontal);
728 /// Scrolls to the item at the specified index.
730 /// <param name="index">Index of item.</param>
731 /// <since_tizen> 8 </since_tizen>
732 public void ScrollToIndex(int index)
734 if (ContentContainer.ChildCount - 1 < index || index < 0)
744 float targetPosition = Math.Min(ScrollingDirection == Direction.Vertical ? Children[index].Position.Y : Children[index].Position.X, maxScrollDistance);
745 AnimateChildTo(ScrollDuration, -targetPosition);
748 private void OnScrollDragStarted()
750 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
751 ScrollDragStarted?.Invoke(this, eventArgs);
754 private void OnScrollDragEnded()
756 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
757 ScrollDragEnded?.Invoke(this, eventArgs);
760 private void OnScrollAnimationStarted()
762 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
763 ScrollAnimationStarted?.Invoke(this, eventArgs);
766 private void OnScrollAnimationEnded()
769 base.Remove(mInterruptTouchingChild);
771 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
772 ScrollAnimationEnded?.Invoke(this, eventArgs);
775 private void OnScroll()
777 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
778 Scrolling?.Invoke(this, eventArgs);
780 bool isHorizontal = ScrollingDirection == Direction.Horizontal;
781 float contentLength = isHorizontal ? ContentContainer.Size.Width : ContentContainer.Size.Height;
782 float currentPosition = isHorizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y;
784 scrollBar?.Update(contentLength, Math.Abs(currentPosition));
785 CheckPreReachedTargetPosition();
788 private void CheckPreReachedTargetPosition()
790 // Check whether we reached pre-reached target position
792 ContentContainer.CurrentPosition.Y <= finalTargetPosition + NoticeAnimationEndBeforePosition &&
793 ContentContainer.CurrentPosition.Y >= finalTargetPosition - NoticeAnimationEndBeforePosition)
796 readyToNotice = false;
797 OnPreReachedTargetPosition(finalTargetPosition);
802 /// This helps developer who wants to know before scroll is reaching target position.
804 /// <param name="targetPosition">Index of item.</param>
805 /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
806 [EditorBrowsable(EditorBrowsableState.Never)]
807 protected virtual void OnPreReachedTargetPosition(float targetPosition)
812 private void StopScroll()
814 if (scrollAnimation != null)
816 if (scrollAnimation.State == Animation.States.Playing)
818 Debug.WriteLineIf(LayoutDebugScrollableBase, "StopScroll Animation Playing");
819 scrollAnimation.Stop(Animation.EndActions.Cancel);
820 OnScrollAnimationEnded();
822 scrollAnimation.Clear();
826 private void AnimateChildTo(int duration, float axisPosition)
828 Debug.WriteLineIf(LayoutDebugScrollableBase, "AnimationTo Animation Duration:" + duration + " Destination:" + axisPosition);
829 finalTargetPosition = axisPosition;
831 StopScroll(); // Will replace previous animation so will stop existing one.
833 if (scrollAnimation == null)
835 scrollAnimation = new Animation();
836 scrollAnimation.Finished += ScrollAnimationFinished;
839 scrollAnimation.Duration = duration;
840 scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.EaseOutSquare);
841 scrollAnimation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", axisPosition, ScrollAlphaFunction);
843 OnScrollAnimationStarted();
844 scrollAnimation.Play();
848 /// Scroll to specific position with or without animation.
850 /// <param name="position">Destination.</param>
851 /// <param name="animate">Scroll with or without animation</param>
852 /// <since_tizen> 8 </since_tizen>
853 public void ScrollTo(float position, bool animate)
855 float currentPositionX = ContentContainer.CurrentPosition.X != 0 ? ContentContainer.CurrentPosition.X : ContentContainer.Position.X;
856 float currentPositionY = ContentContainer.CurrentPosition.Y != 0 ? ContentContainer.CurrentPosition.Y : ContentContainer.Position.Y;
857 float delta = ScrollingDirection == Direction.Horizontal ? currentPositionX : currentPositionY;
858 // The argument position is the new pan position. So the new position of ScrollableBase becomes (-position).
859 // To move ScrollableBase's position to (-position), it moves by (-position - currentPosition).
860 delta = -position - delta;
862 ScrollBy(delta, animate);
865 private float BoundScrollPosition(float targetPosition)
867 if (ScrollAvailableArea != null)
869 float minScrollPosition = ScrollAvailableArea.X;
870 float maxScrollPosition = ScrollAvailableArea.Y;
872 targetPosition = Math.Min(-minScrollPosition, targetPosition);
873 targetPosition = Math.Max(-maxScrollPosition, targetPosition);
877 targetPosition = Math.Min(0, targetPosition);
878 targetPosition = Math.Max(-maxScrollDistance, targetPosition);
881 return targetPosition;
884 private void ScrollBy(float displacement, bool animate)
886 if (GetChildCount() == 0 || maxScrollDistance < 0)
891 float childCurrentPosition = (ScrollingDirection == Direction.Horizontal) ? ContentContainer.PositionX : ContentContainer.PositionY;
893 Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy childCurrentPosition:" + childCurrentPosition +
894 " displacement:" + displacement,
895 " maxScrollDistance:" + maxScrollDistance);
897 childTargetPosition = childCurrentPosition + displacement; // child current position + gesture displacement
900 Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy currentAxisPosition:" + childCurrentPosition + "childTargetPosition:" + childTargetPosition);
904 // Calculate scroll animaton duration
905 float scrollDistance = Math.Abs(displacement);
906 readyToNotice = true;
908 AnimateChildTo(ScrollDuration, BoundScrollPosition(AdjustTargetPositionOfScrollAnimation(BoundScrollPosition(childTargetPosition))));
912 finalTargetPosition = BoundScrollPosition(childTargetPosition);
914 // Set position of scrolling child without an animation
915 if (ScrollingDirection == Direction.Horizontal)
917 ContentContainer.PositionX = finalTargetPosition;
921 ContentContainer.PositionY = finalTargetPosition;
927 /// you can override it to clean-up your own resources.
929 /// <param name="type">DisposeTypes</param>
930 /// This will be public opened in tizen_5.5 after ACR done. Before ACR, need to be hidden as inhouse API.
931 [EditorBrowsable(EditorBrowsableState.Never)]
932 protected override void Dispose(DisposeTypes type)
939 if (type == DisposeTypes.Explicit)
941 StopVerticalShadowAnimation();
944 if (mPanGestureDetector != null)
946 mPanGestureDetector.Detected -= OnPanGestureDetected;
947 mPanGestureDetector.Dispose();
948 mPanGestureDetector = null;
951 propertyNotification.Dispose();
956 private float CalculateMaximumScrollDistance()
958 float scrollingChildLength = 0;
959 float scrollerLength = 0;
960 if (ScrollingDirection == Direction.Horizontal)
962 Debug.WriteLineIf(LayoutDebugScrollableBase, "Horizontal");
964 scrollingChildLength = ContentContainer.Size.Width;
965 scrollerLength = Size.Width;
969 Debug.WriteLineIf(LayoutDebugScrollableBase, "Vertical");
970 scrollingChildLength = ContentContainer.Size.Height;
971 scrollerLength = Size.Height;
974 Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy maxScrollDistance:" + (scrollingChildLength - scrollerLength) +
975 " parent length:" + scrollerLength +
976 " scrolling child length:" + scrollingChildLength);
978 return Math.Max(scrollingChildLength - scrollerLength, 0);
981 private void PageSnap(float velocity)
983 Debug.WriteLineIf(LayoutDebugScrollableBase, "PageSnap with pan candidate totalDisplacement:" + totalDisplacementForPan +
984 " currentPage[" + CurrentPage + "]");
986 //Increment current page if total displacement enough to warrant a page change.
987 if (Math.Abs(totalDisplacementForPan) > (mPageWidth * ratioOfScreenWidthToCompleteScroll))
989 if (totalDisplacementForPan < 0)
991 CurrentPage = Math.Min(Math.Max(Children.Count - 1, 0), ++CurrentPage);
995 CurrentPage = Math.Max(0, --CurrentPage);
998 else if (Math.Abs(velocity) > PageFlickThreshold)
1002 CurrentPage = Math.Min(Math.Max(Children.Count - 1, 0), ++CurrentPage);
1006 CurrentPage = Math.Max(0, --CurrentPage);
1010 // Animate to new page or reposition to current page
1011 float destinationX = -(Children[CurrentPage].Position.X + Children[CurrentPage].CurrentSize.Width / 2 - CurrentSize.Width / 2); // set to middle of current page
1012 Debug.WriteLineIf(LayoutDebugScrollableBase, "Snapping to page[" + CurrentPage + "] to:" + destinationX + " from:" + ContentContainer.PositionX);
1013 AnimateChildTo(ScrollDuration, destinationX);
1016 private void AttachShadowView()
1018 // stop animation if necessary.
1019 StopVerticalShadowAnimation();
1021 base.Add(verticalTopShadowView);
1022 base.Add(verticalBottomShadowView);
1024 verticalTopShadowView.Size = new Size(SizeWidth, 0.0f);
1025 verticalTopShadowView.Opacity = 1.0f;
1027 verticalBottomShadowView.Size = new Size(SizeWidth, 0.0f);
1028 verticalBottomShadowView.Opacity = 1.0f;
1030 // at the beginning, height of vertical shadow is 0, so it is invisible.
1031 isVerticalShadowShown = false;
1034 private void DragVerticalShadow(float displacement)
1036 if ((int)displacement > 0) // downwards
1038 // check if reaching at the top.
1039 if ((int)finalTargetPosition != 0)
1042 // save start displacement, and re-calculate displacement.
1043 if (!isVerticalShadowShown)
1045 startShowShadowDisplacement = displacement;
1046 OnScrollOutOfBound(ScrollOutOfBoundEventArgs.Bound.Top);
1048 isVerticalShadowShown = true;
1050 float newDisplacement = (int)displacement < (int)startShowShadowDisplacement ? 0 : displacement - startShowShadowDisplacement;
1052 // scale limit of width is 60%.
1053 float widthScale = newDisplacement / verticalShadowScaleHeightLimit;
1054 verticalTopShadowView.SizeWidth = widthScale > 0.6f ? SizeWidth * 0.4f : SizeWidth * (1.0f - widthScale);
1056 // scale limit of height is 300%.
1057 verticalTopShadowView.SizeHeight = newDisplacement > verticalShadowScaleHeightLimit ? verticalShadowScaleHeightLimit : newDisplacement;
1059 else if ((int)displacement < 0) // upwards
1061 // check if reaching at the bottom.
1062 if (-(int)finalTargetPosition != (int)maxScrollDistance)
1065 // save start displacement, and re-calculate displacement.
1066 if (!isVerticalShadowShown)
1068 startShowShadowDisplacement = displacement;
1069 OnScrollOutOfBound(ScrollOutOfBoundEventArgs.Bound.Bottom);
1071 isVerticalShadowShown = true;
1073 float newDisplacement = (int)startShowShadowDisplacement < (int)displacement ? 0 : startShowShadowDisplacement - displacement;
1075 // scale limit of width is 60%.
1076 float widthScale = newDisplacement / verticalShadowScaleHeightLimit;
1077 verticalBottomShadowView.SizeWidth = widthScale > 0.6f ? SizeWidth * 0.4f : SizeWidth * (1.0f - widthScale);
1079 // scale limit of height is 300%.
1080 verticalBottomShadowView.SizeHeight = newDisplacement > verticalShadowScaleHeightLimit ? verticalShadowScaleHeightLimit : newDisplacement;
1084 // if total displacement is 0, shadow would become invisible.
1085 isVerticalShadowShown = false;
1089 private void PlayVerticalShadowAnimation()
1091 // stop animation if necessary.
1092 StopVerticalShadowAnimation();
1094 if (verticalShadowAnimation == null)
1096 verticalShadowAnimation = new Animation(verticalShadowAnimationDuration);
1097 verticalShadowAnimation.Finished += OnVerticalShadowAnimationFinished;
1100 View targetView = totalDisplacementForPan < 0 ? verticalBottomShadowView : verticalTopShadowView;
1101 verticalShadowAnimation.AnimateTo(targetView, "SizeWidth", SizeWidth);
1102 verticalShadowAnimation.AnimateTo(targetView, "SizeHeight", 0.0f);
1103 verticalShadowAnimation.AnimateTo(targetView, "Opacity", 0.0f);
1104 verticalShadowAnimation.Play();
1107 private void StopVerticalShadowAnimation()
1109 if (verticalShadowAnimation == null || verticalShadowAnimation.State != Animation.States.Playing)
1112 verticalShadowAnimation.Stop(Animation.EndActions.Cancel);
1113 OnVerticalShadowAnimationFinished(null, null);
1114 verticalShadowAnimation.Clear();
1117 private void OnVerticalShadowAnimationFinished(object sender, EventArgs e)
1119 base.Remove(verticalTopShadowView);
1120 base.Remove(verticalBottomShadowView);
1122 verticalTopShadowView.Size = new Size(SizeWidth, 0.0f);
1123 verticalBottomShadowView.Size = new Size(SizeWidth, 0.0f);
1125 // after animation finished, height & opacity of vertical shadow both are 0, so it is invisible.
1126 isVerticalShadowShown = false;
1129 private void OnScrollOutOfBound(ScrollOutOfBoundEventArgs.Bound bound)
1131 ScrollOutOfBoundEventArgs args = new ScrollOutOfBoundEventArgs(bound);
1132 ScrollOutOfBound?.Invoke(this, args);
1135 private void OnPanGestureDetected(object source, PanGestureDetector.DetectedEventArgs e)
1137 OnPanGesture(e.PanGesture);
1140 private void OnPanGesture(PanGesture panGesture)
1142 if (SnapToPage && scrollAnimation != null && scrollAnimation.State == Animation.States.Playing)
1147 if (panGesture.State == Gesture.StateType.Started)
1149 readyToNotice = false;
1150 base.Add(mInterruptTouchingChild);
1152 Debug.WriteLineIf(LayoutDebugScrollableBase, "Gesture Start");
1153 if (scrolling && !SnapToPage)
1157 totalDisplacementForPan = 0.0f;
1158 OnScrollDragStarted();
1160 else if (panGesture.State == Gesture.StateType.Continuing)
1162 if (ScrollingDirection == Direction.Horizontal)
1164 ScrollBy(panGesture.Displacement.X, false);
1165 totalDisplacementForPan += panGesture.Displacement.X;
1169 // if vertical shadow is shown, does not scroll.
1170 if (!isVerticalShadowShown)
1172 ScrollBy(panGesture.Displacement.Y, false);
1174 totalDisplacementForPan += panGesture.Displacement.Y;
1175 DragVerticalShadow(totalDisplacementForPan);
1177 Debug.WriteLineIf(LayoutDebugScrollableBase, "OnPanGestureDetected Continue totalDisplacementForPan:" + totalDisplacementForPan);
1179 else if (panGesture.State == Gesture.StateType.Finished || panGesture.State == Gesture.StateType.Cancelled)
1181 PlayVerticalShadowAnimation();
1182 OnScrollDragEnded();
1183 StopScroll(); // Will replace previous animation so will stop existing one.
1185 if (scrollAnimation == null)
1187 scrollAnimation = new Animation();
1188 scrollAnimation.Finished += ScrollAnimationFinished;
1191 float panVelocity = (ScrollingDirection == Direction.Horizontal) ? panGesture.Velocity.X : panGesture.Velocity.Y;
1195 PageSnap(panVelocity);
1199 if (panVelocity == 0)
1201 float currentScrollPosition = (ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
1202 scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.Linear);
1203 scrollAnimation.Duration = 0;
1204 scrollAnimation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", currentScrollPosition);
1205 scrollAnimation.Play();
1209 Decelerating(panVelocity, scrollAnimation);
1213 totalDisplacementForPan = 0;
1215 readyToNotice = true;
1216 OnScrollAnimationStarted();
1220 internal override bool OnAccessibilityPan(PanGesture gestures)
1222 if (SnapToPage && scrollAnimation != null && scrollAnimation.State == Animation.States.Playing)
1227 OnPanGesture(gestures);
1231 private float CustomScrollAlphaFunction(float progress)
1233 if (panAnimationDelta == 0)
1239 // Parameter "progress" is normalized value. We need to multiply target duration to calculate distance.
1240 // Can get real distance using equation of deceleration (check Decelerating function)
1241 // After get real distance, normalize it
1242 float realDuration = progress * panAnimationDuration;
1243 float realDistance = velocityOfLastPan * ((float)Math.Pow(decelerationRate, realDuration) - 1) / logValueOfDeceleration;
1244 float result = Math.Min(realDistance / Math.Abs(panAnimationDelta), 1.0f);
1250 /// you can override it to custom your decelerating
1252 /// <param name="velocity">Velocity of current pan.</param>
1253 /// <param name="animation">Scroll animation.</param>
1254 [EditorBrowsable(EditorBrowsableState.Never)]
1255 protected virtual void Decelerating(float velocity, Animation animation)
1257 // Decelerating using deceleration equation ===========
1259 // V : velocity (pixel per milisecond)
1260 // V0 : initial velocity
1261 // d : deceleration rate,
1263 // X : final position after decelerating
1264 // log : natural logarithm
1266 // V(t) = V0 * d pow t;
1267 // X(t) = V0 * (d pow t - 1) / log d; <-- Integrate the velocity function
1268 // X(∞) = V0 * d / (1 - d); <-- Result using inifit T can be final position because T is tending to infinity.
1270 // Because of final T is tending to inifity, we should use threshold value to finish.
1271 // Final T = log(-threshold * log d / |V0| ) / log d;
1273 velocityOfLastPan = Math.Abs(velocity);
1275 float currentScrollPosition = -(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
1276 panAnimationDelta = (velocityOfLastPan * decelerationRate) / (1 - decelerationRate);
1277 panAnimationDelta = velocity > 0 ? -panAnimationDelta : panAnimationDelta;
1279 float destination = -(panAnimationDelta + currentScrollPosition);
1280 float adjustDestination = AdjustTargetPositionOfScrollAnimation(destination);
1281 float maxPosition = ScrollAvailableArea != null ? ScrollAvailableArea.Y : maxScrollDistance;
1282 float minPosition = ScrollAvailableArea != null ? ScrollAvailableArea.X : 0;
1284 if (destination < -maxPosition || destination > minPosition)
1286 panAnimationDelta = velocity > 0 ? (currentScrollPosition - minPosition) : (maxPosition - currentScrollPosition);
1287 destination = velocity > 0 ? minPosition : -maxPosition;
1289 if (panAnimationDelta == 0)
1291 panAnimationDuration = 0.0f;
1295 panAnimationDuration = (float)Math.Log((panAnimationDelta * logValueOfDeceleration / velocityOfLastPan + 1), decelerationRate);
1298 Debug.WriteLineIf(LayoutDebugScrollableBase, "\n" +
1299 "OverRange======================= \n" +
1300 "[decelerationRate] " + decelerationRate + "\n" +
1301 "[logValueOfDeceleration] " + logValueOfDeceleration + "\n" +
1302 "[Velocity] " + velocityOfLastPan + "\n" +
1303 "[CurrentPosition] " + currentScrollPosition + "\n" +
1304 "[CandidateDelta] " + panAnimationDelta + "\n" +
1305 "[Destination] " + destination + "\n" +
1306 "[Duration] " + panAnimationDuration + "\n" +
1307 "================================ \n"
1312 panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
1314 if (adjustDestination != destination)
1316 destination = adjustDestination;
1317 panAnimationDelta = destination + currentScrollPosition;
1318 velocityOfLastPan = Math.Abs(panAnimationDelta * logValueOfDeceleration / ((float)Math.Pow(decelerationRate, panAnimationDuration) - 1));
1319 panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
1322 Debug.WriteLineIf(LayoutDebugScrollableBase, "\n" +
1323 "================================ \n" +
1324 "[decelerationRate] " + decelerationRate + "\n" +
1325 "[logValueOfDeceleration] " + logValueOfDeceleration + "\n" +
1326 "[Velocity] " + velocityOfLastPan + "\n" +
1327 "[CurrentPosition] " + currentScrollPosition + "\n" +
1328 "[CandidateDelta] " + panAnimationDelta + "\n" +
1329 "[Destination] " + destination + "\n" +
1330 "[Duration] " + panAnimationDuration + "\n" +
1331 "================================ \n"
1335 finalTargetPosition = destination;
1337 customScrollAlphaFunction = new UserAlphaFunctionDelegate(CustomScrollAlphaFunction);
1338 animation.DefaultAlphaFunction = new AlphaFunction(customScrollAlphaFunction);
1339 GC.KeepAlive(customScrollAlphaFunction);
1340 animation.Duration = (int)panAnimationDuration;
1341 animation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", destination);
1345 private void ScrollAnimationFinished(object sender, EventArgs e)
1347 OnScrollAnimationEnded();
1351 /// Adjust scrolling position by own scrolling rules.
1352 /// Override this function when developer wants to change destination of flicking.(e.g. always snap to center of item)
1354 /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
1355 [EditorBrowsable(EditorBrowsableState.Never)]
1356 protected virtual float AdjustTargetPositionOfScrollAnimation(float position)
1362 /// Scroll position given to ScrollTo.
1363 /// This is the position in the opposite direction to the position of ContentContainer.
1365 /// <since_tizen> 8 </since_tizen>
1366 public Position ScrollPosition
1370 return new Position(-ContentContainer.Position);
1375 /// Current scroll position in the middle of ScrollTo animation.
1376 /// This is the position in the opposite direction to the current position of ContentContainer.
1378 /// <since_tizen> 8 </since_tizen>
1379 public Position ScrollCurrentPosition
1383 return new Position(-ContentContainer.CurrentPosition);
1388 /// Remove all children in ContentContainer.
1390 /// <param name="dispose">If true, removed child is disposed.</param>
1391 [EditorBrowsable(EditorBrowsableState.Never)]
1392 public void RemoveAllChildren(bool dispose = false)
1394 RecursiveRemoveChildren(ContentContainer, dispose);
1397 private void RecursiveRemoveChildren(View parent, bool dispose)
1403 int maxChild = (int)parent.GetChildCount();
1404 for (int i = maxChild - 1; i >= 0; --i)
1406 View child = parent.GetChildAt((uint)i);
1411 RecursiveRemoveChildren(child, dispose);
1412 parent.Remove(child);