[NUI] Add public types to replace nested types (CA1034 of StyleCop) (#1692)
[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
22 namespace Tizen.NUI.Components
23 {
24     /// <summary>
25     /// ScrollEventArgs is a class to record scroll event arguments which will sent to user.
26     /// </summary>
27     /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
28     [EditorBrowsable(EditorBrowsableState.Never)]
29     public class ScrollEventArgs : EventArgs
30     {
31         Position position;
32
33         /// <summary>
34         /// Default constructor.
35         /// </summary>
36         /// <param name="position">Current scroll position</param>
37         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
38         public ScrollEventArgs(Position position)
39         {
40             this.position = position;
41         }
42
43         /// <summary>
44         /// [Draft] Current scroll position.
45         /// </summary>
46         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
47         [EditorBrowsable(EditorBrowsableState.Never)]
48         public Position Position
49         {
50             get
51             {
52                 return position;
53             }
54         }
55     }
56
57     /// <summary>
58     /// [Draft] 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     /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
61     [EditorBrowsable(EditorBrowsableState.Never)]
62     public class ScrollableBase : Control
63     {
64         static bool LayoutDebugScrollableBase = false; // Debug flag
65         private Direction mScrollingDirection = Direction.Vertical;
66         private bool mScrollEnabled = true;
67         private int mPageWidth = 0;
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> 6 </since_tizen>
177         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
178         [EditorBrowsable(EditorBrowsableState.Never)]
179         public enum Direction
180         {
181             /// <summary>
182             /// Horizontal axis.
183             /// </summary>
184             /// <since_tizen> 6 </since_tizen>
185             Horizontal,
186
187             /// <summary>
188             /// Vertical axis.
189             /// </summary>
190             /// <since_tizen> 6 </since_tizen>
191             Vertical
192         }
193
194         /// <summary>
195         /// [Draft] Configurable speed threshold that register the gestures as a flick.
196         /// If the flick speed less than the threshold then will not be considered a flick.
197         /// </summary>
198         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
199         [EditorBrowsable(EditorBrowsableState.Never)]
200         public float FlickThreshold { get; set; } = 0.2f;
201
202         /// <summary>
203         /// [Draft] Configurable duration modifer for the flick animation.
204         /// Determines the speed of the scroll, large value results in a longer flick animation. Range (0.1 - 1.0)
205         /// </summary>
206         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
207         [EditorBrowsable(EditorBrowsableState.Never)]
208         public float FlickAnimationSpeed { get; set; } = 0.4f;
209
210         /// <summary>
211         /// [Draft] Configurable modifer for the distance to be scrolled when flicked detected.
212         /// It a ratio of the ScrollableBase's length. (not child's length).
213         /// First value is the ratio of the distance to scroll with the weakest flick.
214         /// Second value is the ratio of the distance to scroll with the strongest flick.
215         /// Second > First.
216         /// </summary>
217         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
218         [EditorBrowsable(EditorBrowsableState.Never)]
219         public Vector2 FlickDistanceMultiplierRange { get; set; } = new Vector2(0.6f, 1.8f);
220
221         /// <summary>
222         /// [Draft] Scrolling direction mode.
223         /// Default is Vertical scrolling.
224         /// </summary>
225         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
226         [EditorBrowsable(EditorBrowsableState.Never)]
227         public Direction ScrollingDirection
228         {
229             get
230             {
231                 return mScrollingDirection;
232             }
233             set
234             {
235                 if (value != mScrollingDirection)
236                 {
237                     mScrollingDirection = value;
238                     mPanGestureDetector.RemoveDirection(value == Direction.Horizontal ?
239                         PanGestureDetector.DirectionVertical : PanGestureDetector.DirectionHorizontal);
240                     mPanGestureDetector.AddDirection(value == Direction.Horizontal ?
241                         PanGestureDetector.DirectionHorizontal : PanGestureDetector.DirectionVertical);
242
243                     ContentContainer.WidthSpecification = mScrollingDirection == Direction.Vertical ?
244                         LayoutParamPolicies.MatchParent : LayoutParamPolicies.WrapContent;
245                     ContentContainer.HeightSpecification = mScrollingDirection == Direction.Vertical ?
246                         LayoutParamPolicies.WrapContent : LayoutParamPolicies.MatchParent;
247                 }
248             }
249         }
250
251         /// <summary>
252         /// [Draft] Enable or disable scrolling.
253         /// </summary>
254         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
255         [EditorBrowsable(EditorBrowsableState.Never)]
256         public bool ScrollEnabled
257         {
258             get
259             {
260                 return mScrollEnabled;
261             }
262             set
263             {
264                 if (value != mScrollEnabled)
265                 {
266                     mScrollEnabled = value;
267                     if (mScrollEnabled)
268                     {
269                         mPanGestureDetector.Detected += OnPanGestureDetected;
270                         mTapGestureDetector.Detected += OnTapGestureDetected;
271                     }
272                     else
273                     {
274                         mPanGestureDetector.Detected -= OnPanGestureDetected;
275                         mTapGestureDetector.Detected -= OnTapGestureDetected;
276                     }
277                 }
278             }
279         }
280
281         /// <summary>
282         /// [Draft] Pages mode, enables moving to the next or return to current page depending on pan displacement.
283         /// Default is false.
284         /// </summary>
285         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
286         [EditorBrowsable(EditorBrowsableState.Never)]
287         public bool SnapToPage { set; get; } = false;
288
289         /// <summary>
290         /// [Draft] Get current page.
291         /// Working propery with SnapToPage property.
292         /// </summary>
293         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
294         [EditorBrowsable(EditorBrowsableState.Never)]
295         public int CurrentPage { get; private set; } = 0;
296
297         /// <summary>
298         /// [Draft] Duration of scroll animation.
299         /// </summary>
300         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
301         [EditorBrowsable(EditorBrowsableState.Never)]
302
303         public int ScrollDuration { set; get; } = 125;
304         /// <summary>
305         /// [Draft] Scroll Available area.
306         /// </summary>
307         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
308         [EditorBrowsable(EditorBrowsableState.Never)]
309         public Vector2 ScrollAvailableArea { set; get; }
310
311         /// <summary>
312         /// An event emitted when user starts dragging ScrollableBase, user can subscribe or unsubscribe to this event handler.<br />
313         /// </summary>
314         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
315         [EditorBrowsable(EditorBrowsableState.Never)]
316         public event EventHandler<ScrollEventArgs> ScrollDragStarted;
317
318         /// <summary>
319         /// An event emitted when user stops dragging ScrollableBase, user can subscribe or unsubscribe to this event handler.<br />
320         /// </summary>
321         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
322         [EditorBrowsable(EditorBrowsableState.Never)]
323         public event EventHandler<ScrollEventArgs> ScrollDragEnded;
324
325
326         /// <summary>
327         /// An event emitted when the scrolling slide animation starts, user can subscribe or unsubscribe to this event handler.<br />
328         /// </summary>
329         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
330         [EditorBrowsable(EditorBrowsableState.Never)]
331         public event EventHandler<ScrollEventArgs> ScrollAnimationStarted;
332
333         /// <summary>
334         /// An event emitted when the scrolling slide animation ends, user can subscribe or unsubscribe to this event handler.<br />
335         /// </summary>
336         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
337         [EditorBrowsable(EditorBrowsableState.Never)]
338         public event EventHandler<ScrollEventArgs> ScrollAnimationEnded;
339
340
341         /// <summary>
342         /// An event emitted when scrolling, user can subscribe or unsubscribe to this event handler.<br />
343         /// </summary>
344         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
345         [EditorBrowsable(EditorBrowsableState.Never)]
346         public event EventHandler<ScrollEventArgs> Scrolling;
347
348
349         /// <summary>
350         /// Scrollbar for ScrollableBase.<br />
351         /// </summary>
352         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
353         [EditorBrowsable(EditorBrowsableState.Never)]
354         public ScrollbarBase Scrollbar
355         {
356             get
357             {
358                 return scrollBar;
359             }
360             set
361             {
362                 if (scrollBar)
363                 {
364                     scrollBar.Unparent();
365                 }
366
367                 scrollBar = value;
368                 scrollBar.Name = "ScrollBar";
369                 base.Add(scrollBar);
370
371                 if (hideScrollbar)
372                 {
373                     scrollBar.Hide();
374                 }
375                 else
376                 {
377                     scrollBar.Show();
378                 }
379
380                 SetScrollbar();
381             }
382         }
383
384         /// <summary>
385         /// [Draft] Always hide Scrollbar.
386         /// </summary>
387         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
388         [EditorBrowsable(EditorBrowsableState.Never)]
389         public bool HideScrollBar
390         {
391             get
392             {
393                 return hideScrollbar;
394             }
395             set
396             {
397                 hideScrollbar = value;
398
399                 if (scrollBar)
400                 {
401                     if (value)
402                     {
403                         scrollBar.Hide();
404                     }
405                     else
406                     {
407                         scrollBar.Show();
408                     }
409                 }
410             }
411         }
412
413         private bool hideScrollbar = true;
414         private Animation scrollAnimation;
415         private float maxScrollDistance;
416         private float childTargetPosition = 0.0f;
417         private PanGestureDetector mPanGestureDetector;
418         private TapGestureDetector mTapGestureDetector;
419         private View mInterruptTouchingChild;
420         private ScrollbarBase scrollBar;
421         private float multiplier = 1.0f;
422         private bool scrolling = false;
423         private float ratioOfScreenWidthToCompleteScroll = 0.5f;
424         private float totalDisplacementForPan = 0.0f;
425         private Size previousContainerSize = new Size();
426
427         // If false then can only flick pages when the current animation/scroll as ended.
428         private bool flickWhenAnimating = false;
429         private PropertyNotification propertyNotification;
430
431         // Let's consider more whether this needs to be set as protected.
432         private float finalTargetPosition;
433
434         /// <summary>
435         /// [Draft] Constructor
436         /// </summary>
437         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
438         [EditorBrowsable(EditorBrowsableState.Never)]
439         public ScrollableBase() : base()
440         {
441             base.Layout = new ScrollableBaseCustomLayout();
442             mPanGestureDetector = new PanGestureDetector();
443             mPanGestureDetector.Attach(this);
444             mPanGestureDetector.AddDirection(PanGestureDetector.DirectionVertical);
445             mPanGestureDetector.Detected += OnPanGestureDetected;
446
447             mTapGestureDetector = new TapGestureDetector();
448             mTapGestureDetector.Attach(this);
449             mTapGestureDetector.Detected += OnTapGestureDetected;
450
451             ClippingMode = ClippingModeType.ClipChildren;
452
453             //Default Scrolling child
454             ContentContainer = new View()
455             {
456                 WidthSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.MatchParent : LayoutParamPolicies.WrapContent,
457                 HeightSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.WrapContent : LayoutParamPolicies.MatchParent,
458                 Layout = new AbsoluteLayout(){SetPositionByLayout = false},
459             };
460             ContentContainer.Relayout += OnScrollingChildRelayout;
461             propertyNotification = ContentContainer.AddPropertyNotification("position", PropertyCondition.Step(1.0f));
462             propertyNotification.Notified += OnPropertyChanged;
463             base.Add(ContentContainer);
464
465             //Interrupt touching when panning is started
466             mInterruptTouchingChild = new View()
467             {
468                 Size = new Size(Window.Instance.WindowSize),
469                 BackgroundColor = Color.Transparent,
470             };
471             mInterruptTouchingChild.TouchEvent += OnIterruptTouchingChildTouched;
472
473             Scrollbar = new Scrollbar();
474         }
475
476         /// <summary>
477         /// Container which has content of ScrollableBase.
478         /// </summary>
479         [EditorBrowsable(EditorBrowsableState.Never)]
480         public View ContentContainer { get; private set; }
481
482         /// <summary>
483         /// Set the layout on this View. Replaces any existing Layout.
484         /// </summary>
485         public new LayoutItem Layout
486         {
487             get
488             {
489                 return ContentContainer.Layout;
490             }
491             set
492             {
493                 ContentContainer.Layout = value;
494                 if(ContentContainer.Layout != null)
495                 {
496                     ContentContainer.Layout.SetPositionByLayout = false;
497                 }
498             }
499         }
500
501         /// <summary>
502         /// List of children of Container.
503         /// </summary>
504         public new List<View> Children
505         {
506             get
507             {
508                 return ContentContainer.Children;
509             }
510         }
511
512         private bool OnIterruptTouchingChildTouched(object source, View.TouchEventArgs args)
513         {
514             return true;
515         }
516
517         private void OnPropertyChanged(object source, PropertyNotification.NotifyEventArgs args)
518         {
519             OnScroll();
520         }
521
522         /// <summary>
523         /// Called after a child has been added to the owning view.
524         /// </summary>
525         /// <param name="view">The child which has been added.</param>
526         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
527         [EditorBrowsable(EditorBrowsableState.Never)]
528         public override void Add(View view)
529         {
530             ContentContainer.Add(view);
531         }
532
533         /// <summary>
534         /// Called after a child has been removed from the owning view.
535         /// </summary>
536         /// <param name="view">The child which has been removed.</param>
537         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
538         [EditorBrowsable(EditorBrowsableState.Never)]
539         public override void Remove(View view)
540         {
541             if(SnapToPage && CurrentPage == Children.IndexOf(view) &&  CurrentPage == Children.Count -1)
542             {
543                 // Target View is current page and also last child.
544                 // CurrentPage should be changed to previous page.
545                 CurrentPage = Math.Max(0, CurrentPage-1);
546                 ScrollToIndex(CurrentPage);
547             }
548
549             ContentContainer.Remove(view);
550         }
551
552         private void OnScrollingChildRelayout(object source, EventArgs args)
553         {
554             // Size is changed. Calculate maxScrollDistance.
555             bool isSizeChanged = previousContainerSize.Width != ContentContainer.Size.Width || previousContainerSize.Height != ContentContainer.Size.Height;
556
557             if (isSizeChanged)
558             {
559                 maxScrollDistance = CalculateMaximumScrollDistance();
560                 SetScrollbar();
561             }
562
563             previousContainerSize = ContentContainer.Size;
564         }
565
566         /// <summary>
567         /// The composition of a Scrollbar can vary depending on how you use ScrollableBase. 
568         /// Set the composition that will go into the ScrollableBase according to your ScrollableBase.
569         /// </summary>
570         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
571         [EditorBrowsable(EditorBrowsableState.Never)]
572         protected virtual void SetScrollbar()
573         {
574             if (Scrollbar)
575             {
576                 bool isHorizontal = ScrollingDirection == Direction.Horizontal;
577                 float contentLength = isHorizontal ? ContentContainer.Size.Width : ContentContainer.Size.Height;
578                 float viewportLength = isHorizontal ? Size.Width : Size.Height;
579                 float currentPosition = isHorizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y;
580                 Scrollbar.Initialize(contentLength, viewportLength, currentPosition, isHorizontal);
581             }
582         }
583
584         /// <summary>
585         /// Scrolls to the item at the specified index.
586         /// </summary>
587         /// <param name="index">Index of item.</param>
588         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
589         [EditorBrowsable(EditorBrowsableState.Never)]
590         public void ScrollToIndex(int index)
591         {
592             if (ContentContainer.ChildCount - 1 < index || index < 0)
593             {
594                 return;
595             }
596
597             if (SnapToPage)
598             {
599                 CurrentPage = index;
600             }
601
602             float targetPosition = Math.Min(ScrollingDirection == Direction.Vertical ? Children[index].Position.Y : Children[index].Position.X, maxScrollDistance);
603             AnimateChildTo(ScrollDuration, -targetPosition);
604         }
605
606         private void OnScrollDragStarted()
607         {
608             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
609             ScrollDragStarted?.Invoke(this, eventArgs);
610         }
611
612         private void OnScrollDragEnded()
613         {
614             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
615             ScrollDragEnded?.Invoke(this, eventArgs);
616         }
617
618         private void OnScrollAnimationStarted()
619         {
620             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
621             ScrollAnimationStarted?.Invoke(this, eventArgs);
622         }
623
624         private void OnScrollAnimationEnded()
625         {
626             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
627             ScrollAnimationEnded?.Invoke(this, eventArgs);
628         }
629
630         private bool readyToNotice = false;
631
632         private float noticeAnimationEndBeforePosition = 0.0f;
633         // Let's consider more whether this needs to be set as protected.
634         public float NoticeAnimationEndBeforePosition { get => noticeAnimationEndBeforePosition; set => noticeAnimationEndBeforePosition = value; }
635
636         private void OnScroll()
637         {
638             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
639             Scrolling?.Invoke(this, eventArgs);
640
641             bool isHorizontal = ScrollingDirection == Direction.Horizontal;
642             float contentLength = isHorizontal ? ContentContainer.Size.Width : ContentContainer.Size.Height;
643             float currentPosition = isHorizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y;
644
645             scrollBar.Update(contentLength, Math.Abs(currentPosition));
646             CheckPreReachedTargetPosition();
647         }
648
649         private void CheckPreReachedTargetPosition()
650         {
651             // Check whether we reached pre-reached target position
652             if (readyToNotice &&
653                 ContentContainer.CurrentPosition.Y <= finalTargetPosition + NoticeAnimationEndBeforePosition &&
654                 ContentContainer.CurrentPosition.Y >= finalTargetPosition - NoticeAnimationEndBeforePosition)
655             {
656                 //Notice first
657                 readyToNotice = false;
658                 OnPreReachedTargetPosition(finalTargetPosition);
659             }
660         }
661
662         /// <summary>
663         /// This helps developer who wants to know before scroll is reaching target position.
664         /// </summary>
665         /// <param name="targetPosition">Index of item.</param>
666         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
667         [EditorBrowsable(EditorBrowsableState.Never)]
668         protected virtual void OnPreReachedTargetPosition(float targetPosition)
669         {
670
671         }
672
673         private void StopScroll()
674         {
675             if (scrollAnimation != null)
676             {
677                 if (scrollAnimation.State == Animation.States.Playing)
678                 {
679                     Debug.WriteLineIf(LayoutDebugScrollableBase, "StopScroll Animation Playing");
680                     scrollAnimation.Stop(Animation.EndActions.Cancel);
681                     OnScrollAnimationEnded();
682                 }
683                 scrollAnimation.Clear();
684             }
685         }
686
687         // static constructor registers the control type
688         static ScrollableBase()
689         {
690             // ViewRegistry registers control type with DALi type registry
691             // also uses introspection to find any properties that need to be registered with type registry
692             CustomViewRegistry.Instance.Register(CreateInstance, typeof(ScrollableBase));
693         }
694
695         internal static CustomView CreateInstance()
696         {
697             return new ScrollableBase();
698         }
699
700         private void AnimateChildTo(int duration, float axisPosition)
701         {
702             Debug.WriteLineIf(LayoutDebugScrollableBase, "AnimationTo Animation Duration:" + duration + " Destination:" + axisPosition);
703             finalTargetPosition = axisPosition;
704
705             StopScroll(); // Will replace previous animation so will stop existing one.
706
707             if (scrollAnimation == null)
708             {
709                 scrollAnimation = new Animation();
710                 scrollAnimation.Finished += ScrollAnimationFinished;
711             }
712
713             scrollAnimation.Duration = duration;
714             scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.EaseOutSine);
715             scrollAnimation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", axisPosition);
716             scrolling = true;
717             OnScrollAnimationStarted();
718             scrollAnimation.Play();
719         }
720
721         /// <summary>
722         /// Scroll to specific position with or without animation.
723         /// </summary>
724         /// <param name="position">Destination.</param>
725         /// <param name="animate">Scroll with or without animation</param>
726         [EditorBrowsable(EditorBrowsableState.Never)]
727         public void ScrollTo(float position, bool animate)
728         {
729             float currentPositionX = ContentContainer.CurrentPosition.X != 0 ? ContentContainer.CurrentPosition.X : ContentContainer.Position.X;
730             float currentPositionY = ContentContainer.CurrentPosition.Y != 0 ? ContentContainer.CurrentPosition.Y : ContentContainer.Position.Y;
731             float delta = ScrollingDirection == Direction.Horizontal ? currentPositionX : currentPositionY;
732             // The argument position is the new pan position. So the new position of ScrollableBase becomes (-position).
733             // To move ScrollableBase's position to (-position), it moves by (-position - currentPosition).
734             delta = -position - delta;
735
736             ScrollBy(delta, animate);
737         }
738
739         private float BoundScrollPosition(float targetPosition)
740         {
741             if (ScrollAvailableArea != null)
742             {
743                 float minScrollPosition = ScrollAvailableArea.X;
744                 float maxScrollPosition = ScrollAvailableArea.Y;
745
746                 targetPosition = Math.Min(-minScrollPosition, targetPosition);
747                 targetPosition = Math.Max(-maxScrollPosition, targetPosition);
748             }
749             else
750             {
751                 targetPosition = Math.Min(0, targetPosition);
752                 targetPosition = Math.Max(-maxScrollDistance, targetPosition);
753             }
754
755             return targetPosition;
756         }
757
758         private void ScrollBy(float displacement, bool animate)
759         {
760             if (GetChildCount() == 0 || maxScrollDistance < 0)
761             {
762                 return;
763             }
764
765             float childCurrentPosition = (ScrollingDirection == Direction.Horizontal) ? ContentContainer.PositionX : ContentContainer.PositionY;
766
767             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy childCurrentPosition:" + childCurrentPosition +
768                                                    " displacement:" + displacement,
769                                                    " maxScrollDistance:" + maxScrollDistance);
770
771             childTargetPosition = childCurrentPosition + displacement; // child current position + gesture displacement
772
773
774             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy currentAxisPosition:" + childCurrentPosition + "childTargetPosition:" + childTargetPosition);
775
776             if (animate)
777             {
778                 // Calculate scroll animaton duration
779                 float scrollDistance = Math.Abs(displacement);
780                 int duration = (int)((320 * FlickAnimationSpeed) + (scrollDistance * FlickAnimationSpeed));
781                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Scroll Animation Duration:" + duration + " Distance:" + scrollDistance);
782
783                 readyToNotice = true;
784
785                 AnimateChildTo(duration, BoundScrollPosition(AdjustTargetPositionOfScrollAnimation(BoundScrollPosition(childTargetPosition))));
786             }
787             else
788             {
789                 finalTargetPosition = BoundScrollPosition(childTargetPosition);
790
791                 // Set position of scrolling child without an animation
792                 if (ScrollingDirection == Direction.Horizontal)
793                 {
794                     ContentContainer.PositionX = finalTargetPosition;
795                 }
796                 else
797                 {
798                     ContentContainer.PositionY = finalTargetPosition;
799                 }
800
801             }
802         }
803
804         /// <summary>
805         /// you can override it to clean-up your own resources.
806         /// </summary>
807         /// <param name="type">DisposeTypes</param>
808         /// This will be public opened in tizen_5.5 after ACR done. Before ACR, need to be hidden as inhouse API.
809         [EditorBrowsable(EditorBrowsableState.Never)]
810         protected override void Dispose(DisposeTypes type)
811         {
812             if (disposed)
813             {
814                 return;
815             }
816
817             if (type == DisposeTypes.Explicit)
818             {
819                 StopScroll();
820
821                 if (mPanGestureDetector != null)
822                 {
823                     mPanGestureDetector.Detected -= OnPanGestureDetected;
824                     mPanGestureDetector.Dispose();
825                     mPanGestureDetector = null;
826                 }
827
828                 if (mTapGestureDetector != null)
829                 {
830                     mTapGestureDetector.Detected -= OnTapGestureDetected;
831                     mTapGestureDetector.Dispose();
832                     mTapGestureDetector = null;
833                 }
834             }
835             base.Dispose(type);
836         }
837
838         private float CalculateDisplacementFromVelocity(float axisVelocity)
839         {
840             // Map: flick speed of range (2.0 - 6.0) to flick multiplier of range (0.7 - 1.6)
841             float speedMinimum = FlickThreshold;
842             float speedMaximum = FlickThreshold + 6.0f;
843             float multiplierMinimum = FlickDistanceMultiplierRange.X;
844             float multiplierMaximum = FlickDistanceMultiplierRange.Y;
845
846             float flickDisplacement = 0.0f;
847
848             float speed = Math.Min(4.0f, Math.Abs(axisVelocity));
849
850             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollableBase Candidate Flick speed:" + speed);
851
852             if (speed > FlickThreshold)
853             {
854                 // Flick length is the length of the ScrollableBase.
855                 float flickLength = (ScrollingDirection == Direction.Horizontal) ? CurrentSize.Width : CurrentSize.Height;
856
857                 // Calculate multiplier by mapping speed between the multiplier minimum and maximum.
858                 multiplier = ((speed - speedMinimum) / ((speedMaximum - speedMinimum) * (multiplierMaximum - multiplierMinimum))) + multiplierMinimum;
859
860                 // flick displacement is the product of the flick length and multiplier
861                 flickDisplacement = ((flickLength * multiplier) * speed) / axisVelocity;  // *speed and /velocity to perserve sign.
862
863                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Calculated FlickDisplacement[" + flickDisplacement + "] from speed[" + speed + "] multiplier:"
864                                                         + multiplier);
865             }
866             return flickDisplacement;
867         }
868
869         private float CalculateMaximumScrollDistance()
870         {
871             float scrollingChildLength = 0;
872             float scrollerLength = 0;
873             if (ScrollingDirection == Direction.Horizontal)
874             {
875                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Horizontal");
876
877                 scrollingChildLength = ContentContainer.Size.Width;
878                 scrollerLength = Size.Width;
879             }
880             else
881             {
882                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Vertical");
883                 scrollingChildLength = ContentContainer.Size.Height;
884                 scrollerLength = Size.Height;
885             }
886
887             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy maxScrollDistance:" + (scrollingChildLength - scrollerLength) +
888                                                    " parent length:" + scrollerLength +
889                                                    " scrolling child length:" + scrollingChildLength);
890
891             return Math.Max(scrollingChildLength - scrollerLength, 0);
892         }
893
894         private void PageSnap()
895         {
896             Debug.WriteLineIf(LayoutDebugScrollableBase, "PageSnap with pan candidate totalDisplacement:" + totalDisplacementForPan +
897                                                                 " currentPage[" + CurrentPage + "]");
898
899             //Increment current page if total displacement enough to warrant a page change.
900             if (Math.Abs(totalDisplacementForPan) > (mPageWidth * ratioOfScreenWidthToCompleteScroll))
901             {
902                 if (totalDisplacementForPan < 0)
903                 {
904                     CurrentPage = Math.Min(Math.Max(Children.Count - 1, 0), ++CurrentPage);
905                 }
906                 else
907                 {
908                     CurrentPage = Math.Max(0, --CurrentPage);
909                 }
910             }
911
912             // Animate to new page or reposition to current page
913             float destinationX = -(Children[CurrentPage].Position.X + Children[CurrentPage].CurrentSize.Width / 2 - CurrentSize.Width / 2); // set to middle of current page
914             Debug.WriteLineIf(LayoutDebugScrollableBase, "Snapping to page[" + CurrentPage + "] to:" + destinationX + " from:" + ContentContainer.PositionX);
915             AnimateChildTo(ScrollDuration, destinationX);
916         }
917
918         private void Flick(float flickDisplacement)
919         {
920             if (SnapToPage && Children.Count > 0)
921             {
922                 if ((flickWhenAnimating && scrolling == true) || (scrolling == false))
923                 {
924                     if (flickDisplacement < 0)
925                     {
926                         CurrentPage = Math.Min(Math.Max(Children.Count - 1, 0), CurrentPage + 1);
927                         Debug.WriteLineIf(LayoutDebugScrollableBase, "Snap - to page:" + CurrentPage);
928                     }
929                     else
930                     {
931                         CurrentPage = Math.Max(0, CurrentPage - 1);
932                         Debug.WriteLineIf(LayoutDebugScrollableBase, "Snap + to page:" + CurrentPage);
933                     }
934
935                     float destinationX = -(Children[CurrentPage].Position.X + Children[CurrentPage].CurrentSize.Width / 2.0f - CurrentSize.Width / 2.0f); // set to middle of current page
936                     Debug.WriteLineIf(LayoutDebugScrollableBase, "Snapping to :" + destinationX);
937                     AnimateChildTo(ScrollDuration, destinationX);
938                 }
939             }
940             else
941             {
942                 ScrollBy(flickDisplacement, true); // Animate flickDisplacement.
943             }
944         }
945
946         private void OnPanGestureDetected(object source, PanGestureDetector.DetectedEventArgs e)
947         {
948             if (e.PanGesture.State == Gesture.StateType.Started)
949             {
950                 base.Add(mInterruptTouchingChild);
951                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Gesture Start");
952                 if (scrolling && !SnapToPage)
953                 {
954                     StopScroll();
955                 }
956                 totalDisplacementForPan = 0.0f;
957                 OnScrollDragStarted();
958             }
959             else if (e.PanGesture.State == Gesture.StateType.Continuing)
960             {
961                 if (ScrollingDirection == Direction.Horizontal)
962                 {
963                     ScrollBy(e.PanGesture.Displacement.X, false);
964                     totalDisplacementForPan += e.PanGesture.Displacement.X;
965                 }
966                 else
967                 {
968                     ScrollBy(e.PanGesture.Displacement.Y, false);
969                     totalDisplacementForPan += e.PanGesture.Displacement.Y;
970                 }
971                 Debug.WriteLineIf(LayoutDebugScrollableBase, "OnPanGestureDetected Continue totalDisplacementForPan:" + totalDisplacementForPan);
972             }
973             else if (e.PanGesture.State == Gesture.StateType.Finished)
974             {
975                 float axisVelocity = (ScrollingDirection == Direction.Horizontal) ? e.PanGesture.Velocity.X : e.PanGesture.Velocity.Y;
976                 float flickDisplacement = CalculateDisplacementFromVelocity(axisVelocity);
977
978                 Debug.WriteLineIf(LayoutDebugScrollableBase, "FlickDisplacement:" + flickDisplacement + "TotalDisplacementForPan:" + totalDisplacementForPan);
979                 OnScrollDragEnded();
980
981                 if (flickDisplacement > 0 | flickDisplacement < 0)// Flick detected
982                 {
983                     Flick(flickDisplacement);
984                 }
985                 else
986                 {
987                     // End of panning gesture but was not a flick
988                     if (SnapToPage && Children.Count > 0)
989                     {
990                         PageSnap();
991                     }
992                     else
993                     {
994                         ScrollBy(0, true);
995                     }
996                 }
997                 totalDisplacementForPan = 0;
998
999                 base.Remove(mInterruptTouchingChild);
1000             }
1001         }
1002
1003         private new void OnTapGestureDetected(object source, TapGestureDetector.DetectedEventArgs e)
1004         {
1005             if (e.TapGesture.Type == Gesture.GestureType.Tap)
1006             {
1007                 // Stop scrolling if tap detected (press then relase).
1008                 // Unless in Pages mode, do not want a page change to stop part way.
1009                 if (scrolling && !SnapToPage)
1010                 {
1011                     StopScroll();
1012                 }
1013             }
1014         }
1015
1016         private void ScrollAnimationFinished(object sender, EventArgs e)
1017         {
1018             scrolling = false;
1019             CheckPreReachedTargetPosition();
1020             OnScrollAnimationEnded();
1021         }
1022
1023         /// <summary>
1024         /// Adjust scrolling position by own scrolling rules.
1025         /// Override this function when developer wants to change destination of flicking.(e.g. always snap to center of item)
1026         /// </summary>
1027         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
1028         [EditorBrowsable(EditorBrowsableState.Never)]
1029         protected virtual float AdjustTargetPositionOfScrollAnimation(float position)
1030         {
1031             return position;
1032         }
1033
1034     }
1035
1036 } // namespace