[NUI] Make Scrollable finish when Gesture.StateType is Cancelled (#2029)
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI.Components / Controls / ScrollableBase.cs
1 /* Copyright (c) 2020 Samsung Electronics Co., Ltd.
2  *
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
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
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.
14  *
15  */
16 using System;
17 using Tizen.NUI.BaseComponents;
18 using System.Collections.Generic;
19 using System.ComponentModel;
20 using System.Diagnostics;
21 using System.Runtime.InteropServices;
22 using Tizen.NUI.Accessibility;
23
24 namespace Tizen.NUI.Components
25 {
26     /// <summary>
27     /// ScrollEventArgs is a class to record scroll event arguments which will sent to user.
28     /// </summary>
29     /// <since_tizen> 8 </since_tizen>
30     public class ScrollEventArgs : EventArgs
31     {
32         private Position position;
33
34         /// <summary>
35         /// Default constructor.
36         /// </summary>
37         /// <param name="position">Current scroll position</param>
38         /// <since_tizen> 8 </since_tizen>
39         public ScrollEventArgs(Position position)
40         {
41             this.position = position;
42         }
43
44         /// <summary>
45         /// Current position of ContentContainer.
46         /// </summary>
47         /// <since_tizen> 8 </since_tizen>
48         public Position Position
49         {
50             get
51             {
52                 return position;
53             }
54         }
55     }
56
57     /// <summary>
58     /// This class provides a View that can scroll a single View with a layout. This View can be a nest of Views.
59     /// </summary>
60     /// <since_tizen> 8 </since_tizen>
61     public class ScrollableBase : Control
62     {
63         static bool LayoutDebugScrollableBase = false; // Debug flag
64         private Direction mScrollingDirection = Direction.Vertical;
65         private bool mScrollEnabled = true;
66         private int mScrollDuration = 125;
67         private int mPageWidth = 0;
68         private float mPageFlickThreshold = 0.4f;
69         private float mScrollingEventThreshold = 0.00001f;
70
71         private class ScrollableBaseCustomLayout : LayoutGroup
72         {
73             protected override void OnMeasure(MeasureSpecification widthMeasureSpec, MeasureSpecification heightMeasureSpec)
74             {
75                 Extents padding = Padding;
76                 float totalHeight = padding.Top + padding.Bottom;
77                 float totalWidth = padding.Start + padding.End;
78
79                 MeasuredSize.StateType childWidthState = MeasuredSize.StateType.MeasuredSizeOK;
80                 MeasuredSize.StateType childHeightState = MeasuredSize.StateType.MeasuredSizeOK;
81
82                 Direction scrollingDirection = Direction.Vertical;
83                 ScrollableBase scrollableBase = this.Owner as ScrollableBase;
84                 if (scrollableBase)
85                 {
86                     scrollingDirection = scrollableBase.ScrollingDirection;
87                 }
88
89                 // measure child, should be a single scrolling child
90                 foreach (LayoutItem childLayout in LayoutChildren)
91                 {
92                     if (childLayout != null)
93                     {
94                         // Get size of child
95                         // Use an Unspecified MeasureSpecification mode so scrolling child is not restricted to it's parents size in Height (for vertical scrolling)
96                         // or Width for horizontal scrolling
97                         MeasureSpecification unrestrictedMeasureSpec = new MeasureSpecification(heightMeasureSpec.Size, MeasureSpecification.ModeType.Unspecified);
98
99                         if (scrollingDirection == Direction.Vertical)
100                         {
101                             MeasureChildWithMargins(childLayout, widthMeasureSpec, new LayoutLength(0), unrestrictedMeasureSpec, new LayoutLength(0));  // Height unrestricted by parent
102                         }
103                         else
104                         {
105                             MeasureChildWithMargins(childLayout, unrestrictedMeasureSpec, new LayoutLength(0), heightMeasureSpec, new LayoutLength(0));  // Width unrestricted by parent
106                         }
107
108                         float childWidth = childLayout.MeasuredWidth.Size.AsDecimal();
109                         float childHeight = childLayout.MeasuredHeight.Size.AsDecimal();
110
111                         // Determine the width and height needed by the children using their given position and size.
112                         // Children could overlap so find the left most and right most child.
113                         Position2D childPosition = childLayout.Owner.Position2D;
114                         float childLeft = childPosition.X;
115                         float childTop = childPosition.Y;
116
117                         // Store current width and height needed to contain all children.
118                         Extents childMargin = childLayout.Margin;
119                         totalWidth = childWidth + childMargin.Start + childMargin.End;
120                         totalHeight = childHeight + childMargin.Top + childMargin.Bottom;
121
122                         if (childLayout.MeasuredWidth.State == MeasuredSize.StateType.MeasuredSizeTooSmall)
123                         {
124                             childWidthState = MeasuredSize.StateType.MeasuredSizeTooSmall;
125                         }
126                         if (childLayout.MeasuredHeight.State == MeasuredSize.StateType.MeasuredSizeTooSmall)
127                         {
128                             childHeightState = MeasuredSize.StateType.MeasuredSizeTooSmall;
129                         }
130                     }
131                 }
132
133
134                 MeasuredSize widthSizeAndState = ResolveSizeAndState(new LayoutLength(totalWidth + Padding.Start + Padding.End), widthMeasureSpec, MeasuredSize.StateType.MeasuredSizeOK);
135                 MeasuredSize heightSizeAndState = ResolveSizeAndState(new LayoutLength(totalHeight + Padding.Top + Padding.Bottom), heightMeasureSpec, MeasuredSize.StateType.MeasuredSizeOK);
136                 totalWidth = widthSizeAndState.Size.AsDecimal();
137                 totalHeight = heightSizeAndState.Size.AsDecimal();
138
139                 // Ensure layout respects it's given minimum size
140                 totalWidth = Math.Max(totalWidth, SuggestedMinimumWidth.AsDecimal());
141                 totalHeight = Math.Max(totalHeight, SuggestedMinimumHeight.AsDecimal());
142
143                 widthSizeAndState.State = childWidthState;
144                 heightSizeAndState.State = childHeightState;
145
146                 SetMeasuredDimensions(ResolveSizeAndState(new LayoutLength(totalWidth + Padding.Start + Padding.End), widthMeasureSpec, childWidthState),
147                                        ResolveSizeAndState(new LayoutLength(totalHeight + Padding.Top + Padding.Bottom), heightMeasureSpec, childHeightState));
148
149                 // Size of ScrollableBase is changed. Change Page width too.
150                 scrollableBase.mPageWidth = (int)MeasuredWidth.Size.AsRoundedValue();
151             }
152
153             protected override void OnLayout(bool changed, LayoutLength left, LayoutLength top, LayoutLength right, LayoutLength bottom)
154             {
155                 foreach (LayoutItem childLayout in LayoutChildren)
156                 {
157                     if (childLayout != null)
158                     {
159                         LayoutLength childWidth = childLayout.MeasuredWidth.Size;
160                         LayoutLength childHeight = childLayout.MeasuredHeight.Size;
161
162                         Position2D childPosition = childLayout.Owner.Position2D;
163                         Extents padding = Padding;
164                         Extents childMargin = childLayout.Margin;
165
166                         LayoutLength childLeft = new LayoutLength(childPosition.X + childMargin.Start + padding.Start);
167                         LayoutLength childTop = new LayoutLength(childPosition.Y + childMargin.Top + padding.Top);
168
169                         childLayout.Layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
170                     }
171                 }
172             }
173         } //  ScrollableBaseCustomLayout
174
175         /// <summary>
176         /// The direction axis to scroll.
177         /// </summary>
178         /// <since_tizen> 8 </since_tizen>
179         public enum Direction
180         {
181             /// <summary>
182             /// Horizontal axis.
183             /// </summary>
184             /// <since_tizen> 8 </since_tizen>
185             Horizontal,
186
187             /// <summary>
188             /// Vertical axis.
189             /// </summary>
190             /// <since_tizen> 8 </since_tizen>
191             Vertical
192         }
193
194         /// <summary>
195         /// Scrolling direction mode.
196         /// Default is Vertical scrolling.
197         /// </summary>
198         /// <since_tizen> 8 </since_tizen>
199         public Direction ScrollingDirection
200         {
201             get
202             {
203                 return mScrollingDirection;
204             }
205             set
206             {
207                 if (value != mScrollingDirection)
208                 {
209                     mScrollingDirection = value;
210                     mPanGestureDetector.RemoveDirection(value == Direction.Horizontal ?
211                         PanGestureDetector.DirectionVertical : PanGestureDetector.DirectionHorizontal);
212                     mPanGestureDetector.AddDirection(value == Direction.Horizontal ?
213                         PanGestureDetector.DirectionHorizontal : PanGestureDetector.DirectionVertical);
214
215                     ContentContainer.WidthSpecification = mScrollingDirection == Direction.Vertical ?
216                         LayoutParamPolicies.MatchParent : LayoutParamPolicies.WrapContent;
217                     ContentContainer.HeightSpecification = mScrollingDirection == Direction.Vertical ?
218                         LayoutParamPolicies.WrapContent : LayoutParamPolicies.MatchParent;
219                 }
220             }
221         }
222
223         /// <summary>
224         /// Enable or disable scrolling.
225         /// </summary>
226         /// <since_tizen> 8 </since_tizen>
227         public bool ScrollEnabled
228         {
229             get
230             {
231                 return mScrollEnabled;
232             }
233             set
234             {
235                 if (value != mScrollEnabled)
236                 {
237                     mScrollEnabled = value;
238                     if (mScrollEnabled)
239                     {
240                         mPanGestureDetector.Detected += OnPanGestureDetected;
241                     }
242                     else
243                     {
244                         mPanGestureDetector.Detected -= OnPanGestureDetected;
245                     }
246                 }
247             }
248         }
249
250         /// <summary>
251         /// Pages mode, enables moving to the next or return to current page depending on pan displacement.
252         /// Default is false.
253         /// </summary>
254         /// <since_tizen> 8 </since_tizen>
255         public bool SnapToPage { set; get; } = false;
256
257         /// <summary>
258         /// Get current page.
259         /// Working property with SnapToPage property.
260         /// </summary>
261         /// <since_tizen> 8 </since_tizen>
262         public int CurrentPage { get; private set; } = 0;
263
264         /// <summary>
265         /// Duration of scroll animation.
266         /// Default value is 125ms.
267         /// </summary>
268         /// <since_tizen> 8 </since_tizen>
269         public int ScrollDuration
270         {
271             set
272             {
273                 mScrollDuration = value >= 0 ? value : mScrollDuration;
274             }
275             get
276             {
277                 return mScrollDuration;
278             }
279         }
280
281         /// <summary>
282         /// Scroll Available area.
283         /// </summary>
284         /// <since_tizen> 8 </since_tizen>
285         public Vector2 ScrollAvailableArea { set; get; }
286
287         /// <summary>
288         /// An event emitted when user starts dragging ScrollableBase, user can subscribe or unsubscribe to this event handler.<br />
289         /// </summary>
290         /// <since_tizen> 8 </since_tizen>
291         public event EventHandler<ScrollEventArgs> ScrollDragStarted;
292
293         /// <summary>
294         /// An event emitted when user stops dragging ScrollableBase, user can subscribe or unsubscribe to this event handler.<br />
295         /// </summary>
296         /// <since_tizen> 8 </since_tizen>
297         public event EventHandler<ScrollEventArgs> ScrollDragEnded;
298
299
300         /// <summary>
301         /// An event emitted when the scrolling slide animation starts, user can subscribe or unsubscribe to this event handler.<br />
302         /// </summary>
303         /// <since_tizen> 8 </since_tizen>
304         public event EventHandler<ScrollEventArgs> ScrollAnimationStarted;
305
306         /// <summary>
307         /// An event emitted when the scrolling slide animation ends, user can subscribe or unsubscribe to this event handler.<br />
308         /// </summary>
309         /// <since_tizen> 8 </since_tizen>
310         public event EventHandler<ScrollEventArgs> ScrollAnimationEnded;
311
312
313         /// <summary>
314         /// An event emitted when scrolling, user can subscribe or unsubscribe to this event handler.<br />
315         /// </summary>
316         /// <since_tizen> 8 </since_tizen>
317         public event EventHandler<ScrollEventArgs> Scrolling;
318
319
320         /// <summary>
321         /// Scrollbar for ScrollableBase.
322         /// </summary>
323         /// <since_tizen> 8 </since_tizen>
324         public ScrollbarBase Scrollbar
325         {
326             get
327             {
328                 return scrollBar;
329             }
330             set
331             {
332                 if (scrollBar)
333                 {
334                     scrollBar.Unparent();
335                 }
336
337                 scrollBar = value;
338                 scrollBar.Name = "ScrollBar";
339                 base.Add(scrollBar);
340
341                 if (hideScrollbar)
342                 {
343                     scrollBar.Hide();
344                 }
345                 else
346                 {
347                     scrollBar.Show();
348                 }
349
350                 SetScrollbar();
351             }
352         }
353
354         /// <summary>
355         /// Always hide Scrollbar.
356         /// </summary>
357         /// <since_tizen> 8 </since_tizen>
358         public bool HideScrollbar
359         {
360             get
361             {
362                 return hideScrollbar;
363             }
364             set
365             {
366                 hideScrollbar = value;
367
368                 if (scrollBar)
369                 {
370                     if (value)
371                     {
372                         scrollBar.Hide();
373                     }
374                     else
375                     {
376                         scrollBar.Show();
377                     }
378                 }
379             }
380         }
381
382         /// <summary>
383         /// Container which has content of ScrollableBase.
384         /// </summary>
385         /// <since_tizen> 8 </since_tizen>
386         public View ContentContainer { get; private set; }
387
388         /// <summary>
389         /// Set the layout on this View. Replaces any existing Layout.
390         /// </summary>
391         /// <since_tizen> 8 </since_tizen>
392         public new LayoutItem Layout
393         {
394             get
395             {
396                 return ContentContainer.Layout;
397             }
398             set
399             {
400                 ContentContainer.Layout = value;
401                 if (ContentContainer.Layout != null)
402                 {
403                     ContentContainer.Layout.SetPositionByLayout = false;
404                 }
405             }
406         }
407
408         /// <summary>
409         /// List of children of Container.
410         /// </summary>
411         /// <since_tizen> 8 </since_tizen>
412         public new List<View> Children
413         {
414             get
415             {
416                 return ContentContainer.Children;
417             }
418         }
419
420         /// <summary>
421         /// Deceleration rate of scrolling by finger.
422         /// Rate should be bigger than 0 and smaller than 1.
423         /// Default value is 0.998f;
424         /// </summary>
425         /// <since_tizen> 8 </since_tizen>
426         public float DecelerationRate
427         {
428             get
429             {
430                 return decelerationRate;
431             }
432             set
433             {
434                 decelerationRate = (value < 1 && value > 0) ? value : decelerationRate;
435                 logValueOfDeceleration = (float)Math.Log(value);
436             }
437         }
438
439         /// <summary>
440         /// Threashold not to go infinit at the end of scrolling animation.
441         /// </summary>
442         [EditorBrowsable(EditorBrowsableState.Never)]
443         public float DecelerationThreshold { get; set; } = 0.1f;
444
445         /// <summary>
446         /// Scrolling event will be thrown when this amount of scroll positino is changed.
447         /// </summary>
448         [EditorBrowsable(EditorBrowsableState.Never)]
449         public float ScrollingEventThreshold
450         {
451             get
452             {
453                 return mScrollingEventThreshold;
454             }
455             set
456             {
457                 if (mScrollingEventThreshold != value && value > 0)
458                 {
459                     ContentContainer.RemovePropertyNotification(propertyNotification);
460                     propertyNotification = ContentContainer.AddPropertyNotification("position", PropertyCondition.Step(value));
461                     propertyNotification.Notified += OnPropertyChanged;
462                     mScrollingEventThreshold = value;
463                 }
464             }
465         }
466
467
468         /// <summary>
469         /// Page will be changed when velocity of panning is over threshold.
470         /// The unit of threshold is pixel per milisec.
471         /// </summary>
472         /// <since_tizen> 8 </since_tizen>
473         public float PageFlickThreshold
474         {
475             get
476             {
477                 return mPageFlickThreshold;
478             }
479             set
480             {
481                 mPageFlickThreshold = value >= 0f ? value : mPageFlickThreshold;
482             }
483         }
484
485         /// <summary>
486         /// Alphafunction for scroll animation.
487         /// </summary>
488         [EditorBrowsable(EditorBrowsableState.Never)]
489         public AlphaFunction ScrollAlphaFunction { get; set; } = new AlphaFunction(AlphaFunction.BuiltinFunctions.Linear);
490
491         private bool hideScrollbar = true;
492         private float maxScrollDistance;
493         private float childTargetPosition = 0.0f;
494         private PanGestureDetector mPanGestureDetector;
495         private View mInterruptTouchingChild;
496         private ScrollbarBase scrollBar;
497         private bool scrolling = false;
498         private float ratioOfScreenWidthToCompleteScroll = 0.5f;
499         private float totalDisplacementForPan = 0.0f;
500         private Size previousContainerSize = new Size();
501         private PropertyNotification propertyNotification;
502         private float noticeAnimationEndBeforePosition = 0.0f;
503         private bool readyToNotice = false;
504
505         /// <summary>
506         /// Notice before animation is finished.
507         /// </summary>
508         [EditorBrowsable(EditorBrowsableState.Never)]
509         // Let's consider more whether this needs to be set as protected.
510         public float NoticeAnimationEndBeforePosition { get => noticeAnimationEndBeforePosition; set => noticeAnimationEndBeforePosition = value; }
511
512         // Let's consider more whether this needs to be set as protected.
513         private float finalTargetPosition;
514
515         private Animation scrollAnimation;
516         // Declare user alpha function delegate
517         [UnmanagedFunctionPointer(CallingConvention.StdCall)]
518         private delegate float UserAlphaFunctionDelegate(float progress);
519         private UserAlphaFunctionDelegate customScrollAlphaFunction;
520         private float velocityOfLastPan = 0.0f;
521         private float panAnimationDuration = 0.0f;
522         private float panAnimationDelta = 0.0f;
523         private float logValueOfDeceleration = 0.0f;
524         private float decelerationRate = 0.0f;
525
526         /// <summary>
527         /// Default Constructor
528         /// </summary>
529         /// <since_tizen> 8 </since_tizen>
530         public ScrollableBase() : base()
531         {
532             DecelerationRate = 0.998f;
533
534             base.Layout = new ScrollableBaseCustomLayout();
535             mPanGestureDetector = new PanGestureDetector();
536             mPanGestureDetector.Attach(this);
537             mPanGestureDetector.AddDirection(PanGestureDetector.DirectionVertical);
538             mPanGestureDetector.Detected += OnPanGestureDetected;
539
540             ClippingMode = ClippingModeType.ClipChildren;
541
542             //Default Scrolling child
543             ContentContainer = new View()
544             {
545                 WidthSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.MatchParent : LayoutParamPolicies.WrapContent,
546                 HeightSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.WrapContent : LayoutParamPolicies.MatchParent,
547                 Layout = new AbsoluteLayout() { SetPositionByLayout = false },
548             };
549             ContentContainer.Relayout += OnScrollingChildRelayout;
550             propertyNotification = ContentContainer.AddPropertyNotification("position", PropertyCondition.Step(mScrollingEventThreshold));
551             propertyNotification.Notified += OnPropertyChanged;
552             base.Add(ContentContainer);
553
554             //Interrupt touching when panning is started
555             mInterruptTouchingChild = new View()
556             {
557                 Size = new Size(Window.Instance.WindowSize),
558                 BackgroundColor = Color.Transparent,
559             };
560             mInterruptTouchingChild.TouchEvent += OnIterruptTouchingChildTouched;
561             Scrollbar = new Scrollbar();
562
563             AccessibilityManager.Instance.SetAccessibilityAttribute(this, AccessibilityManager.AccessibilityAttribute.Trait, "ScrollableBase");
564         }
565
566         private bool OnIterruptTouchingChildTouched(object source, View.TouchEventArgs args)
567         {
568             if (args.Touch.GetState(0) == PointStateType.Down)
569             {
570                 if (scrolling && !SnapToPage)
571                 {
572                     StopScroll();
573                 }
574             }
575             return true;
576         }
577
578         private void OnPropertyChanged(object source, PropertyNotification.NotifyEventArgs args)
579         {
580             OnScroll();
581         }
582
583         /// <summary>
584         /// Called after a child has been added to the owning view.
585         /// </summary>
586         /// <param name="view">The child which has been added.</param>
587         /// <since_tizen> 8 </since_tizen>
588         public override void Add(View view)
589         {
590             ContentContainer.Add(view);
591         }
592
593         /// <summary>
594         /// Called after a child has been removed from the owning view.
595         /// </summary>
596         /// <param name="view">The child which has been removed.</param>
597         /// <since_tizen> 8 </since_tizen>
598         public override void Remove(View view)
599         {
600             if (SnapToPage && CurrentPage == Children.IndexOf(view) && CurrentPage == Children.Count - 1)
601             {
602                 // Target View is current page and also last child.
603                 // CurrentPage should be changed to previous page.
604                 CurrentPage = Math.Max(0, CurrentPage - 1);
605                 ScrollToIndex(CurrentPage);
606             }
607
608             ContentContainer.Remove(view);
609         }
610
611         private void OnScrollingChildRelayout(object source, EventArgs args)
612         {
613             // Size is changed. Calculate maxScrollDistance.
614             bool isSizeChanged = previousContainerSize.Width != ContentContainer.Size.Width || previousContainerSize.Height != ContentContainer.Size.Height;
615
616             if (isSizeChanged)
617             {
618                 maxScrollDistance = CalculateMaximumScrollDistance();
619                 SetScrollbar();
620             }
621
622             previousContainerSize = ContentContainer.Size;
623         }
624
625         /// <summary>
626         /// The composition of a Scrollbar can vary depending on how you use ScrollableBase.
627         /// Set the composition that will go into the ScrollableBase according to your ScrollableBase.
628         /// </summary>
629         /// <since_tizen> 8 </since_tizen>
630         [EditorBrowsable(EditorBrowsableState.Never)]
631         protected virtual void SetScrollbar()
632         {
633             if (Scrollbar)
634             {
635                 bool isHorizontal = ScrollingDirection == Direction.Horizontal;
636                 float contentLength = isHorizontal ? ContentContainer.Size.Width : ContentContainer.Size.Height;
637                 float viewportLength = isHorizontal ? Size.Width : Size.Height;
638                 float currentPosition = isHorizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y;
639                 Scrollbar.Initialize(contentLength, viewportLength, currentPosition, isHorizontal);
640             }
641         }
642
643         /// <summary>
644         /// Scrolls to the item at the specified index.
645         /// </summary>
646         /// <param name="index">Index of item.</param>
647         /// <since_tizen> 8 </since_tizen>
648         public void ScrollToIndex(int index)
649         {
650             if (ContentContainer.ChildCount - 1 < index || index < 0)
651             {
652                 return;
653             }
654
655             if (SnapToPage)
656             {
657                 CurrentPage = index;
658             }
659
660             float targetPosition = Math.Min(ScrollingDirection == Direction.Vertical ? Children[index].Position.Y : Children[index].Position.X, maxScrollDistance);
661             AnimateChildTo(ScrollDuration, -targetPosition);
662         }
663
664         private void OnScrollDragStarted()
665         {
666             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
667             ScrollDragStarted?.Invoke(this, eventArgs);
668         }
669
670         private void OnScrollDragEnded()
671         {
672             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
673             ScrollDragEnded?.Invoke(this, eventArgs);
674         }
675
676         private void OnScrollAnimationStarted()
677         {
678             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
679             ScrollAnimationStarted?.Invoke(this, eventArgs);
680         }
681
682         private void OnScrollAnimationEnded()
683         {
684             scrolling = false;
685             base.Remove(mInterruptTouchingChild);
686
687             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
688             ScrollAnimationEnded?.Invoke(this, eventArgs);
689         }
690
691         private void OnScroll()
692         {
693             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
694             Scrolling?.Invoke(this, eventArgs);
695
696             bool isHorizontal = ScrollingDirection == Direction.Horizontal;
697             float contentLength = isHorizontal ? ContentContainer.Size.Width : ContentContainer.Size.Height;
698             float currentPosition = isHorizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y;
699
700             scrollBar.Update(contentLength, Math.Abs(currentPosition));
701             CheckPreReachedTargetPosition();
702         }
703
704         private void CheckPreReachedTargetPosition()
705         {
706             // Check whether we reached pre-reached target position
707             if (readyToNotice &&
708                 ContentContainer.CurrentPosition.Y <= finalTargetPosition + NoticeAnimationEndBeforePosition &&
709                 ContentContainer.CurrentPosition.Y >= finalTargetPosition - NoticeAnimationEndBeforePosition)
710             {
711                 //Notice first
712                 readyToNotice = false;
713                 OnPreReachedTargetPosition(finalTargetPosition);
714             }
715         }
716
717         /// <summary>
718         /// This helps developer who wants to know before scroll is reaching target position.
719         /// </summary>
720         /// <param name="targetPosition">Index of item.</param>
721         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
722         [EditorBrowsable(EditorBrowsableState.Never)]
723         protected virtual void OnPreReachedTargetPosition(float targetPosition)
724         {
725
726         }
727
728         private void StopScroll()
729         {
730             if (scrollAnimation != null)
731             {
732                 if (scrollAnimation.State == Animation.States.Playing)
733                 {
734                     Debug.WriteLineIf(LayoutDebugScrollableBase, "StopScroll Animation Playing");
735                     scrollAnimation.Stop(Animation.EndActions.Cancel);
736                     OnScrollAnimationEnded();
737                 }
738                 scrollAnimation.Clear();
739             }
740         }
741
742         private void AnimateChildTo(int duration, float axisPosition)
743         {
744             Debug.WriteLineIf(LayoutDebugScrollableBase, "AnimationTo Animation Duration:" + duration + " Destination:" + axisPosition);
745             finalTargetPosition = axisPosition;
746
747             StopScroll(); // Will replace previous animation so will stop existing one.
748
749             if (scrollAnimation == null)
750             {
751                 scrollAnimation = new Animation();
752                 scrollAnimation.Finished += ScrollAnimationFinished;
753             }
754
755             scrollAnimation.Duration = duration;
756             scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.EaseOutSquare);
757             scrollAnimation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", axisPosition, ScrollAlphaFunction);
758             scrolling = true;
759             OnScrollAnimationStarted();
760             scrollAnimation.Play();
761         }
762
763         /// <summary>
764         /// Scroll to specific position with or without animation.
765         /// </summary>
766         /// <param name="position">Destination.</param>
767         /// <param name="animate">Scroll with or without animation</param>
768         /// <since_tizen> 8 </since_tizen>
769         public void ScrollTo(float position, bool animate)
770         {
771             float currentPositionX = ContentContainer.CurrentPosition.X != 0 ? ContentContainer.CurrentPosition.X : ContentContainer.Position.X;
772             float currentPositionY = ContentContainer.CurrentPosition.Y != 0 ? ContentContainer.CurrentPosition.Y : ContentContainer.Position.Y;
773             float delta = ScrollingDirection == Direction.Horizontal ? currentPositionX : currentPositionY;
774             // The argument position is the new pan position. So the new position of ScrollableBase becomes (-position).
775             // To move ScrollableBase's position to (-position), it moves by (-position - currentPosition).
776             delta = -position - delta;
777
778             ScrollBy(delta, animate);
779         }
780
781         private float BoundScrollPosition(float targetPosition)
782         {
783             if (ScrollAvailableArea != null)
784             {
785                 float minScrollPosition = ScrollAvailableArea.X;
786                 float maxScrollPosition = ScrollAvailableArea.Y;
787
788                 targetPosition = Math.Min(-minScrollPosition, targetPosition);
789                 targetPosition = Math.Max(-maxScrollPosition, targetPosition);
790             }
791             else
792             {
793                 targetPosition = Math.Min(0, targetPosition);
794                 targetPosition = Math.Max(-maxScrollDistance, targetPosition);
795             }
796
797             return targetPosition;
798         }
799
800         private void ScrollBy(float displacement, bool animate)
801         {
802             if (GetChildCount() == 0 || maxScrollDistance < 0)
803             {
804                 return;
805             }
806
807             float childCurrentPosition = (ScrollingDirection == Direction.Horizontal) ? ContentContainer.PositionX : ContentContainer.PositionY;
808
809             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy childCurrentPosition:" + childCurrentPosition +
810                                                    " displacement:" + displacement,
811                                                    " maxScrollDistance:" + maxScrollDistance);
812
813             childTargetPosition = childCurrentPosition + displacement; // child current position + gesture displacement
814
815
816             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy currentAxisPosition:" + childCurrentPosition + "childTargetPosition:" + childTargetPosition);
817
818             if (animate)
819             {
820                 // Calculate scroll animaton duration
821                 float scrollDistance = Math.Abs(displacement);
822                 readyToNotice = true;
823
824                 AnimateChildTo(ScrollDuration, BoundScrollPosition(AdjustTargetPositionOfScrollAnimation(BoundScrollPosition(childTargetPosition))));
825             }
826             else
827             {
828                 finalTargetPosition = BoundScrollPosition(childTargetPosition);
829
830                 // Set position of scrolling child without an animation
831                 if (ScrollingDirection == Direction.Horizontal)
832                 {
833                     ContentContainer.PositionX = finalTargetPosition;
834                 }
835                 else
836                 {
837                     ContentContainer.PositionY = finalTargetPosition;
838                 }
839
840             }
841         }
842
843         /// <summary>
844         /// you can override it to clean-up your own resources.
845         /// </summary>
846         /// <param name="type">DisposeTypes</param>
847         /// This will be public opened in tizen_5.5 after ACR done. Before ACR, need to be hidden as inhouse API.
848         [EditorBrowsable(EditorBrowsableState.Never)]
849         protected override void Dispose(DisposeTypes type)
850         {
851             if (disposed)
852             {
853                 return;
854             }
855
856             if (type == DisposeTypes.Explicit)
857             {
858                 StopScroll();
859
860                 if (mPanGestureDetector != null)
861                 {
862                     mPanGestureDetector.Detected -= OnPanGestureDetected;
863                     mPanGestureDetector.Dispose();
864                     mPanGestureDetector = null;
865                 }
866
867                 propertyNotification.Dispose();
868             }
869             base.Dispose(type);
870         }
871
872         private float CalculateMaximumScrollDistance()
873         {
874             float scrollingChildLength = 0;
875             float scrollerLength = 0;
876             if (ScrollingDirection == Direction.Horizontal)
877             {
878                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Horizontal");
879
880                 scrollingChildLength = ContentContainer.Size.Width;
881                 scrollerLength = Size.Width;
882             }
883             else
884             {
885                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Vertical");
886                 scrollingChildLength = ContentContainer.Size.Height;
887                 scrollerLength = Size.Height;
888             }
889
890             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy maxScrollDistance:" + (scrollingChildLength - scrollerLength) +
891                                                    " parent length:" + scrollerLength +
892                                                    " scrolling child length:" + scrollingChildLength);
893
894             return Math.Max(scrollingChildLength - scrollerLength, 0);
895         }
896
897         private void PageSnap(float velocity)
898         {
899             Debug.WriteLineIf(LayoutDebugScrollableBase, "PageSnap with pan candidate totalDisplacement:" + totalDisplacementForPan +
900                                                                 " currentPage[" + CurrentPage + "]");
901
902             //Increment current page if total displacement enough to warrant a page change.
903             if (Math.Abs(totalDisplacementForPan) > (mPageWidth * ratioOfScreenWidthToCompleteScroll))
904             {
905                 if (totalDisplacementForPan < 0)
906                 {
907                     CurrentPage = Math.Min(Math.Max(Children.Count - 1, 0), ++CurrentPage);
908                 }
909                 else
910                 {
911                     CurrentPage = Math.Max(0, --CurrentPage);
912                 }
913             }
914             else if (Math.Abs(velocity) > PageFlickThreshold)
915             {
916                 if (velocity < 0)
917                 {
918                     CurrentPage = Math.Min(Math.Max(Children.Count - 1, 0), ++CurrentPage);
919                 }
920                 else
921                 {
922                     CurrentPage = Math.Max(0, --CurrentPage);
923                 }
924             }
925
926             // Animate to new page or reposition to current page
927             float destinationX = -(Children[CurrentPage].Position.X + Children[CurrentPage].CurrentSize.Width / 2 - CurrentSize.Width / 2); // set to middle of current page
928             Debug.WriteLineIf(LayoutDebugScrollableBase, "Snapping to page[" + CurrentPage + "] to:" + destinationX + " from:" + ContentContainer.PositionX);
929             AnimateChildTo(ScrollDuration, destinationX);
930         }
931
932         private void OnPanGestureDetected(object source, PanGestureDetector.DetectedEventArgs e)
933         {
934             OnPanGesture(e.PanGesture);
935         }
936
937         private void OnPanGesture(PanGesture panGesture)
938         {
939             if (SnapToPage && scrollAnimation != null && scrollAnimation.State == Animation.States.Playing)
940             {
941                 return;
942             }
943
944             if (panGesture.State == Gesture.StateType.Started)
945             {
946                 readyToNotice = false;
947                 base.Add(mInterruptTouchingChild);
948                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Gesture Start");
949                 if (scrolling && !SnapToPage)
950                 {
951                     StopScroll();
952                 }
953                 totalDisplacementForPan = 0.0f;
954                 OnScrollDragStarted();
955             }
956             else if (panGesture.State == Gesture.StateType.Continuing)
957             {
958                 if (ScrollingDirection == Direction.Horizontal)
959                 {
960                     ScrollBy(panGesture.Displacement.X, false);
961                     totalDisplacementForPan += panGesture.Displacement.X;
962                 }
963                 else
964                 {
965                     ScrollBy(panGesture.Displacement.Y, false);
966                     totalDisplacementForPan += panGesture.Displacement.Y;
967                 }
968                 Debug.WriteLineIf(LayoutDebugScrollableBase, "OnPanGestureDetected Continue totalDisplacementForPan:" + totalDisplacementForPan);
969             }
970             else if (panGesture.State == Gesture.StateType.Finished || panGesture.State == Gesture.StateType.Cancelled)
971             {
972                 OnScrollDragEnded();
973                 StopScroll(); // Will replace previous animation so will stop existing one.
974
975                 if (scrollAnimation == null)
976                 {
977                     scrollAnimation = new Animation();
978                     scrollAnimation.Finished += ScrollAnimationFinished;
979                 }
980
981                 float panVelocity = (ScrollingDirection == Direction.Horizontal) ? panGesture.Velocity.X : panGesture.Velocity.Y;
982
983                 if (SnapToPage)
984                 {
985                     PageSnap(panVelocity);
986                 }
987                 else
988                 {
989                     if (panVelocity == 0)
990                     {
991                         float currentScrollPosition = (ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
992                         scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.Linear);
993                         scrollAnimation.Duration = 0;
994                         scrollAnimation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", currentScrollPosition);
995                         scrollAnimation.Play();
996                     }
997                     else
998                     {
999                         Decelerating(panVelocity, scrollAnimation);
1000                     }
1001                 }
1002
1003                 totalDisplacementForPan = 0;
1004                 scrolling = true;
1005                 readyToNotice = true;
1006                 OnScrollAnimationStarted();
1007             }
1008         }
1009
1010         internal override bool OnAccessibilityPan(PanGesture gestures)
1011         {
1012             if (SnapToPage && scrollAnimation != null && scrollAnimation.State == Animation.States.Playing)
1013             {
1014                 return false;
1015             }
1016
1017             OnPanGesture(gestures);
1018             return true;
1019         }
1020
1021         private float CustomScrollAlphaFunction(float progress)
1022         {
1023             if (panAnimationDelta == 0)
1024             {
1025                 return 1.0f;
1026             }
1027             else
1028             {
1029                 // Parameter "progress" is normalized value. We need to multiply target duration to calculate distance.
1030                 // Can get real distance using equation of deceleration (check Decelerating function)
1031                 // After get real distance, normalize it
1032                 float realDuration = progress * panAnimationDuration;
1033                 float realDistance = velocityOfLastPan * ((float)Math.Pow(decelerationRate, realDuration) - 1) / logValueOfDeceleration;
1034                 float result = Math.Min(realDistance / Math.Abs(panAnimationDelta), 1.0f);
1035                 return result;
1036             }
1037         }
1038
1039         /// <summary>
1040         /// you can override it to custom your decelerating
1041         /// </summary>
1042         /// <param name="velocity">Velocity of current pan.</param>
1043         /// <param name="animation">Scroll animation.</param>
1044         [EditorBrowsable(EditorBrowsableState.Never)]
1045         protected virtual void Decelerating(float velocity, Animation animation)
1046         {
1047             // Decelerating using deceleration equation ===========
1048             //
1049             // V   : velocity (pixel per milisecond)
1050             // V0  : initial velocity
1051             // d   : deceleration rate,
1052             // t   : time
1053             // X   : final position after decelerating
1054             // log : natural logarithm
1055             //
1056             // V(t) = V0 * d pow t;
1057             // X(t) = V0 * (d pow t - 1) / log d;  <-- Integrate the velocity function
1058             // X(∞) = V0 * d / (1 - d); <-- Result using inifit T can be final position because T is tending to infinity.
1059             //
1060             // Because of final T is tending to inifity, we should use threshold value to finish.
1061             // Final T = log(-threshold * log d / |V0| ) / log d;
1062
1063             velocityOfLastPan = Math.Abs(velocity);
1064
1065             float currentScrollPosition = -(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
1066             panAnimationDelta = (velocityOfLastPan * decelerationRate) / (1 - decelerationRate);
1067             panAnimationDelta = velocity > 0 ? -panAnimationDelta : panAnimationDelta;
1068
1069             float destination = -(panAnimationDelta + currentScrollPosition);
1070             float adjustDestination = AdjustTargetPositionOfScrollAnimation(destination);
1071             float maxPosition = ScrollAvailableArea != null ? ScrollAvailableArea.Y : maxScrollDistance;
1072             float minPosition = ScrollAvailableArea != null ? ScrollAvailableArea.X : 0;
1073
1074             if (destination < -maxPosition || destination > minPosition)
1075             {
1076                 panAnimationDelta = velocity > 0 ? (currentScrollPosition - minPosition) : (maxPosition - currentScrollPosition);
1077                 destination = velocity > 0 ? minPosition : -maxPosition;
1078
1079                 if (panAnimationDelta == 0)
1080                 {
1081                     panAnimationDuration = 0.0f;
1082                 }
1083                 else
1084                 {
1085                     panAnimationDuration = (float)Math.Log((panAnimationDelta * logValueOfDeceleration / velocityOfLastPan + 1), decelerationRate);
1086                 }
1087
1088                 Debug.WriteLineIf(LayoutDebugScrollableBase, "\n" +
1089                     "OverRange======================= \n" +
1090                     "[decelerationRate] " + decelerationRate + "\n" +
1091                     "[logValueOfDeceleration] " + logValueOfDeceleration + "\n" +
1092                     "[Velocity] " + velocityOfLastPan + "\n" +
1093                     "[CurrentPosition] " + currentScrollPosition + "\n" +
1094                     "[CandidateDelta] " + panAnimationDelta + "\n" +
1095                     "[Destination] " + destination + "\n" +
1096                     "[Duration] " + panAnimationDuration + "\n" +
1097                     "================================ \n"
1098                 );
1099             }
1100             else
1101             {
1102                 panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
1103
1104                 if (adjustDestination != destination)
1105                 {
1106                     destination = adjustDestination;
1107                     panAnimationDelta = destination + currentScrollPosition;
1108                     velocityOfLastPan = Math.Abs(panAnimationDelta * logValueOfDeceleration / ((float)Math.Pow(decelerationRate, panAnimationDuration) - 1));
1109                     panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
1110                 }
1111
1112                 Debug.WriteLineIf(LayoutDebugScrollableBase, "\n" +
1113                     "================================ \n" +
1114                     "[decelerationRate] " + decelerationRate + "\n" +
1115                     "[logValueOfDeceleration] " + logValueOfDeceleration + "\n" +
1116                     "[Velocity] " + velocityOfLastPan + "\n" +
1117                     "[CurrentPosition] " + currentScrollPosition + "\n" +
1118                     "[CandidateDelta] " + panAnimationDelta + "\n" +
1119                     "[Destination] " + destination + "\n" +
1120                     "[Duration] " + panAnimationDuration + "\n" +
1121                     "================================ \n"
1122                 );
1123             }
1124
1125             finalTargetPosition = destination;
1126
1127             customScrollAlphaFunction = new UserAlphaFunctionDelegate(CustomScrollAlphaFunction);
1128             animation.DefaultAlphaFunction = new AlphaFunction(customScrollAlphaFunction);
1129             GC.KeepAlive(customScrollAlphaFunction);
1130             animation.Duration = (int)panAnimationDuration;
1131             animation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", destination);
1132             animation.Play();
1133         }
1134
1135         private void ScrollAnimationFinished(object sender, EventArgs e)
1136         {
1137             OnScrollAnimationEnded();
1138         }
1139
1140         /// <summary>
1141         /// Adjust scrolling position by own scrolling rules.
1142         /// Override this function when developer wants to change destination of flicking.(e.g. always snap to center of item)
1143         /// </summary>
1144         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
1145         [EditorBrowsable(EditorBrowsableState.Never)]
1146         protected virtual float AdjustTargetPositionOfScrollAnimation(float position)
1147         {
1148             return position;
1149         }
1150
1151         /// <summary>
1152         /// Scroll position given to ScrollTo.
1153         /// This is the position in the opposite direction to the position of ContentContainer.
1154         /// </summary>
1155         /// <since_tizen> 8 </since_tizen>
1156         public Position ScrollPosition
1157         {
1158             get
1159             {
1160                 return new Position(-ContentContainer.Position);
1161             }
1162         }
1163
1164         /// <summary>
1165         /// Current scroll position in the middle of ScrollTo animation.
1166         /// This is the position in the opposite direction to the current position of ContentContainer.
1167         /// </summary>
1168         /// <since_tizen> 8 </since_tizen>
1169         public Position ScrollCurrentPosition
1170         {
1171             get
1172             {
1173                 return new Position(-ContentContainer.CurrentPosition);
1174             }
1175         }
1176     }
1177
1178 } // namespace