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.
19 using Tizen.NUI.BaseComponents;
20 using System.Collections.Generic;
21 using System.ComponentModel;
22 using System.Diagnostics;
23 using System.Runtime.InteropServices;
25 using Tizen.NUI.Accessibility;
27 namespace Tizen.NUI.Components
30 /// ScrollEventArgs is a class to record scroll event arguments which will sent to user.
32 /// <since_tizen> 8 </since_tizen>
33 public class ScrollEventArgs : EventArgs
35 private Position position;
38 /// Default constructor.
40 /// <param name="position">Current scroll position</param>
41 /// <since_tizen> 8 </since_tizen>
42 public ScrollEventArgs(Position position)
44 this.position = position;
48 /// Current position of ContentContainer.
50 /// <since_tizen> 8 </since_tizen>
51 public Position Position
61 /// ScrollOutofBoundEventArgs is to record scroll out-of-bound event arguments which will be sent to user.
63 [EditorBrowsable(EditorBrowsableState.Never)]
64 public class ScrollOutOfBoundEventArgs : EventArgs
67 /// The bound to be scrolled out of.
69 [EditorBrowsable(EditorBrowsableState.Never)]
75 [EditorBrowsable(EditorBrowsableState.Never)]
81 [EditorBrowsable(EditorBrowsableState.Never)]
86 /// Default constructor.
88 /// <param name="bound">Current scrollable bound</param>
89 [EditorBrowsable(EditorBrowsableState.Never)]
90 public ScrollOutOfBoundEventArgs(Bound bound)
92 ScrollableBound = bound;
96 /// Current position of ContentContainer.
98 [EditorBrowsable(EditorBrowsableState.Never)]
99 public Bound ScrollableBound
106 /// This class provides a View that can scroll a single View with a layout. This View can be a nest of Views.
108 /// <since_tizen> 8 </since_tizen>
109 public class ScrollableBase : Control
111 static bool LayoutDebugScrollableBase = false; // Debug flag
112 private Direction mScrollingDirection = Direction.Vertical;
113 private bool mScrollEnabled = true;
114 private int mScrollDuration = 125;
115 private int mPageWidth = 0;
116 private float mPageFlickThreshold = 0.4f;
117 private float mScrollingEventThreshold = 0.00001f;
119 private class ScrollableBaseCustomLayout : LayoutGroup
121 protected override void OnMeasure(MeasureSpecification widthMeasureSpec, MeasureSpecification heightMeasureSpec)
123 MeasuredSize.StateType childWidthState = MeasuredSize.StateType.MeasuredSizeOK;
124 MeasuredSize.StateType childHeightState = MeasuredSize.StateType.MeasuredSizeOK;
126 Direction scrollingDirection = Direction.Vertical;
127 ScrollableBase scrollableBase = this.Owner as ScrollableBase;
130 scrollingDirection = scrollableBase.ScrollingDirection;
133 float totalWidth = 0.0f;
134 float totalHeight = 0.0f;
136 // measure child, should be a single scrolling child
137 foreach (LayoutItem childLayout in LayoutChildren)
139 if (childLayout != null && childLayout.Owner.Name == "ContentContainer")
142 // Use an Unspecified MeasureSpecification mode so scrolling child is not restricted to it's parents size in Height (for vertical scrolling)
143 // or Width for horizontal scrolling
144 if (scrollingDirection == Direction.Vertical)
146 MeasureSpecification unrestrictedMeasureSpec = new MeasureSpecification(heightMeasureSpec.Size, MeasureSpecification.ModeType.Unspecified);
147 MeasureChildWithMargins(childLayout, widthMeasureSpec, new LayoutLength(0), unrestrictedMeasureSpec, new LayoutLength(0)); // Height unrestricted by parent
151 MeasureSpecification unrestrictedMeasureSpec = new MeasureSpecification(widthMeasureSpec.Size, MeasureSpecification.ModeType.Unspecified);
152 MeasureChildWithMargins(childLayout, unrestrictedMeasureSpec, new LayoutLength(0), heightMeasureSpec, new LayoutLength(0)); // Width unrestricted by parent
155 totalWidth = childLayout.MeasuredWidth.Size.AsDecimal();
156 totalHeight = childLayout.MeasuredHeight.Size.AsDecimal();
158 if (childLayout.MeasuredWidth.State == MeasuredSize.StateType.MeasuredSizeTooSmall)
160 childWidthState = MeasuredSize.StateType.MeasuredSizeTooSmall;
162 if (childLayout.MeasuredHeight.State == MeasuredSize.StateType.MeasuredSizeTooSmall)
164 childHeightState = MeasuredSize.StateType.MeasuredSizeTooSmall;
169 MeasuredSize widthSizeAndState = ResolveSizeAndState(new LayoutLength(totalWidth), widthMeasureSpec, MeasuredSize.StateType.MeasuredSizeOK);
170 MeasuredSize heightSizeAndState = ResolveSizeAndState(new LayoutLength(totalHeight), heightMeasureSpec, MeasuredSize.StateType.MeasuredSizeOK);
171 totalWidth = widthSizeAndState.Size.AsDecimal();
172 totalHeight = heightSizeAndState.Size.AsDecimal();
174 // Ensure layout respects it's given minimum size
175 totalWidth = Math.Max(totalWidth, SuggestedMinimumWidth.AsDecimal());
176 totalHeight = Math.Max(totalHeight, SuggestedMinimumHeight.AsDecimal());
178 widthSizeAndState.State = childWidthState;
179 heightSizeAndState.State = childHeightState;
181 SetMeasuredDimensions(ResolveSizeAndState(new LayoutLength(totalWidth), widthMeasureSpec, childWidthState),
182 ResolveSizeAndState(new LayoutLength(totalHeight), heightMeasureSpec, childHeightState));
184 // Size of ScrollableBase is changed. Change Page width too.
185 scrollableBase.mPageWidth = (int)MeasuredWidth.Size.AsRoundedValue();
186 scrollableBase.OnScrollingChildRelayout(null, null);
189 protected override void OnLayout(bool changed, LayoutLength left, LayoutLength top, LayoutLength right, LayoutLength bottom)
191 foreach (LayoutItem childLayout in LayoutChildren)
193 if (childLayout != null && childLayout.Owner.Name == "ContentContainer")
195 LayoutLength childWidth = childLayout.MeasuredWidth.Size;
196 LayoutLength childHeight = childLayout.MeasuredHeight.Size;
198 Position2D childPosition = childLayout.Owner.Position2D;
200 LayoutLength childLeft = new LayoutLength(childPosition.X);
201 LayoutLength childTop = new LayoutLength(childPosition.Y);
203 childLayout.Layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
207 } // ScrollableBaseCustomLayout
210 /// The direction axis to scroll.
212 /// <since_tizen> 8 </since_tizen>
213 public enum Direction
218 /// <since_tizen> 8 </since_tizen>
224 /// <since_tizen> 8 </since_tizen>
229 /// Scrolling direction mode.
230 /// Default is Vertical scrolling.
232 /// <since_tizen> 8 </since_tizen>
233 public Direction ScrollingDirection
237 return mScrollingDirection;
241 if (value != mScrollingDirection)
243 mScrollingDirection = value;
244 mPanGestureDetector.ClearAngles();
245 mPanGestureDetector.AddDirection(value == Direction.Horizontal ?
246 PanGestureDetector.DirectionHorizontal : PanGestureDetector.DirectionVertical);
248 ContentContainer.WidthSpecification = LayoutParamPolicies.WrapContent;
249 ContentContainer.HeightSpecification = LayoutParamPolicies.WrapContent;
255 /// Enable or disable scrolling.
257 /// <since_tizen> 8 </since_tizen>
258 public bool ScrollEnabled
262 return mScrollEnabled;
266 if (value != mScrollEnabled)
268 mScrollEnabled = value;
271 mPanGestureDetector.Detected += OnPanGestureDetected;
275 mPanGestureDetector.Detected -= OnPanGestureDetected;
282 /// Pages mode, enables moving to the next or return to current page depending on pan displacement.
283 /// Default is false.
285 /// <since_tizen> 8 </since_tizen>
286 public bool SnapToPage { set; get; } = false;
289 /// Get current page.
290 /// Working property with SnapToPage property.
292 /// <since_tizen> 8 </since_tizen>
293 public int CurrentPage { get; private set; } = 0;
296 /// Duration of scroll animation.
297 /// Default value is 125ms.
299 /// <since_tizen> 8 </since_tizen>
300 public int ScrollDuration
304 mScrollDuration = value >= 0 ? value : mScrollDuration;
308 return mScrollDuration;
313 /// Scroll Available area.
315 /// <since_tizen> 8 </since_tizen>
316 public Vector2 ScrollAvailableArea { set; get; }
319 /// An event emitted when user starts dragging ScrollableBase, user can subscribe or unsubscribe to this event handler.<br />
321 /// <since_tizen> 8 </since_tizen>
322 public event EventHandler<ScrollEventArgs> ScrollDragStarted;
325 /// An event emitted when user stops dragging ScrollableBase, user can subscribe or unsubscribe to this event handler.<br />
327 /// <since_tizen> 8 </since_tizen>
328 public event EventHandler<ScrollEventArgs> ScrollDragEnded;
331 /// An event emitted when the scrolling slide animation starts, user can subscribe or unsubscribe to this event handler.<br />
333 /// <since_tizen> 8 </since_tizen>
334 public event EventHandler<ScrollEventArgs> ScrollAnimationStarted;
337 /// An event emitted when the scrolling slide animation ends, user can subscribe or unsubscribe to this event handler.<br />
339 /// <since_tizen> 8 </since_tizen>
340 public event EventHandler<ScrollEventArgs> ScrollAnimationEnded;
343 /// An event emitted when scrolling, user can subscribe or unsubscribe to this event handler.<br />
345 /// <since_tizen> 8 </since_tizen>
346 public event EventHandler<ScrollEventArgs> Scrolling;
349 /// An event emitted when scrolling out of bound, user can subscribe or unsubscribe to this event handler.<br />
351 [EditorBrowsable(EditorBrowsableState.Never)]
352 public event EventHandler<ScrollOutOfBoundEventArgs> ScrollOutOfBound;
355 /// Scrollbar for ScrollableBase.
357 /// <since_tizen> 8 </since_tizen>
358 public ScrollbarBase Scrollbar
368 scrollBar.Unparent();
372 if (scrollBar != null)
391 /// Always hide Scrollbar.
393 /// <since_tizen> 8 </since_tizen>
394 public bool HideScrollbar
398 return hideScrollbar;
402 hideScrollbar = value;
419 /// Container which has content of ScrollableBase.
421 /// <since_tizen> 8 </since_tizen>
422 public View ContentContainer { get; private set; }
425 /// Set the layout on this View. Replaces any existing Layout.
427 /// <since_tizen> 8 </since_tizen>
428 public new LayoutItem Layout
432 return ContentContainer.Layout;
436 ContentContainer.Layout = value;
437 if (ContentContainer.Layout != null)
439 ContentContainer.Layout.SetPositionByLayout = true;
445 /// List of children of Container.
447 /// <since_tizen> 8 </since_tizen>
448 public new List<View> Children
452 return ContentContainer.Children;
457 /// Deceleration rate of scrolling by finger.
458 /// Rate should be bigger than 0 and smaller than 1.
459 /// Default value is 0.998f;
461 /// <since_tizen> 8 </since_tizen>
462 public float DecelerationRate
466 return decelerationRate;
470 decelerationRate = (value < 1 && value > 0) ? value : decelerationRate;
471 logValueOfDeceleration = (float)Math.Log(value);
476 /// Threashold not to go infinit at the end of scrolling animation.
478 [EditorBrowsable(EditorBrowsableState.Never)]
479 public float DecelerationThreshold { get; set; } = 0.1f;
482 /// Scrolling event will be thrown when this amount of scroll positino is changed.
484 [EditorBrowsable(EditorBrowsableState.Never)]
485 public float ScrollingEventThreshold
489 return mScrollingEventThreshold;
493 if (mScrollingEventThreshold != value && value > 0)
495 ContentContainer.RemovePropertyNotification(propertyNotification);
496 propertyNotification = ContentContainer.AddPropertyNotification("position", PropertyCondition.Step(value));
497 propertyNotification.Notified += OnPropertyChanged;
498 mScrollingEventThreshold = value;
504 /// Page will be changed when velocity of panning is over threshold.
505 /// The unit of threshold is pixel per milisec.
507 /// <since_tizen> 8 </since_tizen>
508 public float PageFlickThreshold
512 return mPageFlickThreshold;
516 mPageFlickThreshold = value >= 0f ? value : mPageFlickThreshold;
521 /// Padding for the ScrollableBase
523 [EditorBrowsable(EditorBrowsableState.Never)]
524 public Extents Padding
528 return ContentContainer.Padding;
532 ContentContainer.Padding = value;
537 /// Alphafunction for scroll animation.
539 [EditorBrowsable(EditorBrowsableState.Never)]
540 public AlphaFunction ScrollAlphaFunction { get; set; } = new AlphaFunction(AlphaFunction.BuiltinFunctions.Linear);
542 private bool hideScrollbar = true;
543 private float maxScrollDistance;
544 private float childTargetPosition = 0.0f;
545 private PanGestureDetector mPanGestureDetector;
546 private View mInterruptTouchingChild;
547 private ScrollbarBase scrollBar;
548 private bool scrolling = false;
549 private float ratioOfScreenWidthToCompleteScroll = 0.5f;
550 private float totalDisplacementForPan = 0.0f;
551 private Size previousContainerSize = new Size();
552 private Size previousSize = new Size();
553 private PropertyNotification propertyNotification;
554 private float noticeAnimationEndBeforePosition = 0.0f;
555 private bool readyToNotice = false;
558 /// Notice before animation is finished.
560 [EditorBrowsable(EditorBrowsableState.Never)]
561 // Let's consider more whether this needs to be set as protected.
562 public float NoticeAnimationEndBeforePosition
564 get => noticeAnimationEndBeforePosition;
565 set => noticeAnimationEndBeforePosition = value;
568 // Let's consider more whether this needs to be set as protected.
569 private float finalTargetPosition;
571 private Animation scrollAnimation;
572 // Declare user alpha function delegate
573 [UnmanagedFunctionPointer(CallingConvention.StdCall)]
574 private delegate float UserAlphaFunctionDelegate(float progress);
575 private UserAlphaFunctionDelegate customScrollAlphaFunction;
576 private float velocityOfLastPan = 0.0f;
577 private float panAnimationDuration = 0.0f;
578 private float panAnimationDelta = 0.0f;
579 private float logValueOfDeceleration = 0.0f;
580 private float decelerationRate = 0.0f;
582 private View verticalTopShadowView;
583 private View verticalBottomShadowView;
584 private const int verticalShadowScaleHeightLimit = 64 * 3;
585 private const int verticalShadowAnimationDuration = 300;
586 private Animation verticalShadowAnimation;
587 private bool isVerticalShadowShown = false;
588 private float startShowShadowDisplacement;
591 /// Default Constructor
593 /// <since_tizen> 8 </since_tizen>
594 public ScrollableBase() : base()
596 DecelerationRate = 0.998f;
598 base.Layout = new ScrollableBaseCustomLayout();
599 mPanGestureDetector = new PanGestureDetector();
600 mPanGestureDetector.Attach(this);
601 mPanGestureDetector.AddDirection(PanGestureDetector.DirectionVertical);
602 mPanGestureDetector.Detected += OnPanGestureDetected;
604 ClippingMode = ClippingModeType.ClipChildren;
606 //Default Scrolling child
607 ContentContainer = new View()
609 Name = "ContentContainer",
610 WidthSpecification = LayoutParamPolicies.WrapContent,
611 HeightSpecification = LayoutParamPolicies.WrapContent,
612 Layout = new LinearLayout()
614 SetPositionByLayout = true
617 ContentContainer.Relayout += OnScrollingChildRelayout;
618 propertyNotification = ContentContainer.AddPropertyNotification("position", PropertyCondition.Step(mScrollingEventThreshold));
619 propertyNotification.Notified += OnPropertyChanged;
620 base.Add(ContentContainer);
622 //Interrupt touching when panning is started
623 mInterruptTouchingChild = new View()
625 Size = new Size(Window.Instance.WindowSize),
626 BackgroundColor = Color.Transparent,
628 mInterruptTouchingChild.TouchEvent += OnIterruptTouchingChildTouched;
629 Scrollbar = new Scrollbar();
631 //Show vertical shadow on the top (or bottom) of the scrollable when panning down (or up).
632 verticalTopShadowView = new View
634 BackgroundImage = Tizen.NUI.StyleManager.FrameworkResourcePath + "nui_component_default_scroll_over_shooting_top.png",
637 PositionUsesPivotPoint = true,
638 ParentOrigin = NUI.ParentOrigin.TopCenter,
639 PivotPoint = NUI.PivotPoint.TopCenter,
641 verticalBottomShadowView = new View
643 BackgroundImage = Tizen.NUI.StyleManager.FrameworkResourcePath + "nui_component_default_scroll_over_shooting_bottom.png",
646 PositionUsesPivotPoint = true,
647 ParentOrigin = NUI.ParentOrigin.BottomCenter,
648 PivotPoint = NUI.PivotPoint.BottomCenter,
651 AccessibilityManager.Instance.SetAccessibilityAttribute(this, AccessibilityManager.AccessibilityAttribute.Trait, "ScrollableBase");
654 private bool OnIterruptTouchingChildTouched(object source, View.TouchEventArgs args)
656 if (args.Touch.GetState(0) == PointStateType.Down)
658 if (scrolling && !SnapToPage)
666 private void OnPropertyChanged(object source, PropertyNotification.NotifyEventArgs args)
672 /// Called after a child has been added to the owning view.
674 /// <param name="view">The child which has been added.</param>
675 /// <since_tizen> 8 </since_tizen>
676 public override void Add(View view)
678 ContentContainer.Add(view);
682 /// Called after a child has been removed from the owning view.
684 /// <param name="view">The child which has been removed.</param>
685 /// <since_tizen> 8 </since_tizen>
686 public override void Remove(View view)
688 if (SnapToPage && CurrentPage == Children.IndexOf(view) && CurrentPage == Children.Count - 1)
690 // Target View is current page and also last child.
691 // CurrentPage should be changed to previous page.
692 CurrentPage = Math.Max(0, CurrentPage - 1);
693 ScrollToIndex(CurrentPage);
696 ContentContainer.Remove(view);
699 private void OnScrollingChildRelayout(object source, EventArgs args)
701 // Size is changed. Calculate maxScrollDistance.
702 bool isSizeChanged = previousContainerSize.Width != ContentContainer.Size.Width || previousContainerSize.Height != ContentContainer.Size.Height ||
703 previousSize.Width != Size.Width || previousSize.Height != Size.Height;
707 maxScrollDistance = CalculateMaximumScrollDistance();
711 previousContainerSize = ContentContainer.Size;
716 /// The composition of a Scrollbar can vary depending on how you use ScrollableBase.
717 /// Set the composition that will go into the ScrollableBase according to your ScrollableBase.
719 /// <since_tizen> 8 </since_tizen>
720 [EditorBrowsable(EditorBrowsableState.Never)]
721 protected virtual void SetScrollbar()
725 bool isHorizontal = ScrollingDirection == Direction.Horizontal;
726 float contentLength = isHorizontal ? ContentContainer.Size.Width : ContentContainer.Size.Height;
727 float viewportLength = isHorizontal ? Size.Width : Size.Height;
728 float currentPosition = isHorizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y;
729 Scrollbar.Initialize(contentLength, viewportLength, currentPosition, isHorizontal);
734 /// Scrolls to the item at the specified index.
736 /// <param name="index">Index of item.</param>
737 /// <since_tizen> 8 </since_tizen>
738 public void ScrollToIndex(int index)
740 if (ContentContainer.ChildCount - 1 < index || index < 0)
750 float targetPosition = Math.Min(ScrollingDirection == Direction.Vertical ? Children[index].Position.Y : Children[index].Position.X, maxScrollDistance);
751 AnimateChildTo(ScrollDuration, -targetPosition);
754 private void OnScrollDragStarted()
756 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
757 ScrollDragStarted?.Invoke(this, eventArgs);
760 private void OnScrollDragEnded()
762 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
763 ScrollDragEnded?.Invoke(this, eventArgs);
766 private void OnScrollAnimationStarted()
768 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
769 ScrollAnimationStarted?.Invoke(this, eventArgs);
772 private void OnScrollAnimationEnded()
775 base.Remove(mInterruptTouchingChild);
777 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
778 ScrollAnimationEnded?.Invoke(this, eventArgs);
781 private void OnScroll()
783 ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
784 Scrolling?.Invoke(this, eventArgs);
786 bool isHorizontal = ScrollingDirection == Direction.Horizontal;
787 float contentLength = isHorizontal ? ContentContainer.Size.Width : ContentContainer.Size.Height;
788 float currentPosition = isHorizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y;
790 scrollBar?.Update(contentLength, Math.Abs(currentPosition));
791 CheckPreReachedTargetPosition();
794 private void CheckPreReachedTargetPosition()
796 // Check whether we reached pre-reached target position
798 ContentContainer.CurrentPosition.Y <= finalTargetPosition + NoticeAnimationEndBeforePosition &&
799 ContentContainer.CurrentPosition.Y >= finalTargetPosition - NoticeAnimationEndBeforePosition)
802 readyToNotice = false;
803 OnPreReachedTargetPosition(finalTargetPosition);
808 /// This helps developer who wants to know before scroll is reaching target position.
810 /// <param name="targetPosition">Index of item.</param>
811 /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
812 [EditorBrowsable(EditorBrowsableState.Never)]
813 protected virtual void OnPreReachedTargetPosition(float targetPosition)
818 private void StopScroll()
820 if (scrollAnimation != null)
822 if (scrollAnimation.State == Animation.States.Playing)
824 Debug.WriteLineIf(LayoutDebugScrollableBase, "StopScroll Animation Playing");
825 scrollAnimation.Stop(Animation.EndActions.Cancel);
826 OnScrollAnimationEnded();
828 scrollAnimation.Clear();
832 private void AnimateChildTo(int duration, float axisPosition)
834 Debug.WriteLineIf(LayoutDebugScrollableBase, "AnimationTo Animation Duration:" + duration + " Destination:" + axisPosition);
835 finalTargetPosition = axisPosition;
837 StopScroll(); // Will replace previous animation so will stop existing one.
839 if (scrollAnimation == null)
841 scrollAnimation = new Animation();
842 scrollAnimation.Finished += ScrollAnimationFinished;
845 scrollAnimation.Duration = duration;
846 scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.EaseOutSquare);
847 scrollAnimation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", axisPosition, ScrollAlphaFunction);
849 OnScrollAnimationStarted();
850 scrollAnimation.Play();
854 /// Scroll to specific position with or without animation.
856 /// <param name="position">Destination.</param>
857 /// <param name="animate">Scroll with or without animation</param>
858 /// <since_tizen> 8 </since_tizen>
859 public void ScrollTo(float position, bool animate)
861 float currentPositionX = ContentContainer.CurrentPosition.X != 0 ? ContentContainer.CurrentPosition.X : ContentContainer.Position.X;
862 float currentPositionY = ContentContainer.CurrentPosition.Y != 0 ? ContentContainer.CurrentPosition.Y : ContentContainer.Position.Y;
863 float delta = ScrollingDirection == Direction.Horizontal ? currentPositionX : currentPositionY;
864 // The argument position is the new pan position. So the new position of ScrollableBase becomes (-position).
865 // To move ScrollableBase's position to (-position), it moves by (-position - currentPosition).
866 delta = -position - delta;
868 ScrollBy(delta, animate);
871 private float BoundScrollPosition(float targetPosition)
873 if (ScrollAvailableArea != null)
875 float minScrollPosition = ScrollAvailableArea.X;
876 float maxScrollPosition = ScrollAvailableArea.Y;
878 targetPosition = Math.Min(-minScrollPosition, targetPosition);
879 targetPosition = Math.Max(-maxScrollPosition, targetPosition);
883 targetPosition = Math.Min(0, targetPosition);
884 targetPosition = Math.Max(-maxScrollDistance, targetPosition);
887 return targetPosition;
890 private void ScrollBy(float displacement, bool animate)
892 if (GetChildCount() == 0 || maxScrollDistance < 0)
897 float childCurrentPosition = (ScrollingDirection == Direction.Horizontal) ? ContentContainer.PositionX : ContentContainer.PositionY;
899 Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy childCurrentPosition:" + childCurrentPosition +
900 " displacement:" + displacement,
901 " maxScrollDistance:" + maxScrollDistance);
903 childTargetPosition = childCurrentPosition + displacement; // child current position + gesture displacement
905 Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy currentAxisPosition:" + childCurrentPosition + "childTargetPosition:" + childTargetPosition);
909 // Calculate scroll animaton duration
910 float scrollDistance = Math.Abs(displacement);
911 readyToNotice = true;
913 AnimateChildTo(ScrollDuration, BoundScrollPosition(AdjustTargetPositionOfScrollAnimation(BoundScrollPosition(childTargetPosition))));
917 finalTargetPosition = BoundScrollPosition(childTargetPosition);
919 // Set position of scrolling child without an animation
920 if (ScrollingDirection == Direction.Horizontal)
922 ContentContainer.PositionX = finalTargetPosition;
926 ContentContainer.PositionY = finalTargetPosition;
932 /// you can override it to clean-up your own resources.
934 /// <param name="type">DisposeTypes</param>
935 /// This will be public opened in tizen_5.5 after ACR done. Before ACR, need to be hidden as inhouse API.
936 [EditorBrowsable(EditorBrowsableState.Never)]
937 protected override void Dispose(DisposeTypes type)
944 if (type == DisposeTypes.Explicit)
946 StopVerticalShadowAnimation();
949 if (mPanGestureDetector != null)
951 mPanGestureDetector.Detected -= OnPanGestureDetected;
952 mPanGestureDetector.Dispose();
953 mPanGestureDetector = null;
956 propertyNotification.Dispose();
961 private float CalculateMaximumScrollDistance()
963 float scrollingChildLength = 0;
964 float scrollerLength = 0;
965 if (ScrollingDirection == Direction.Horizontal)
967 Debug.WriteLineIf(LayoutDebugScrollableBase, "Horizontal");
969 scrollingChildLength = ContentContainer.Size.Width;
970 scrollerLength = Size.Width;
974 Debug.WriteLineIf(LayoutDebugScrollableBase, "Vertical");
975 scrollingChildLength = ContentContainer.Size.Height;
976 scrollerLength = Size.Height;
979 Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy maxScrollDistance:" + (scrollingChildLength - scrollerLength) +
980 " parent length:" + scrollerLength +
981 " scrolling child length:" + scrollingChildLength);
983 return Math.Max(scrollingChildLength - scrollerLength, 0);
986 private void PageSnap(float velocity)
988 Debug.WriteLineIf(LayoutDebugScrollableBase, "PageSnap with pan candidate totalDisplacement:" + totalDisplacementForPan +
989 " currentPage[" + CurrentPage + "]");
991 //Increment current page if total displacement enough to warrant a page change.
992 if (Math.Abs(totalDisplacementForPan) > (mPageWidth * ratioOfScreenWidthToCompleteScroll))
994 if (totalDisplacementForPan < 0)
996 CurrentPage = Math.Min(Math.Max(Children.Count - 1, 0), ++CurrentPage);
1000 CurrentPage = Math.Max(0, --CurrentPage);
1003 else if (Math.Abs(velocity) > PageFlickThreshold)
1007 CurrentPage = Math.Min(Math.Max(Children.Count - 1, 0), ++CurrentPage);
1011 CurrentPage = Math.Max(0, --CurrentPage);
1015 // Animate to new page or reposition to current page
1016 float destinationX = -(Children[CurrentPage].Position.X + Children[CurrentPage].CurrentSize.Width / 2 - CurrentSize.Width / 2); // set to middle of current page
1017 Debug.WriteLineIf(LayoutDebugScrollableBase, "Snapping to page[" + CurrentPage + "] to:" + destinationX + " from:" + ContentContainer.PositionX);
1018 AnimateChildTo(ScrollDuration, destinationX);
1021 private void AttachShadowView()
1023 // stop animation if necessary.
1024 StopVerticalShadowAnimation();
1026 base.Add(verticalTopShadowView);
1027 base.Add(verticalBottomShadowView);
1029 verticalTopShadowView.Size = new Size(SizeWidth, 0.0f);
1030 verticalTopShadowView.Opacity = 1.0f;
1032 verticalBottomShadowView.Size = new Size(SizeWidth, 0.0f);
1033 verticalBottomShadowView.Opacity = 1.0f;
1035 // at the beginning, height of vertical shadow is 0, so it is invisible.
1036 isVerticalShadowShown = false;
1039 private void DragVerticalShadow(float displacement)
1041 if ((int)displacement > 0) // downwards
1043 // check if reaching at the top.
1044 if ((int)finalTargetPosition != 0)
1047 // save start displacement, and re-calculate displacement.
1048 if (!isVerticalShadowShown)
1050 startShowShadowDisplacement = displacement;
1051 OnScrollOutOfBound(ScrollOutOfBoundEventArgs.Bound.Top);
1053 isVerticalShadowShown = true;
1055 float newDisplacement = (int)displacement < (int)startShowShadowDisplacement ? 0 : displacement - startShowShadowDisplacement;
1057 // scale limit of width is 60%.
1058 float widthScale = newDisplacement / verticalShadowScaleHeightLimit;
1059 verticalTopShadowView.SizeWidth = widthScale > 0.6f ? SizeWidth * 0.4f : SizeWidth * (1.0f - widthScale);
1061 // scale limit of height is 300%.
1062 verticalTopShadowView.SizeHeight = newDisplacement > verticalShadowScaleHeightLimit ? verticalShadowScaleHeightLimit : newDisplacement;
1064 else if ((int)displacement < 0) // upwards
1066 // check if reaching at the bottom.
1067 if (-(int)finalTargetPosition != (int)maxScrollDistance)
1070 // save start displacement, and re-calculate displacement.
1071 if (!isVerticalShadowShown)
1073 startShowShadowDisplacement = displacement;
1074 OnScrollOutOfBound(ScrollOutOfBoundEventArgs.Bound.Bottom);
1076 isVerticalShadowShown = true;
1078 float newDisplacement = (int)startShowShadowDisplacement < (int)displacement ? 0 : startShowShadowDisplacement - displacement;
1080 // scale limit of width is 60%.
1081 float widthScale = newDisplacement / verticalShadowScaleHeightLimit;
1082 verticalBottomShadowView.SizeWidth = widthScale > 0.6f ? SizeWidth * 0.4f : SizeWidth * (1.0f - widthScale);
1084 // scale limit of height is 300%.
1085 verticalBottomShadowView.SizeHeight = newDisplacement > verticalShadowScaleHeightLimit ? verticalShadowScaleHeightLimit : newDisplacement;
1089 // if total displacement is 0, shadow would become invisible.
1090 isVerticalShadowShown = false;
1094 private void PlayVerticalShadowAnimation()
1096 // stop animation if necessary.
1097 StopVerticalShadowAnimation();
1099 if (verticalShadowAnimation == null)
1101 verticalShadowAnimation = new Animation(verticalShadowAnimationDuration);
1102 verticalShadowAnimation.Finished += OnVerticalShadowAnimationFinished;
1105 View targetView = totalDisplacementForPan < 0 ? verticalBottomShadowView : verticalTopShadowView;
1106 verticalShadowAnimation.AnimateTo(targetView, "SizeWidth", SizeWidth);
1107 verticalShadowAnimation.AnimateTo(targetView, "SizeHeight", 0.0f);
1108 verticalShadowAnimation.AnimateTo(targetView, "Opacity", 0.0f);
1109 verticalShadowAnimation.Play();
1112 private void StopVerticalShadowAnimation()
1114 if (verticalShadowAnimation == null || verticalShadowAnimation.State != Animation.States.Playing)
1117 verticalShadowAnimation.Stop(Animation.EndActions.Cancel);
1118 OnVerticalShadowAnimationFinished(null, null);
1119 verticalShadowAnimation.Clear();
1122 private void OnVerticalShadowAnimationFinished(object sender, EventArgs e)
1124 base.Remove(verticalTopShadowView);
1125 base.Remove(verticalBottomShadowView);
1127 verticalTopShadowView.Size = new Size(SizeWidth, 0.0f);
1128 verticalBottomShadowView.Size = new Size(SizeWidth, 0.0f);
1130 // after animation finished, height & opacity of vertical shadow both are 0, so it is invisible.
1131 isVerticalShadowShown = false;
1134 private void OnScrollOutOfBound(ScrollOutOfBoundEventArgs.Bound bound)
1136 ScrollOutOfBoundEventArgs args = new ScrollOutOfBoundEventArgs(bound);
1137 ScrollOutOfBound?.Invoke(this, args);
1140 private void OnPanGestureDetected(object source, PanGestureDetector.DetectedEventArgs e)
1142 OnPanGesture(e.PanGesture);
1145 private void OnPanGesture(PanGesture panGesture)
1147 if (SnapToPage && scrollAnimation != null && scrollAnimation.State == Animation.States.Playing)
1152 if (panGesture.State == Gesture.StateType.Started)
1154 readyToNotice = false;
1155 base.Add(mInterruptTouchingChild);
1157 Debug.WriteLineIf(LayoutDebugScrollableBase, "Gesture Start");
1158 if (scrolling && !SnapToPage)
1162 totalDisplacementForPan = 0.0f;
1163 OnScrollDragStarted();
1165 else if (panGesture.State == Gesture.StateType.Continuing)
1167 if (ScrollingDirection == Direction.Horizontal)
1169 ScrollBy(panGesture.Displacement.X, false);
1170 totalDisplacementForPan += panGesture.Displacement.X;
1174 // if vertical shadow is shown, does not scroll.
1175 if (!isVerticalShadowShown)
1177 ScrollBy(panGesture.Displacement.Y, false);
1179 totalDisplacementForPan += panGesture.Displacement.Y;
1180 DragVerticalShadow(totalDisplacementForPan);
1182 Debug.WriteLineIf(LayoutDebugScrollableBase, "OnPanGestureDetected Continue totalDisplacementForPan:" + totalDisplacementForPan);
1184 else if (panGesture.State == Gesture.StateType.Finished || panGesture.State == Gesture.StateType.Cancelled)
1186 PlayVerticalShadowAnimation();
1187 OnScrollDragEnded();
1188 StopScroll(); // Will replace previous animation so will stop existing one.
1190 if (scrollAnimation == null)
1192 scrollAnimation = new Animation();
1193 scrollAnimation.Finished += ScrollAnimationFinished;
1196 float panVelocity = (ScrollingDirection == Direction.Horizontal) ? panGesture.Velocity.X : panGesture.Velocity.Y;
1200 PageSnap(panVelocity);
1204 if (panVelocity == 0)
1206 float currentScrollPosition = (ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
1207 scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.Linear);
1208 scrollAnimation.Duration = 0;
1209 scrollAnimation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", currentScrollPosition);
1210 scrollAnimation.Play();
1214 Decelerating(panVelocity, scrollAnimation);
1218 totalDisplacementForPan = 0;
1220 readyToNotice = true;
1221 OnScrollAnimationStarted();
1225 internal override bool OnAccessibilityPan(PanGesture gestures)
1227 if (SnapToPage && scrollAnimation != null && scrollAnimation.State == Animation.States.Playing)
1232 OnPanGesture(gestures);
1236 private float CustomScrollAlphaFunction(float progress)
1238 if (panAnimationDelta == 0)
1244 // Parameter "progress" is normalized value. We need to multiply target duration to calculate distance.
1245 // Can get real distance using equation of deceleration (check Decelerating function)
1246 // After get real distance, normalize it
1247 float realDuration = progress * panAnimationDuration;
1248 float realDistance = velocityOfLastPan * ((float)Math.Pow(decelerationRate, realDuration) - 1) / logValueOfDeceleration;
1249 float result = Math.Min(realDistance / Math.Abs(panAnimationDelta), 1.0f);
1255 /// you can override it to custom your decelerating
1257 /// <param name="velocity">Velocity of current pan.</param>
1258 /// <param name="animation">Scroll animation.</param>
1259 [EditorBrowsable(EditorBrowsableState.Never)]
1260 protected virtual void Decelerating(float velocity, Animation animation)
1262 // Decelerating using deceleration equation ===========
1264 // V : velocity (pixel per milisecond)
1265 // V0 : initial velocity
1266 // d : deceleration rate,
1268 // X : final position after decelerating
1269 // log : natural logarithm
1271 // V(t) = V0 * d pow t;
1272 // X(t) = V0 * (d pow t - 1) / log d; <-- Integrate the velocity function
1273 // X(∞) = V0 * d / (1 - d); <-- Result using inifit T can be final position because T is tending to infinity.
1275 // Because of final T is tending to inifity, we should use threshold value to finish.
1276 // Final T = log(-threshold * log d / |V0| ) / log d;
1278 velocityOfLastPan = Math.Abs(velocity);
1280 float currentScrollPosition = -(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
1281 panAnimationDelta = (velocityOfLastPan * decelerationRate) / (1 - decelerationRate);
1282 panAnimationDelta = velocity > 0 ? -panAnimationDelta : panAnimationDelta;
1284 float destination = -(panAnimationDelta + currentScrollPosition);
1285 float adjustDestination = AdjustTargetPositionOfScrollAnimation(destination);
1286 float maxPosition = ScrollAvailableArea != null ? ScrollAvailableArea.Y : maxScrollDistance;
1287 float minPosition = ScrollAvailableArea != null ? ScrollAvailableArea.X : 0;
1289 if (destination < -maxPosition || destination > minPosition)
1291 panAnimationDelta = velocity > 0 ? (currentScrollPosition - minPosition) : (maxPosition - currentScrollPosition);
1292 destination = velocity > 0 ? minPosition : -maxPosition;
1294 if (panAnimationDelta == 0)
1296 panAnimationDuration = 0.0f;
1300 panAnimationDuration = (float)Math.Log((panAnimationDelta * logValueOfDeceleration / velocityOfLastPan + 1), decelerationRate);
1303 Debug.WriteLineIf(LayoutDebugScrollableBase, "\n" +
1304 "OverRange======================= \n" +
1305 "[decelerationRate] " + decelerationRate + "\n" +
1306 "[logValueOfDeceleration] " + logValueOfDeceleration + "\n" +
1307 "[Velocity] " + velocityOfLastPan + "\n" +
1308 "[CurrentPosition] " + currentScrollPosition + "\n" +
1309 "[CandidateDelta] " + panAnimationDelta + "\n" +
1310 "[Destination] " + destination + "\n" +
1311 "[Duration] " + panAnimationDuration + "\n" +
1312 "================================ \n"
1317 panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
1319 if (adjustDestination != destination)
1321 destination = adjustDestination;
1322 panAnimationDelta = destination + currentScrollPosition;
1323 velocityOfLastPan = Math.Abs(panAnimationDelta * logValueOfDeceleration / ((float)Math.Pow(decelerationRate, panAnimationDuration) - 1));
1324 panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
1327 Debug.WriteLineIf(LayoutDebugScrollableBase, "\n" +
1328 "================================ \n" +
1329 "[decelerationRate] " + decelerationRate + "\n" +
1330 "[logValueOfDeceleration] " + logValueOfDeceleration + "\n" +
1331 "[Velocity] " + velocityOfLastPan + "\n" +
1332 "[CurrentPosition] " + currentScrollPosition + "\n" +
1333 "[CandidateDelta] " + panAnimationDelta + "\n" +
1334 "[Destination] " + destination + "\n" +
1335 "[Duration] " + panAnimationDuration + "\n" +
1336 "================================ \n"
1340 finalTargetPosition = destination;
1342 customScrollAlphaFunction = new UserAlphaFunctionDelegate(CustomScrollAlphaFunction);
1343 animation.DefaultAlphaFunction = new AlphaFunction(customScrollAlphaFunction);
1344 GC.KeepAlive(customScrollAlphaFunction);
1345 animation.Duration = (int)panAnimationDuration;
1346 animation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", destination);
1350 private void ScrollAnimationFinished(object sender, EventArgs e)
1352 OnScrollAnimationEnded();
1356 /// Adjust scrolling position by own scrolling rules.
1357 /// Override this function when developer wants to change destination of flicking.(e.g. always snap to center of item)
1359 /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
1360 [EditorBrowsable(EditorBrowsableState.Never)]
1361 protected virtual float AdjustTargetPositionOfScrollAnimation(float position)
1367 /// Scroll position given to ScrollTo.
1368 /// This is the position in the opposite direction to the position of ContentContainer.
1370 /// <since_tizen> 8 </since_tizen>
1371 public Position ScrollPosition
1375 return new Position(-ContentContainer.Position);
1380 /// Current scroll position in the middle of ScrollTo animation.
1381 /// This is the position in the opposite direction to the current position of ContentContainer.
1383 /// <since_tizen> 8 </since_tizen>
1384 public Position ScrollCurrentPosition
1388 return new Position(-ContentContainer.CurrentPosition);
1393 /// Remove all children in ContentContainer.
1395 /// <param name="dispose">If true, removed child is disposed.</param>
1396 [EditorBrowsable(EditorBrowsableState.Never)]
1397 public void RemoveAllChildren(bool dispose = false)
1399 RecursiveRemoveChildren(ContentContainer, dispose);
1402 private void RecursiveRemoveChildren(View parent, bool dispose)
1408 int maxChild = (int)parent.GetChildCount();
1409 for (int i = maxChild - 1; i >= 0; --i)
1411 View child = parent.GetChildAt((uint)i);
1416 RecursiveRemoveChildren(child, dispose);
1417 parent.Remove(child);