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