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;
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.AsDecimal();
154 totalHeight = childLayout.MeasuredHeight.Size.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 = LayoutParamPolicies.WrapContent;
247 ContentContainer.HeightSpecification = LayoutParamPolicies.WrapContent;
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 = true;
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 = LayoutParamPolicies.WrapContent,
610 HeightSpecification = LayoutParamPolicies.WrapContent,
611 Layout = new LinearLayout()
613 SetPositionByLayout = true
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 // stop animation if necessary.
1023 StopVerticalShadowAnimation();
1025 base.Add(verticalTopShadowView);
1026 base.Add(verticalBottomShadowView);
1028 verticalTopShadowView.Size = new Size(SizeWidth, 0.0f);
1029 verticalTopShadowView.Opacity = 1.0f;
1031 verticalBottomShadowView.Size = new Size(SizeWidth, 0.0f);
1032 verticalBottomShadowView.Opacity = 1.0f;
1034 // at the beginning, height of vertical shadow is 0, so it is invisible.
1035 isVerticalShadowShown = false;
1038 private void DragVerticalShadow(float displacement)
1040 if ((int)displacement > 0) // downwards
1042 // check if reaching at the top.
1043 if ((int)finalTargetPosition != 0)
1046 // save start displacement, and re-calculate displacement.
1047 if (!isVerticalShadowShown)
1049 startShowShadowDisplacement = displacement;
1050 OnScrollOutOfBound(ScrollOutOfBoundEventArgs.Bound.Top);
1052 isVerticalShadowShown = true;
1054 float newDisplacement = (int)displacement < (int)startShowShadowDisplacement ? 0 : displacement - startShowShadowDisplacement;
1056 // scale limit of width is 60%.
1057 float widthScale = newDisplacement / verticalShadowScaleHeightLimit;
1058 verticalTopShadowView.SizeWidth = widthScale > 0.6f ? SizeWidth * 0.4f : SizeWidth * (1.0f - widthScale);
1060 // scale limit of height is 300%.
1061 verticalTopShadowView.SizeHeight = newDisplacement > verticalShadowScaleHeightLimit ? verticalShadowScaleHeightLimit : newDisplacement;
1063 else if ((int)displacement < 0) // upwards
1065 // check if reaching at the bottom.
1066 if (-(int)finalTargetPosition != (int)maxScrollDistance)
1069 // save start displacement, and re-calculate displacement.
1070 if (!isVerticalShadowShown)
1072 startShowShadowDisplacement = displacement;
1073 OnScrollOutOfBound(ScrollOutOfBoundEventArgs.Bound.Bottom);
1075 isVerticalShadowShown = true;
1077 float newDisplacement = (int)startShowShadowDisplacement < (int)displacement ? 0 : startShowShadowDisplacement - displacement;
1079 // scale limit of width is 60%.
1080 float widthScale = newDisplacement / verticalShadowScaleHeightLimit;
1081 verticalBottomShadowView.SizeWidth = widthScale > 0.6f ? SizeWidth * 0.4f : SizeWidth * (1.0f - widthScale);
1083 // scale limit of height is 300%.
1084 verticalBottomShadowView.SizeHeight = newDisplacement > verticalShadowScaleHeightLimit ? verticalShadowScaleHeightLimit : newDisplacement;
1088 // if total displacement is 0, shadow would become invisible.
1089 isVerticalShadowShown = false;
1093 private void PlayVerticalShadowAnimation()
1095 // stop animation if necessary.
1096 StopVerticalShadowAnimation();
1098 if (verticalShadowAnimation == null)
1100 verticalShadowAnimation = new Animation(verticalShadowAnimationDuration);
1101 verticalShadowAnimation.Finished += OnVerticalShadowAnimationFinished;
1104 View targetView = totalDisplacementForPan < 0 ? verticalBottomShadowView : verticalTopShadowView;
1105 verticalShadowAnimation.AnimateTo(targetView, "SizeWidth", SizeWidth);
1106 verticalShadowAnimation.AnimateTo(targetView, "SizeHeight", 0.0f);
1107 verticalShadowAnimation.AnimateTo(targetView, "Opacity", 0.0f);
1108 verticalShadowAnimation.Play();
1111 private void StopVerticalShadowAnimation()
1113 if (verticalShadowAnimation == null || verticalShadowAnimation.State != Animation.States.Playing)
1116 verticalShadowAnimation.Stop(Animation.EndActions.Cancel);
1117 OnVerticalShadowAnimationFinished(null, null);
1118 verticalShadowAnimation.Clear();
1121 private void OnVerticalShadowAnimationFinished(object sender, EventArgs e)
1123 base.Remove(verticalTopShadowView);
1124 base.Remove(verticalBottomShadowView);
1126 verticalTopShadowView.Size = new Size(SizeWidth, 0.0f);
1127 verticalBottomShadowView.Size = new Size(SizeWidth, 0.0f);
1129 // after animation finished, height & opacity of vertical shadow both are 0, so it is invisible.
1130 isVerticalShadowShown = false;
1133 private void OnScrollOutOfBound(ScrollOutOfBoundEventArgs.Bound bound)
1135 ScrollOutOfBoundEventArgs args = new ScrollOutOfBoundEventArgs(bound);
1136 ScrollOutOfBound?.Invoke(this, args);
1139 private void OnPanGestureDetected(object source, PanGestureDetector.DetectedEventArgs e)
1141 OnPanGesture(e.PanGesture);
1144 private void OnPanGesture(PanGesture panGesture)
1146 if (SnapToPage && scrollAnimation != null && scrollAnimation.State == Animation.States.Playing)
1151 if (panGesture.State == Gesture.StateType.Started)
1153 readyToNotice = false;
1154 base.Add(mInterruptTouchingChild);
1156 Debug.WriteLineIf(LayoutDebugScrollableBase, "Gesture Start");
1157 if (scrolling && !SnapToPage)
1161 totalDisplacementForPan = 0.0f;
1162 OnScrollDragStarted();
1164 else if (panGesture.State == Gesture.StateType.Continuing)
1166 if (ScrollingDirection == Direction.Horizontal)
1168 ScrollBy(panGesture.Displacement.X, false);
1169 totalDisplacementForPan += panGesture.Displacement.X;
1173 // if vertical shadow is shown, does not scroll.
1174 if (!isVerticalShadowShown)
1176 ScrollBy(panGesture.Displacement.Y, false);
1178 totalDisplacementForPan += panGesture.Displacement.Y;
1179 DragVerticalShadow(totalDisplacementForPan);
1181 Debug.WriteLineIf(LayoutDebugScrollableBase, "OnPanGestureDetected Continue totalDisplacementForPan:" + totalDisplacementForPan);
1183 else if (panGesture.State == Gesture.StateType.Finished || panGesture.State == Gesture.StateType.Cancelled)
1185 PlayVerticalShadowAnimation();
1186 OnScrollDragEnded();
1187 StopScroll(); // Will replace previous animation so will stop existing one.
1189 if (scrollAnimation == null)
1191 scrollAnimation = new Animation();
1192 scrollAnimation.Finished += ScrollAnimationFinished;
1195 float panVelocity = (ScrollingDirection == Direction.Horizontal) ? panGesture.Velocity.X : panGesture.Velocity.Y;
1199 PageSnap(panVelocity);
1203 if (panVelocity == 0)
1205 float currentScrollPosition = (ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
1206 scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.Linear);
1207 scrollAnimation.Duration = 0;
1208 scrollAnimation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", currentScrollPosition);
1209 scrollAnimation.Play();
1213 Decelerating(panVelocity, scrollAnimation);
1217 totalDisplacementForPan = 0;
1219 readyToNotice = true;
1220 OnScrollAnimationStarted();
1224 internal override bool OnAccessibilityPan(PanGesture gestures)
1226 if (SnapToPage && scrollAnimation != null && scrollAnimation.State == Animation.States.Playing)
1231 OnPanGesture(gestures);
1235 private float CustomScrollAlphaFunction(float progress)
1237 if (panAnimationDelta == 0)
1243 // Parameter "progress" is normalized value. We need to multiply target duration to calculate distance.
1244 // Can get real distance using equation of deceleration (check Decelerating function)
1245 // After get real distance, normalize it
1246 float realDuration = progress * panAnimationDuration;
1247 float realDistance = velocityOfLastPan * ((float)Math.Pow(decelerationRate, realDuration) - 1) / logValueOfDeceleration;
1248 float result = Math.Min(realDistance / Math.Abs(panAnimationDelta), 1.0f);
1254 /// you can override it to custom your decelerating
1256 /// <param name="velocity">Velocity of current pan.</param>
1257 /// <param name="animation">Scroll animation.</param>
1258 [EditorBrowsable(EditorBrowsableState.Never)]
1259 protected virtual void Decelerating(float velocity, Animation animation)
1261 // Decelerating using deceleration equation ===========
1263 // V : velocity (pixel per milisecond)
1264 // V0 : initial velocity
1265 // d : deceleration rate,
1267 // X : final position after decelerating
1268 // log : natural logarithm
1270 // V(t) = V0 * d pow t;
1271 // X(t) = V0 * (d pow t - 1) / log d; <-- Integrate the velocity function
1272 // X(∞) = V0 * d / (1 - d); <-- Result using inifit T can be final position because T is tending to infinity.
1274 // Because of final T is tending to inifity, we should use threshold value to finish.
1275 // Final T = log(-threshold * log d / |V0| ) / log d;
1277 velocityOfLastPan = Math.Abs(velocity);
1279 float currentScrollPosition = -(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
1280 panAnimationDelta = (velocityOfLastPan * decelerationRate) / (1 - decelerationRate);
1281 panAnimationDelta = velocity > 0 ? -panAnimationDelta : panAnimationDelta;
1283 float destination = -(panAnimationDelta + currentScrollPosition);
1284 float adjustDestination = AdjustTargetPositionOfScrollAnimation(destination);
1285 float maxPosition = ScrollAvailableArea != null ? ScrollAvailableArea.Y : maxScrollDistance;
1286 float minPosition = ScrollAvailableArea != null ? ScrollAvailableArea.X : 0;
1288 if (destination < -maxPosition || destination > minPosition)
1290 panAnimationDelta = velocity > 0 ? (currentScrollPosition - minPosition) : (maxPosition - currentScrollPosition);
1291 destination = velocity > 0 ? minPosition : -maxPosition;
1293 if (panAnimationDelta == 0)
1295 panAnimationDuration = 0.0f;
1299 panAnimationDuration = (float)Math.Log((panAnimationDelta * logValueOfDeceleration / velocityOfLastPan + 1), decelerationRate);
1302 Debug.WriteLineIf(LayoutDebugScrollableBase, "\n" +
1303 "OverRange======================= \n" +
1304 "[decelerationRate] " + decelerationRate + "\n" +
1305 "[logValueOfDeceleration] " + logValueOfDeceleration + "\n" +
1306 "[Velocity] " + velocityOfLastPan + "\n" +
1307 "[CurrentPosition] " + currentScrollPosition + "\n" +
1308 "[CandidateDelta] " + panAnimationDelta + "\n" +
1309 "[Destination] " + destination + "\n" +
1310 "[Duration] " + panAnimationDuration + "\n" +
1311 "================================ \n"
1316 panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
1318 if (adjustDestination != destination)
1320 destination = adjustDestination;
1321 panAnimationDelta = destination + currentScrollPosition;
1322 velocityOfLastPan = Math.Abs(panAnimationDelta * logValueOfDeceleration / ((float)Math.Pow(decelerationRate, panAnimationDuration) - 1));
1323 panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
1326 Debug.WriteLineIf(LayoutDebugScrollableBase, "\n" +
1327 "================================ \n" +
1328 "[decelerationRate] " + decelerationRate + "\n" +
1329 "[logValueOfDeceleration] " + logValueOfDeceleration + "\n" +
1330 "[Velocity] " + velocityOfLastPan + "\n" +
1331 "[CurrentPosition] " + currentScrollPosition + "\n" +
1332 "[CandidateDelta] " + panAnimationDelta + "\n" +
1333 "[Destination] " + destination + "\n" +
1334 "[Duration] " + panAnimationDuration + "\n" +
1335 "================================ \n"
1339 finalTargetPosition = destination;
1341 customScrollAlphaFunction = new UserAlphaFunctionDelegate(CustomScrollAlphaFunction);
1342 animation.DefaultAlphaFunction = new AlphaFunction(customScrollAlphaFunction);
1343 GC.KeepAlive(customScrollAlphaFunction);
1344 animation.Duration = (int)panAnimationDuration;
1345 animation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", destination);
1349 private void ScrollAnimationFinished(object sender, EventArgs e)
1351 OnScrollAnimationEnded();
1355 /// Adjust scrolling position by own scrolling rules.
1356 /// Override this function when developer wants to change destination of flicking.(e.g. always snap to center of item)
1358 /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
1359 [EditorBrowsable(EditorBrowsableState.Never)]
1360 protected virtual float AdjustTargetPositionOfScrollAnimation(float position)
1366 /// Scroll position given to ScrollTo.
1367 /// This is the position in the opposite direction to the position of ContentContainer.
1369 /// <since_tizen> 8 </since_tizen>
1370 public Position ScrollPosition
1374 return new Position(-ContentContainer.Position);
1379 /// Current scroll position in the middle of ScrollTo animation.
1380 /// This is the position in the opposite direction to the current position of ContentContainer.
1382 /// <since_tizen> 8 </since_tizen>
1383 public Position ScrollCurrentPosition
1387 return new Position(-ContentContainer.CurrentPosition);
1392 /// Remove all children in ContentContainer.
1394 /// <param name="dispose">If true, removed child is disposed.</param>
1395 [EditorBrowsable(EditorBrowsableState.Never)]
1396 public void RemoveAllChildren(bool dispose = false)
1398 RecursiveRemoveChildren(ContentContainer, dispose);
1401 private void RecursiveRemoveChildren(View parent, bool dispose)
1407 int maxChild = (int)parent.GetChildCount();
1408 for (int i = maxChild - 1; i >= 0; --i)
1410 View child = parent.GetChildAt((uint)i);
1415 RecursiveRemoveChildren(child, dispose);
1416 parent.Remove(child);