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