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 MeasuredSize.StateType childWidthState = MeasuredSize.StateType.MeasuredSizeOK;
122 MeasuredSize.StateType childHeightState = MeasuredSize.StateType.MeasuredSizeOK;
124 Direction scrollingDirection = Direction.Vertical;
125 ScrollableBase scrollableBase = this.Owner as ScrollableBase;
126 if (scrollableBase != null)
128 scrollingDirection = scrollableBase.ScrollingDirection;
131 float totalWidth = 0.0f;
132 float totalHeight = 0.0f;
134 // measure child, should be a single scrolling child
135 foreach (LayoutItem childLayout in LayoutChildren)
137 if (childLayout != null && childLayout.Owner.Name == "ContentContainer")
140 // Use an Unspecified MeasureSpecification mode so scrolling child is not restricted to it's parents size in Height (for vertical scrolling)
141 // or Width for horizontal scrolling
142 if (scrollingDirection == Direction.Vertical)
144 MeasureSpecification unrestrictedMeasureSpec = new MeasureSpecification(heightMeasureSpec.Size, MeasureSpecification.ModeType.Unspecified);
145 MeasureChildWithMargins(childLayout, widthMeasureSpec, new LayoutLength(0), unrestrictedMeasureSpec, new LayoutLength(0)); // Height unrestricted by parent
149 MeasureSpecification unrestrictedMeasureSpec = new MeasureSpecification(widthMeasureSpec.Size, MeasureSpecification.ModeType.Unspecified);
150 MeasureChildWithMargins(childLayout, unrestrictedMeasureSpec, new LayoutLength(0), heightMeasureSpec, new LayoutLength(0)); // Width unrestricted by parent
153 totalWidth = (childLayout.MeasuredWidth.Size + (childLayout.Padding.Start + childLayout.Padding.End)).AsDecimal();
154 totalHeight = (childLayout.MeasuredHeight.Size + (childLayout.Padding.Top + childLayout.Padding.Bottom)).AsDecimal();
156 if (childLayout.MeasuredWidth.State == MeasuredSize.StateType.MeasuredSizeTooSmall)
158 childWidthState = MeasuredSize.StateType.MeasuredSizeTooSmall;
160 if (childLayout.MeasuredHeight.State == MeasuredSize.StateType.MeasuredSizeTooSmall)
162 childHeightState = MeasuredSize.StateType.MeasuredSizeTooSmall;
167 MeasuredSize widthSizeAndState = ResolveSizeAndState(new LayoutLength(totalWidth), widthMeasureSpec, MeasuredSize.StateType.MeasuredSizeOK);
168 MeasuredSize heightSizeAndState = ResolveSizeAndState(new LayoutLength(totalHeight), heightMeasureSpec, MeasuredSize.StateType.MeasuredSizeOK);
169 totalWidth = widthSizeAndState.Size.AsDecimal();
170 totalHeight = heightSizeAndState.Size.AsDecimal();
172 // Ensure layout respects it's given minimum size
173 totalWidth = Math.Max(totalWidth, SuggestedMinimumWidth.AsDecimal());
174 totalHeight = Math.Max(totalHeight, SuggestedMinimumHeight.AsDecimal());
176 widthSizeAndState.State = childWidthState;
177 heightSizeAndState.State = childHeightState;
179 SetMeasuredDimensions(ResolveSizeAndState(new LayoutLength(totalWidth), widthMeasureSpec, childWidthState),
180 ResolveSizeAndState(new LayoutLength(totalHeight), heightMeasureSpec, childHeightState));
182 // Size of ScrollableBase is changed. Change Page width too.
183 scrollableBase.mPageWidth = (int)MeasuredWidth.Size.AsRoundedValue();
184 scrollableBase.OnScrollingChildRelayout(null, null);
187 protected override void OnLayout(bool changed, LayoutLength left, LayoutLength top, LayoutLength right, LayoutLength bottom)
189 foreach (LayoutItem childLayout in LayoutChildren)
191 if (childLayout != null && childLayout.Owner.Name == "ContentContainer")
193 LayoutLength childWidth = childLayout.MeasuredWidth.Size;
194 LayoutLength childHeight = childLayout.MeasuredHeight.Size;
196 Position2D childPosition = childLayout.Owner.Position2D;
198 LayoutLength childLeft = new LayoutLength(childPosition.X);
199 LayoutLength childTop = new LayoutLength(childPosition.Y);
201 childLayout.Layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
205 } // ScrollableBaseCustomLayout
208 /// The direction axis to scroll.
210 /// <since_tizen> 8 </since_tizen>
211 public enum Direction
216 /// <since_tizen> 8 </since_tizen>
222 /// <since_tizen> 8 </since_tizen>
227 /// Scrolling direction mode.
228 /// Default is Vertical scrolling.
230 /// <since_tizen> 8 </since_tizen>
231 public Direction ScrollingDirection
235 return mScrollingDirection;
239 if (value != mScrollingDirection)
241 mScrollingDirection = value;
242 mPanGestureDetector.ClearAngles();
243 mPanGestureDetector.AddDirection(value == Direction.Horizontal ?
244 PanGestureDetector.DirectionHorizontal : PanGestureDetector.DirectionVertical);
246 ContentContainer.WidthSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.MatchParent : LayoutParamPolicies.WrapContent;
247 ContentContainer.HeightSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.WrapContent : LayoutParamPolicies.MatchParent;
253 /// Enable or disable scrolling.
255 /// <since_tizen> 8 </since_tizen>
256 public bool ScrollEnabled
260 return mScrollEnabled;
264 if (value != mScrollEnabled)
266 mScrollEnabled = value;
269 mPanGestureDetector.Detected += OnPanGestureDetected;
273 mPanGestureDetector.Detected -= OnPanGestureDetected;
280 /// Pages mode, enables moving to the next or return to current page depending on pan displacement.
281 /// Default is false.
283 /// <since_tizen> 8 </since_tizen>
284 public bool SnapToPage { set; get; } = false;
287 /// Get current page.
288 /// Working property with SnapToPage property.
290 /// <since_tizen> 8 </since_tizen>
291 public int CurrentPage { get; private set; } = 0;
294 /// Duration of scroll animation.
295 /// Default value is 125ms.
297 /// <since_tizen> 8 </since_tizen>
298 public int ScrollDuration
302 mScrollDuration = value >= 0 ? value : mScrollDuration;
306 return mScrollDuration;
311 /// Scroll Available area.
313 /// <since_tizen> 8 </since_tizen>
314 public Vector2 ScrollAvailableArea { set; get; }
317 /// An event emitted when user starts dragging ScrollableBase, user can subscribe or unsubscribe to this event handler.<br />
319 /// <since_tizen> 8 </since_tizen>
320 public event EventHandler<ScrollEventArgs> ScrollDragStarted;
323 /// An event emitted when user stops dragging ScrollableBase, user can subscribe or unsubscribe to this event handler.<br />
325 /// <since_tizen> 8 </since_tizen>
326 public event EventHandler<ScrollEventArgs> ScrollDragEnded;
329 /// An event emitted when the scrolling slide animation starts, user can subscribe or unsubscribe to this event handler.<br />
331 /// <since_tizen> 8 </since_tizen>
332 public event EventHandler<ScrollEventArgs> ScrollAnimationStarted;
335 /// An event emitted when the scrolling slide animation ends, user can subscribe or unsubscribe to this event handler.<br />
337 /// <since_tizen> 8 </since_tizen>
338 public event EventHandler<ScrollEventArgs> ScrollAnimationEnded;
341 /// An event emitted when scrolling, user can subscribe or unsubscribe to this event handler.<br />
343 /// <since_tizen> 8 </since_tizen>
344 public event EventHandler<ScrollEventArgs> Scrolling;
347 /// An event emitted when scrolling out of bound, user can subscribe or unsubscribe to this event handler.<br />
349 [EditorBrowsable(EditorBrowsableState.Never)]
350 public event EventHandler<ScrollOutOfBoundEventArgs> ScrollOutOfBound;
353 /// Scrollbar for ScrollableBase.
355 /// <since_tizen> 8 </since_tizen>
356 public ScrollbarBase Scrollbar
366 scrollBar.Unparent();
370 if (scrollBar != null)
372 scrollBar.Name = "ScrollBar";
390 /// Always hide Scrollbar.
392 /// <since_tizen> 8 </since_tizen>
393 public bool HideScrollbar
397 return hideScrollbar;
401 hideScrollbar = value;
418 /// Container which has content of ScrollableBase.
420 /// <since_tizen> 8 </since_tizen>
421 public View ContentContainer { get; private set; }
424 /// Set the layout on this View. Replaces any existing Layout.
426 /// <since_tizen> 8 </since_tizen>
427 public new LayoutItem Layout
431 return ContentContainer.Layout;
435 ContentContainer.Layout = value;
436 if (ContentContainer.Layout != null)
438 ContentContainer.Layout.SetPositionByLayout = false;
444 /// List of children of Container.
446 /// <since_tizen> 8 </since_tizen>
447 public new List<View> Children
451 return ContentContainer.Children;
456 /// Deceleration rate of scrolling by finger.
457 /// Rate should be bigger than 0 and smaller than 1.
458 /// Default value is 0.998f;
460 /// <since_tizen> 8 </since_tizen>
461 public float DecelerationRate
465 return decelerationRate;
469 decelerationRate = (value < 1 && value > 0) ? value : decelerationRate;
470 logValueOfDeceleration = (float)Math.Log(value);
475 /// Threashold not to go infinit at the end of scrolling animation.
477 [EditorBrowsable(EditorBrowsableState.Never)]
478 public float DecelerationThreshold { get; set; } = 0.1f;
481 /// Scrolling event will be thrown when this amount of scroll positino is changed.
483 [EditorBrowsable(EditorBrowsableState.Never)]
484 public float ScrollingEventThreshold
488 return mScrollingEventThreshold;
492 if (mScrollingEventThreshold != value && value > 0)
494 ContentContainer.RemovePropertyNotification(propertyNotification);
495 propertyNotification = ContentContainer.AddPropertyNotification("position", PropertyCondition.Step(value));
496 propertyNotification.Notified += OnPropertyChanged;
497 mScrollingEventThreshold = value;
503 /// Page will be changed when velocity of panning is over threshold.
504 /// The unit of threshold is pixel per milisec.
506 /// <since_tizen> 8 </since_tizen>
507 public float PageFlickThreshold
511 return mPageFlickThreshold;
515 mPageFlickThreshold = value >= 0f ? value : mPageFlickThreshold;
520 /// Padding for the ScrollableBase
522 [EditorBrowsable(EditorBrowsableState.Never)]
523 public Extents Padding
527 return ContentContainer.Padding;
531 ContentContainer.Padding = value;
536 /// Alphafunction for scroll animation.
538 [EditorBrowsable(EditorBrowsableState.Never)]
539 public AlphaFunction ScrollAlphaFunction { get; set; } = new AlphaFunction(AlphaFunction.BuiltinFunctions.Linear);
541 private bool hideScrollbar = true;
542 private float maxScrollDistance;
543 private float childTargetPosition = 0.0f;
544 private PanGestureDetector mPanGestureDetector;
545 private View mInterruptTouchingChild;
546 private ScrollbarBase scrollBar;
547 private bool scrolling = false;
548 private float ratioOfScreenWidthToCompleteScroll = 0.5f;
549 private float totalDisplacementForPan = 0.0f;
550 private Size previousContainerSize = new Size();
551 private Size previousSize = new Size();
552 private PropertyNotification propertyNotification;
553 private float noticeAnimationEndBeforePosition = 0.0f;
554 private bool readyToNotice = false;
557 /// Notice before animation is finished.
559 [EditorBrowsable(EditorBrowsableState.Never)]
560 // Let's consider more whether this needs to be set as protected.
561 public float NoticeAnimationEndBeforePosition
563 get => noticeAnimationEndBeforePosition;
564 set => noticeAnimationEndBeforePosition = value;
567 // Let's consider more whether this needs to be set as protected.
568 private float finalTargetPosition;
570 private Animation scrollAnimation;
571 // Declare user alpha function delegate
572 [UnmanagedFunctionPointer(CallingConvention.StdCall)]
573 private delegate float UserAlphaFunctionDelegate(float progress);
574 private UserAlphaFunctionDelegate customScrollAlphaFunction;
575 private float velocityOfLastPan = 0.0f;
576 private float panAnimationDuration = 0.0f;
577 private float panAnimationDelta = 0.0f;
578 private float logValueOfDeceleration = 0.0f;
579 private float decelerationRate = 0.0f;
581 private View verticalTopShadowView;
582 private View verticalBottomShadowView;
583 private const int verticalShadowScaleHeightLimit = 64 * 3;
584 private const int verticalShadowAnimationDuration = 300;
585 private Animation verticalShadowAnimation;
586 private bool isVerticalShadowShown = false;
587 private float startShowShadowDisplacement;
590 /// Default Constructor
592 /// <since_tizen> 8 </since_tizen>
593 public ScrollableBase() : base()
595 DecelerationRate = 0.998f;
597 base.Layout = new ScrollableBaseCustomLayout();
598 mPanGestureDetector = new PanGestureDetector();
599 mPanGestureDetector.Attach(this);
600 mPanGestureDetector.AddDirection(PanGestureDetector.DirectionVertical);
601 mPanGestureDetector.Detected += OnPanGestureDetected;
603 ClippingMode = ClippingModeType.ClipChildren;
605 //Default Scrolling child
606 ContentContainer = new View()
608 Name = "ContentContainer",
609 WidthSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.MatchParent : LayoutParamPolicies.WrapContent,
610 HeightSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.WrapContent : LayoutParamPolicies.MatchParent,
611 Layout = new AbsoluteLayout()
613 SetPositionByLayout = false
616 ContentContainer.Relayout += OnScrollingChildRelayout;
617 propertyNotification = ContentContainer.AddPropertyNotification("position", PropertyCondition.Step(mScrollingEventThreshold));
618 propertyNotification.Notified += OnPropertyChanged;
619 base.Add(ContentContainer);
621 //Interrupt touching when panning is started
622 mInterruptTouchingChild = new View()
624 Size = new Size(Window.Instance.WindowSize),
625 BackgroundColor = Color.Transparent,
627 mInterruptTouchingChild.TouchEvent += OnIterruptTouchingChildTouched;
628 Scrollbar = new Scrollbar();
630 //Show vertical shadow on the top (or bottom) of the scrollable when panning down (or up).
631 verticalTopShadowView = new View
633 BackgroundImage = Tizen.NUI.StyleManager.FrameworkResourcePath + "nui_component_default_scroll_over_shooting_top.png",
636 PositionUsesPivotPoint = true,
637 ParentOrigin = NUI.ParentOrigin.TopCenter,
638 PivotPoint = NUI.PivotPoint.TopCenter,
640 verticalBottomShadowView = new View
642 BackgroundImage = Tizen.NUI.StyleManager.FrameworkResourcePath + "nui_component_default_scroll_over_shooting_bottom.png",
645 PositionUsesPivotPoint = true,
646 ParentOrigin = NUI.ParentOrigin.BottomCenter,
647 PivotPoint = NUI.PivotPoint.BottomCenter,
650 AccessibilityManager.Instance.SetAccessibilityAttribute(this, AccessibilityManager.AccessibilityAttribute.Trait, "ScrollableBase");
653 private bool OnIterruptTouchingChildTouched(object source, View.TouchEventArgs args)
655 if (args.Touch.GetState(0) == PointStateType.Down)
657 if (scrolling && !SnapToPage)
665 private void OnPropertyChanged(object source, PropertyNotification.NotifyEventArgs args)
671 /// Called after a child has been added to the owning view.
673 /// <param name="view">The child which has been added.</param>
674 /// <since_tizen> 8 </since_tizen>
675 public override void Add(View view)
677 ContentContainer.Add(view);
681 /// Called after a child has been removed from the owning view.
683 /// <param name="view">The child which has been removed.</param>
684 /// <since_tizen> 8 </since_tizen>
685 public override void Remove(View view)
687 if (SnapToPage && CurrentPage == Children.IndexOf(view) && CurrentPage == Children.Count - 1)
689 // Target View is current page and also last child.
690 // CurrentPage should be changed to previous page.
691 CurrentPage = Math.Max(0, CurrentPage - 1);
692 ScrollToIndex(CurrentPage);
695 ContentContainer.Remove(view);
698 private void OnScrollingChildRelayout(object source, EventArgs args)
700 // Size is changed. Calculate maxScrollDistance.
701 bool isSizeChanged = previousContainerSize.Width != ContentContainer.Size.Width || previousContainerSize.Height != ContentContainer.Size.Height ||
702 previousSize.Width != Size.Width || previousSize.Height != Size.Height;
706 maxScrollDistance = CalculateMaximumScrollDistance();
710 previousContainerSize = ContentContainer.Size;
715 /// The composition of a Scrollbar can vary depending on how you use ScrollableBase.
716 /// Set the composition that will go into the ScrollableBase according to your ScrollableBase.
718 /// <since_tizen> 8 </since_tizen>
719 [EditorBrowsable(EditorBrowsableState.Never)]
720 protected virtual void SetScrollbar()
724 bool isHorizontal = ScrollingDirection == Direction.Horizontal;
725 float contentLength = isHorizontal ? ContentContainer.Size.Width : ContentContainer.Size.Height;
726 float viewportLength = isHorizontal ? Size.Width : Size.Height;
727 float currentPosition = isHorizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y;
728 Scrollbar.Initialize(contentLength, viewportLength, currentPosition, isHorizontal);
733 /// Scrolls to the item at the specified index.
735 /// <param name="index">Index of item.</param>
736 /// <since_tizen> 8 </since_tizen>
737 public void ScrollToIndex(int index)
739 if (ContentContainer.ChildCount - 1 < index || index < 0)
749 float targetPosition = Math.Min(ScrollingDirection == Direction.Vertical ? Children[index].Position.Y : Children[index].Position.X, maxScrollDistance);
750 AnimateChildTo(ScrollDuration, -targetPosition);
753 private void OnScrollDragStarted()
755 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
756 ScrollDragStarted?.Invoke(this, eventArgs);
759 private void OnScrollDragEnded()
761 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
762 ScrollDragEnded?.Invoke(this, eventArgs);
765 private void OnScrollAnimationStarted()
767 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
768 ScrollAnimationStarted?.Invoke(this, eventArgs);
771 private void OnScrollAnimationEnded()
774 base.Remove(mInterruptTouchingChild);
776 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
777 ScrollAnimationEnded?.Invoke(this, eventArgs);
780 private void OnScroll()
782 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
783 Scrolling?.Invoke(this, eventArgs);
785 bool isHorizontal = ScrollingDirection == Direction.Horizontal;
786 float contentLength = isHorizontal ? ContentContainer.Size.Width : ContentContainer.Size.Height;
787 float currentPosition = isHorizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y;
789 scrollBar?.Update(contentLength, Math.Abs(currentPosition));
790 CheckPreReachedTargetPosition();
793 private void CheckPreReachedTargetPosition()
795 // Check whether we reached pre-reached target position
797 ContentContainer.CurrentPosition.Y <= finalTargetPosition + NoticeAnimationEndBeforePosition &&
798 ContentContainer.CurrentPosition.Y >= finalTargetPosition - NoticeAnimationEndBeforePosition)
801 readyToNotice = false;
802 OnPreReachedTargetPosition(finalTargetPosition);
807 /// This helps developer who wants to know before scroll is reaching target position.
809 /// <param name="targetPosition">Index of item.</param>
810 /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
811 [EditorBrowsable(EditorBrowsableState.Never)]
812 protected virtual void OnPreReachedTargetPosition(float targetPosition)
817 private void StopScroll()
819 if (scrollAnimation != null)
821 if (scrollAnimation.State == Animation.States.Playing)
823 Debug.WriteLineIf(LayoutDebugScrollableBase, "StopScroll Animation Playing");
824 scrollAnimation.Stop(Animation.EndActions.Cancel);
825 OnScrollAnimationEnded();
827 scrollAnimation.Clear();
831 private void AnimateChildTo(int duration, float axisPosition)
833 Debug.WriteLineIf(LayoutDebugScrollableBase, "AnimationTo Animation Duration:" + duration + " Destination:" + axisPosition);
834 finalTargetPosition = axisPosition;
836 StopScroll(); // Will replace previous animation so will stop existing one.
838 if (scrollAnimation == null)
840 scrollAnimation = new Animation();
841 scrollAnimation.Finished += ScrollAnimationFinished;
844 scrollAnimation.Duration = duration;
845 scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.EaseOutSquare);
846 scrollAnimation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", axisPosition, ScrollAlphaFunction);
848 OnScrollAnimationStarted();
849 scrollAnimation.Play();
853 /// Scroll to specific position with or without animation.
855 /// <param name="position">Destination.</param>
856 /// <param name="animate">Scroll with or without animation</param>
857 /// <since_tizen> 8 </since_tizen>
858 public void ScrollTo(float position, bool animate)
860 float currentPositionX = ContentContainer.CurrentPosition.X != 0 ? ContentContainer.CurrentPosition.X : ContentContainer.Position.X;
861 float currentPositionY = ContentContainer.CurrentPosition.Y != 0 ? ContentContainer.CurrentPosition.Y : ContentContainer.Position.Y;
862 float delta = ScrollingDirection == Direction.Horizontal ? currentPositionX : currentPositionY;
863 // The argument position is the new pan position. So the new position of ScrollableBase becomes (-position).
864 // To move ScrollableBase's position to (-position), it moves by (-position - currentPosition).
865 delta = -position - delta;
867 ScrollBy(delta, animate);
870 private float BoundScrollPosition(float targetPosition)
872 if (ScrollAvailableArea != null)
874 float minScrollPosition = ScrollAvailableArea.X;
875 float maxScrollPosition = ScrollAvailableArea.Y;
877 targetPosition = Math.Min(-minScrollPosition, targetPosition);
878 targetPosition = Math.Max(-maxScrollPosition, targetPosition);
882 targetPosition = Math.Min(0, targetPosition);
883 targetPosition = Math.Max(-maxScrollDistance, targetPosition);
886 return targetPosition;
889 private void ScrollBy(float displacement, bool animate)
891 if (GetChildCount() == 0 || maxScrollDistance < 0)
896 float childCurrentPosition = (ScrollingDirection == Direction.Horizontal) ? ContentContainer.PositionX : ContentContainer.PositionY;
898 Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy childCurrentPosition:" + childCurrentPosition +
899 " displacement:" + displacement,
900 " maxScrollDistance:" + maxScrollDistance);
902 childTargetPosition = childCurrentPosition + displacement; // child current position + gesture displacement
904 Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy currentAxisPosition:" + childCurrentPosition + "childTargetPosition:" + childTargetPosition);
908 // Calculate scroll animaton duration
909 float scrollDistance = Math.Abs(displacement);
910 readyToNotice = true;
912 AnimateChildTo(ScrollDuration, BoundScrollPosition(AdjustTargetPositionOfScrollAnimation(BoundScrollPosition(childTargetPosition))));
916 finalTargetPosition = BoundScrollPosition(childTargetPosition);
918 // Set position of scrolling child without an animation
919 if (ScrollingDirection == Direction.Horizontal)
921 ContentContainer.PositionX = finalTargetPosition;
925 ContentContainer.PositionY = finalTargetPosition;
931 /// you can override it to clean-up your own resources.
933 /// <param name="type">DisposeTypes</param>
934 /// This will be public opened in tizen_5.5 after ACR done. Before ACR, need to be hidden as inhouse API.
935 [EditorBrowsable(EditorBrowsableState.Never)]
936 protected override void Dispose(DisposeTypes type)
943 if (type == DisposeTypes.Explicit)
945 StopVerticalShadowAnimation();
948 if (mPanGestureDetector != null)
950 mPanGestureDetector.Detected -= OnPanGestureDetected;
951 mPanGestureDetector.Dispose();
952 mPanGestureDetector = null;
955 propertyNotification.Dispose();
960 private float CalculateMaximumScrollDistance()
962 float scrollingChildLength = 0;
963 float scrollerLength = 0;
964 if (ScrollingDirection == Direction.Horizontal)
966 Debug.WriteLineIf(LayoutDebugScrollableBase, "Horizontal");
968 scrollingChildLength = ContentContainer.Size.Width;
969 scrollerLength = Size.Width;
973 Debug.WriteLineIf(LayoutDebugScrollableBase, "Vertical");
974 scrollingChildLength = ContentContainer.Size.Height;
975 scrollerLength = Size.Height;
978 Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy maxScrollDistance:" + (scrollingChildLength - scrollerLength) +
979 " parent length:" + scrollerLength +
980 " scrolling child length:" + scrollingChildLength);
982 return Math.Max(scrollingChildLength - scrollerLength, 0);
985 private void PageSnap(float velocity)
987 Debug.WriteLineIf(LayoutDebugScrollableBase, "PageSnap with pan candidate totalDisplacement:" + totalDisplacementForPan +
988 " currentPage[" + CurrentPage + "]");
990 //Increment current page if total displacement enough to warrant a page change.
991 if (Math.Abs(totalDisplacementForPan) > (mPageWidth * ratioOfScreenWidthToCompleteScroll))
993 if (totalDisplacementForPan < 0)
995 CurrentPage = Math.Min(Math.Max(Children.Count - 1, 0), ++CurrentPage);
999 CurrentPage = Math.Max(0, --CurrentPage);
1002 else if (Math.Abs(velocity) > PageFlickThreshold)
1006 CurrentPage = Math.Min(Math.Max(Children.Count - 1, 0), ++CurrentPage);
1010 CurrentPage = Math.Max(0, --CurrentPage);
1014 // Animate to new page or reposition to current page
1015 float destinationX = -(Children[CurrentPage].Position.X + Children[CurrentPage].CurrentSize.Width / 2 - CurrentSize.Width / 2); // set to middle of current page
1016 Debug.WriteLineIf(LayoutDebugScrollableBase, "Snapping to page[" + CurrentPage + "] to:" + destinationX + " from:" + ContentContainer.PositionX);
1017 AnimateChildTo(ScrollDuration, destinationX);
1020 private void AttachShadowView()
1022 if (ScrollingDirection != Direction.Vertical)
1025 // stop animation if necessary.
1026 StopVerticalShadowAnimation();
1028 base.Add(verticalTopShadowView);
1029 base.Add(verticalBottomShadowView);
1031 verticalTopShadowView.Size = new Size(SizeWidth, 0.0f);
1032 verticalTopShadowView.Opacity = 1.0f;
1034 verticalBottomShadowView.Size = new Size(SizeWidth, 0.0f);
1035 verticalBottomShadowView.Opacity = 1.0f;
1037 // at the beginning, height of vertical shadow is 0, so it is invisible.
1038 isVerticalShadowShown = false;
1041 private void DragVerticalShadow(float displacement)
1043 if (ScrollingDirection != Direction.Vertical)
1046 if ((int)displacement > 0) // downwards
1048 // check if reaching at the top.
1049 if ((int)finalTargetPosition != 0)
1052 // save start displacement, and re-calculate displacement.
1053 if (!isVerticalShadowShown)
1055 startShowShadowDisplacement = displacement;
1056 OnScrollOutOfBound(ScrollOutOfBoundEventArgs.Bound.Top);
1058 isVerticalShadowShown = true;
1060 float newDisplacement = (int)displacement < (int)startShowShadowDisplacement ? 0 : displacement - startShowShadowDisplacement;
1062 // scale limit of width is 60%.
1063 float widthScale = newDisplacement / verticalShadowScaleHeightLimit;
1064 verticalTopShadowView.SizeWidth = widthScale > 0.6f ? SizeWidth * 0.4f : SizeWidth * (1.0f - widthScale);
1066 // scale limit of height is 300%.
1067 verticalTopShadowView.SizeHeight = newDisplacement > verticalShadowScaleHeightLimit ? verticalShadowScaleHeightLimit : newDisplacement;
1069 else if ((int)displacement < 0) // upwards
1071 // check if reaching at the bottom.
1072 if (-(int)finalTargetPosition != (int)maxScrollDistance)
1075 // save start displacement, and re-calculate displacement.
1076 if (!isVerticalShadowShown)
1078 startShowShadowDisplacement = displacement;
1079 OnScrollOutOfBound(ScrollOutOfBoundEventArgs.Bound.Bottom);
1081 isVerticalShadowShown = true;
1083 float newDisplacement = (int)startShowShadowDisplacement < (int)displacement ? 0 : startShowShadowDisplacement - displacement;
1085 // scale limit of width is 60%.
1086 float widthScale = newDisplacement / verticalShadowScaleHeightLimit;
1087 verticalBottomShadowView.SizeWidth = widthScale > 0.6f ? SizeWidth * 0.4f : SizeWidth * (1.0f - widthScale);
1089 // scale limit of height is 300%.
1090 verticalBottomShadowView.SizeHeight = newDisplacement > verticalShadowScaleHeightLimit ? verticalShadowScaleHeightLimit : newDisplacement;
1094 // if total displacement is 0, shadow would become invisible.
1095 isVerticalShadowShown = false;
1099 private void PlayVerticalShadowAnimation()
1101 if (ScrollingDirection != Direction.Vertical)
1104 // stop animation if necessary.
1105 StopVerticalShadowAnimation();
1107 if (verticalShadowAnimation == null)
1109 verticalShadowAnimation = new Animation(verticalShadowAnimationDuration);
1110 verticalShadowAnimation.Finished += OnVerticalShadowAnimationFinished;
1113 View targetView = totalDisplacementForPan < 0 ? verticalBottomShadowView : verticalTopShadowView;
1114 verticalShadowAnimation.AnimateTo(targetView, "SizeWidth", SizeWidth);
1115 verticalShadowAnimation.AnimateTo(targetView, "SizeHeight", 0.0f);
1116 verticalShadowAnimation.AnimateTo(targetView, "Opacity", 0.0f);
1117 verticalShadowAnimation.Play();
1120 private void StopVerticalShadowAnimation()
1122 if (verticalShadowAnimation == null || verticalShadowAnimation.State != Animation.States.Playing)
1125 verticalShadowAnimation.Stop(Animation.EndActions.Cancel);
1126 OnVerticalShadowAnimationFinished(null, null);
1127 verticalShadowAnimation.Clear();
1130 private void OnVerticalShadowAnimationFinished(object sender, EventArgs e)
1132 base.Remove(verticalTopShadowView);
1133 base.Remove(verticalBottomShadowView);
1135 verticalTopShadowView.Size = new Size(SizeWidth, 0.0f);
1136 verticalBottomShadowView.Size = new Size(SizeWidth, 0.0f);
1138 // after animation finished, height & opacity of vertical shadow both are 0, so it is invisible.
1139 isVerticalShadowShown = false;
1142 private void OnScrollOutOfBound(ScrollOutOfBoundEventArgs.Bound bound)
1144 ScrollOutOfBoundEventArgs args = new ScrollOutOfBoundEventArgs(bound);
1145 ScrollOutOfBound?.Invoke(this, args);
1148 private void OnPanGestureDetected(object source, PanGestureDetector.DetectedEventArgs e)
1150 OnPanGesture(e.PanGesture);
1153 private void OnPanGesture(PanGesture panGesture)
1155 if (SnapToPage && scrollAnimation != null && scrollAnimation.State == Animation.States.Playing)
1160 if (panGesture.State == Gesture.StateType.Started)
1162 readyToNotice = false;
1163 base.Add(mInterruptTouchingChild);
1165 Debug.WriteLineIf(LayoutDebugScrollableBase, "Gesture Start");
1166 if (scrolling && !SnapToPage)
1170 totalDisplacementForPan = 0.0f;
1171 OnScrollDragStarted();
1173 else if (panGesture.State == Gesture.StateType.Continuing)
1175 if (ScrollingDirection == Direction.Horizontal)
1177 ScrollBy(panGesture.Displacement.X, false);
1178 totalDisplacementForPan += panGesture.Displacement.X;
1182 // if vertical shadow is shown, does not scroll.
1183 if (!isVerticalShadowShown)
1185 ScrollBy(panGesture.Displacement.Y, false);
1187 totalDisplacementForPan += panGesture.Displacement.Y;
1188 DragVerticalShadow(totalDisplacementForPan);
1190 Debug.WriteLineIf(LayoutDebugScrollableBase, "OnPanGestureDetected Continue totalDisplacementForPan:" + totalDisplacementForPan);
1192 else if (panGesture.State == Gesture.StateType.Finished || panGesture.State == Gesture.StateType.Cancelled)
1194 PlayVerticalShadowAnimation();
1195 OnScrollDragEnded();
1196 StopScroll(); // Will replace previous animation so will stop existing one.
1198 if (scrollAnimation == null)
1200 scrollAnimation = new Animation();
1201 scrollAnimation.Finished += ScrollAnimationFinished;
1204 float panVelocity = (ScrollingDirection == Direction.Horizontal) ? panGesture.Velocity.X : panGesture.Velocity.Y;
1208 PageSnap(panVelocity);
1212 if (panVelocity == 0)
1214 float currentScrollPosition = (ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
1215 scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.Linear);
1216 scrollAnimation.Duration = 0;
1217 scrollAnimation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", currentScrollPosition);
1218 scrollAnimation.Play();
1222 Decelerating(panVelocity, scrollAnimation);
1226 totalDisplacementForPan = 0;
1228 readyToNotice = true;
1229 OnScrollAnimationStarted();
1233 internal override bool OnAccessibilityPan(PanGesture gestures)
1235 if (SnapToPage && scrollAnimation != null && scrollAnimation.State == Animation.States.Playing)
1240 OnPanGesture(gestures);
1244 private float CustomScrollAlphaFunction(float progress)
1246 if (panAnimationDelta == 0)
1252 // Parameter "progress" is normalized value. We need to multiply target duration to calculate distance.
1253 // Can get real distance using equation of deceleration (check Decelerating function)
1254 // After get real distance, normalize it
1255 float realDuration = progress * panAnimationDuration;
1256 float realDistance = velocityOfLastPan * ((float)Math.Pow(decelerationRate, realDuration) - 1) / logValueOfDeceleration;
1257 float result = Math.Min(realDistance / Math.Abs(panAnimationDelta), 1.0f);
1263 /// you can override it to custom your decelerating
1265 /// <param name="velocity">Velocity of current pan.</param>
1266 /// <param name="animation">Scroll animation.</param>
1267 [EditorBrowsable(EditorBrowsableState.Never)]
1268 protected virtual void Decelerating(float velocity, Animation animation)
1270 // Decelerating using deceleration equation ===========
1272 // V : velocity (pixel per milisecond)
1273 // V0 : initial velocity
1274 // d : deceleration rate,
1276 // X : final position after decelerating
1277 // log : natural logarithm
1279 // V(t) = V0 * d pow t;
1280 // X(t) = V0 * (d pow t - 1) / log d; <-- Integrate the velocity function
1281 // X(∞) = V0 * d / (1 - d); <-- Result using inifit T can be final position because T is tending to infinity.
1283 // Because of final T is tending to inifity, we should use threshold value to finish.
1284 // Final T = log(-threshold * log d / |V0| ) / log d;
1286 velocityOfLastPan = Math.Abs(velocity);
1288 float currentScrollPosition = -(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
1289 panAnimationDelta = (velocityOfLastPan * decelerationRate) / (1 - decelerationRate);
1290 panAnimationDelta = velocity > 0 ? -panAnimationDelta : panAnimationDelta;
1292 float destination = -(panAnimationDelta + currentScrollPosition);
1293 float adjustDestination = AdjustTargetPositionOfScrollAnimation(destination);
1294 float maxPosition = ScrollAvailableArea != null ? ScrollAvailableArea.Y : maxScrollDistance;
1295 float minPosition = ScrollAvailableArea != null ? ScrollAvailableArea.X : 0;
1297 if (destination < -maxPosition || destination > minPosition)
1299 panAnimationDelta = velocity > 0 ? (currentScrollPosition - minPosition) : (maxPosition - currentScrollPosition);
1300 destination = velocity > 0 ? minPosition : -maxPosition;
1302 if (panAnimationDelta == 0)
1304 panAnimationDuration = 0.0f;
1308 panAnimationDuration = (float)Math.Log((panAnimationDelta * logValueOfDeceleration / velocityOfLastPan + 1), decelerationRate);
1311 Debug.WriteLineIf(LayoutDebugScrollableBase, "\n" +
1312 "OverRange======================= \n" +
1313 "[decelerationRate] " + decelerationRate + "\n" +
1314 "[logValueOfDeceleration] " + logValueOfDeceleration + "\n" +
1315 "[Velocity] " + velocityOfLastPan + "\n" +
1316 "[CurrentPosition] " + currentScrollPosition + "\n" +
1317 "[CandidateDelta] " + panAnimationDelta + "\n" +
1318 "[Destination] " + destination + "\n" +
1319 "[Duration] " + panAnimationDuration + "\n" +
1320 "================================ \n"
1325 panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
1327 if (adjustDestination != destination)
1329 destination = adjustDestination;
1330 panAnimationDelta = destination + currentScrollPosition;
1331 velocityOfLastPan = Math.Abs(panAnimationDelta * logValueOfDeceleration / ((float)Math.Pow(decelerationRate, panAnimationDuration) - 1));
1332 panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
1335 Debug.WriteLineIf(LayoutDebugScrollableBase, "\n" +
1336 "================================ \n" +
1337 "[decelerationRate] " + decelerationRate + "\n" +
1338 "[logValueOfDeceleration] " + logValueOfDeceleration + "\n" +
1339 "[Velocity] " + velocityOfLastPan + "\n" +
1340 "[CurrentPosition] " + currentScrollPosition + "\n" +
1341 "[CandidateDelta] " + panAnimationDelta + "\n" +
1342 "[Destination] " + destination + "\n" +
1343 "[Duration] " + panAnimationDuration + "\n" +
1344 "================================ \n"
1348 finalTargetPosition = destination;
1350 customScrollAlphaFunction = new UserAlphaFunctionDelegate(CustomScrollAlphaFunction);
1351 animation.DefaultAlphaFunction = new AlphaFunction(customScrollAlphaFunction);
1352 GC.KeepAlive(customScrollAlphaFunction);
1353 animation.Duration = (int)panAnimationDuration;
1354 animation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", destination);
1358 private void ScrollAnimationFinished(object sender, EventArgs e)
1360 OnScrollAnimationEnded();
1364 /// Adjust scrolling position by own scrolling rules.
1365 /// Override this function when developer wants to change destination of flicking.(e.g. always snap to center of item)
1367 /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
1368 [EditorBrowsable(EditorBrowsableState.Never)]
1369 protected virtual float AdjustTargetPositionOfScrollAnimation(float position)
1375 /// Scroll position given to ScrollTo.
1376 /// This is the position in the opposite direction to the position of ContentContainer.
1378 /// <since_tizen> 8 </since_tizen>
1379 public Position ScrollPosition
1383 return new Position(-ContentContainer.Position);
1388 /// Current scroll position in the middle of ScrollTo animation.
1389 /// This is the position in the opposite direction to the current position of ContentContainer.
1391 /// <since_tizen> 8 </since_tizen>
1392 public Position ScrollCurrentPosition
1396 return new Position(-ContentContainer.CurrentPosition);
1401 /// Remove all children in ContentContainer.
1403 /// <param name="dispose">If true, removed child is disposed.</param>
1404 [EditorBrowsable(EditorBrowsableState.Never)]
1405 public void RemoveAllChildren(bool dispose = false)
1407 RecursiveRemoveChildren(ContentContainer, dispose);
1410 private void RecursiveRemoveChildren(View parent, bool dispose)
1416 int maxChild = (int)parent.GetChildCount();
1417 for (int i = maxChild - 1; i >= 0; --i)
1419 View child = parent.GetChildAt((uint)i);
1424 RecursiveRemoveChildren(child, dispose);
1425 parent.Remove(child);