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