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