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