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