1e64db7b0a19fef9531fde91af309aaf787768ce
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI.Components / Controls / ScrollableBase.cs
1 /* Copyright (c) 2019 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.ComponentModel;
19 using System.Diagnostics;
20 namespace Tizen.NUI.Components
21 {
22     /// <summary>
23     /// [Draft] This class provides a View that can scroll a single View with a layout. This View can be a nest of Views.
24     /// </summary>
25     /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
26     [EditorBrowsable(EditorBrowsableState.Never)]
27     public class ScrollableBase : Control
28     {
29             static bool LayoutDebugScrollableBase = false; // Debug flag
30         private Direction mScrollingDirection = Direction.Vertical;
31         private bool mScrollEnabled = true;
32
33         private class ScrollableBaseCustomLayout : LayoutGroup
34         {
35             protected override void OnMeasure(MeasureSpecification widthMeasureSpec, MeasureSpecification heightMeasureSpec)
36             {
37                 Extents padding = Padding;
38                 float totalHeight = padding.Top + padding.Bottom;
39                 float totalWidth = padding.Start + padding.End;
40
41                 MeasuredSize.StateType childWidthState = MeasuredSize.StateType.MeasuredSizeOK;
42                 MeasuredSize.StateType childHeightState = MeasuredSize.StateType.MeasuredSizeOK;
43
44                 Direction scrollingDirection = Direction.Vertical;
45                 ScrollableBase scrollableBase = this.Owner as ScrollableBase;
46                 if (scrollableBase)
47                 {
48                    scrollingDirection = scrollableBase.ScrollingDirection;
49                 }
50
51                 // measure child, should be a single scrolling child
52                 foreach( LayoutItem childLayout in LayoutChildren )
53                 {
54                     if (childLayout != null)
55                     {
56                         // Get size of child
57                         // Use an Unspecified MeasureSpecification mode so scrolling child is not restricted to it's parents size in Height (for vertical scrolling)
58                         // or Width for horizontal scrolling
59                         MeasureSpecification unrestrictedMeasureSpec = new MeasureSpecification( heightMeasureSpec.Size, MeasureSpecification.ModeType.Unspecified);
60
61                         if (scrollingDirection == Direction.Vertical)
62                         {
63                             MeasureChild( childLayout, widthMeasureSpec, unrestrictedMeasureSpec );  // Height unrestricted by parent
64                         }
65                         else
66                         {
67                             MeasureChild( childLayout, unrestrictedMeasureSpec, heightMeasureSpec );  // Width unrestricted by parent
68                         }
69
70                         float childWidth = childLayout.MeasuredWidth.Size.AsDecimal();
71                         float childHeight = childLayout.MeasuredHeight.Size.AsDecimal();
72
73                         // Determine the width and height needed by the children using their given position and size.
74                         // Children could overlap so find the left most and right most child.
75                         Position2D childPosition = childLayout.Owner.Position2D;
76                         float childLeft = childPosition.X;
77                         float childTop = childPosition.Y;
78
79                         // Store current width and height needed to contain all children.
80                         Extents childMargin = childLayout.Margin;
81                         totalWidth = childWidth + childMargin.Start + childMargin.End;
82                         totalHeight = childHeight + childMargin.Top + childMargin.Bottom;
83
84                         if (childLayout.MeasuredWidth.State == MeasuredSize.StateType.MeasuredSizeTooSmall)
85                         {
86                             childWidthState = MeasuredSize.StateType.MeasuredSizeTooSmall;
87                         }
88                         if (childLayout.MeasuredWidth.State == MeasuredSize.StateType.MeasuredSizeTooSmall)
89                         {
90                             childHeightState = MeasuredSize.StateType.MeasuredSizeTooSmall;
91                         }
92                     }
93                 }
94
95
96                 MeasuredSize widthSizeAndState = ResolveSizeAndState(new LayoutLength(totalWidth), widthMeasureSpec, MeasuredSize.StateType.MeasuredSizeOK);
97                 MeasuredSize heightSizeAndState = ResolveSizeAndState(new LayoutLength(totalHeight), heightMeasureSpec, MeasuredSize.StateType.MeasuredSizeOK);
98                 totalWidth = widthSizeAndState.Size.AsDecimal();
99                 totalHeight = heightSizeAndState.Size.AsDecimal();
100
101                 // Ensure layout respects it's given minimum size
102                 totalWidth = Math.Max( totalWidth, SuggestedMinimumWidth.AsDecimal() );
103                 totalHeight = Math.Max( totalHeight, SuggestedMinimumHeight.AsDecimal() );
104
105                 widthSizeAndState.State = childWidthState;
106                 heightSizeAndState.State = childHeightState;
107
108                 SetMeasuredDimensions( ResolveSizeAndState( new LayoutLength(totalWidth), widthMeasureSpec, childWidthState ),
109                                        ResolveSizeAndState( new LayoutLength(totalHeight), heightMeasureSpec, childHeightState ) );
110
111             }
112
113             protected override void OnLayout(bool changed, LayoutLength left, LayoutLength top, LayoutLength right, LayoutLength bottom)
114             {
115                 foreach( LayoutItem childLayout in LayoutChildren )
116                 {
117                     if( childLayout != null )
118                     {
119                         LayoutLength childWidth = childLayout.MeasuredWidth.Size;
120                         LayoutLength childHeight = childLayout.MeasuredHeight.Size;
121
122                         Position2D childPosition = childLayout.Owner.Position2D;
123                         Extents padding = Padding;
124                         Extents childMargin = childLayout.Margin;
125
126                         LayoutLength childLeft = new LayoutLength(childPosition.X + childMargin.Start + padding.Start);
127                         LayoutLength childTop = new LayoutLength(childPosition.Y + childMargin.Top + padding.Top);
128
129                         childLayout.Layout( childLeft, childTop, childLeft + childWidth, childTop + childHeight );
130                     }
131                 }
132             }
133         } //  ScrollableBaseCustomLayout
134
135         /// <summary>
136         /// The direction axis to scroll.
137         /// </summary>
138         /// <since_tizen> 6 </since_tizen>
139         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
140         [EditorBrowsable(EditorBrowsableState.Never)]
141         public enum Direction
142         {
143             /// <summary>
144             /// Horizontal axis.
145             /// </summary>
146             /// <since_tizen> 6 </since_tizen>
147             Horizontal,
148
149             /// <summary>
150             /// Vertical axis.
151             /// </summary>
152             /// <since_tizen> 6 </since_tizen>
153             Vertical
154         }
155
156         /// <summary>
157         /// [Draft] Configurable speed threshold that register the gestures as a flick.
158         /// If the flick speed less than the threshold then will not be considered a flick.
159         /// </summary>
160         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API.
161         [EditorBrowsable(EditorBrowsableState.Never)]
162         public float FlickThreshold { get; set; } = 0.2f;
163
164         /// <summary>
165         /// [Draft] Configurable duration modifer for the flick animation.
166         /// Determines the speed of the scroll, large value results in a longer flick animation. Range (0.1 - 1.0)
167         /// </summary>
168         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
169         [EditorBrowsable(EditorBrowsableState.Never)]
170         public float FlickAnimationSpeed { get; set; } = 0.4f;
171
172         /// <summary>
173         /// [Draft] Configurable modifer for the distance to be scrolled when flicked detected.
174         /// It a ratio of the ScrollableBase's length. (not child's length).
175         /// First value is the ratio of the distance to scroll with the weakest flick.
176         /// Second value is the ratio of the distance to scroll with the strongest flick.
177         /// Second > First.
178         /// </summary>
179         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
180         [EditorBrowsable(EditorBrowsableState.Never)]
181         public Vector2 FlickDistanceMultiplierRange { get; set; } = new Vector2(0.6f, 1.8f);
182
183         /// <summary>
184         /// [Draft] Scrolling direction mode.
185         /// Default is Vertical scrolling.
186         /// </summary>
187         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
188         [EditorBrowsable(EditorBrowsableState.Never)]
189         public Direction ScrollingDirection
190         {
191             get
192             {
193                 return mScrollingDirection;
194             }
195             set
196             {
197                 if(value != mScrollingDirection)
198                 {
199                     mScrollingDirection = value;
200                     mPanGestureDetector.RemoveDirection(value == Direction.Horizontal ? PanGestureDetector.DirectionVertical : PanGestureDetector.DirectionHorizontal);
201                     mPanGestureDetector.AddDirection(value == Direction.Horizontal ? PanGestureDetector.DirectionHorizontal : PanGestureDetector.DirectionVertical);
202                 }
203             }
204         }
205
206         /// <summary>
207         /// [Draft] Enable or disable scrolling.
208         /// </summary>
209         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
210         [EditorBrowsable(EditorBrowsableState.Never)]
211         public bool ScrollEnabled
212         {
213             get
214             {
215                 return mScrollEnabled;
216             }
217             set
218             {
219                 if (value != mScrollEnabled)
220                 {
221                     mScrollEnabled = value;
222                     if(mScrollEnabled)
223                     {
224                         mPanGestureDetector.Detected += OnPanGestureDetected;
225                         mTapGestureDetector.Detected += OnTapGestureDetected;
226                     }
227                     else
228                     {
229                         mPanGestureDetector.Detected -= OnPanGestureDetected;
230                         mTapGestureDetector.Detected -= OnTapGestureDetected;
231                     }
232                 }
233             }
234         }
235
236         /// <summary>
237         /// [Draft] Pages mode, enables moving to the next or return to current page depending on pan displacement.
238         /// Default is false.
239         /// </summary>
240         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
241         [EditorBrowsable(EditorBrowsableState.Never)]
242         public bool SnapToPage { set; get; } = false;
243
244         /// <summary>
245         /// [Draft] Get current page.
246         /// Working propery with SnapToPage property.
247         /// </summary>
248         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
249         [EditorBrowsable(EditorBrowsableState.Never)]
250         public int CurrentPage { get; private set; } = 0;
251
252         /// <summary>
253         /// [Draft] Pages mode, Number of pages.
254         /// </summary>
255         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
256         [EditorBrowsable(EditorBrowsableState.Never)]
257         public int NumberOfPages { set; get; } = 1;
258
259         /// <summary>
260         /// [Draft] Duration of scroll animation.
261         /// </summary>
262         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
263         [EditorBrowsable(EditorBrowsableState.Never)]
264         public int ScrollDuration { set; get; } = 125;
265
266         /// <summary>
267         /// [Draft] Width of the Page.
268         /// </summary>
269         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
270         [EditorBrowsable(EditorBrowsableState.Never)]
271         public int PageWidth { set; get; } = 1080; // Temporary use for prototype, should get ScrollableBase width
272
273         /// <summary>
274         /// ScrollEventArgs is a class to record scroll event arguments which will sent to user.
275         /// </summary>
276         /// <since_tizen> 6 </since_tizen>
277         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
278         [EditorBrowsable(EditorBrowsableState.Never)]
279         public class ScrollEventArgs : EventArgs
280         {
281         }
282
283         /// <summary>
284         /// An event emitted when the scrolling starts, user can subscribe or unsubscribe to this event handler.<br />
285         /// </summary>
286         /// <since_tizen> 6 </since_tizen>
287         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
288         [EditorBrowsable(EditorBrowsableState.Never)]
289         public event EventHandler<ScrollEventArgs> ScrollStartedEvent;
290
291         /// <summary>
292         /// An event emitted when the scrolling ends, user can subscribe or unsubscribe to this event handler.<br />
293         /// </summary>
294         /// <since_tizen> 6 </since_tizen>
295         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
296         [EditorBrowsable(EditorBrowsableState.Never)]
297         public event EventHandler<ScrollEventArgs> ScrollEndedEvent;
298
299         private Animation scrollAnimation;
300         private float maxScrollDistance;
301         private float childTargetPosition = 0.0f;
302         private PanGestureDetector mPanGestureDetector;
303         private TapGestureDetector mTapGestureDetector;
304         private View mScrollingChild;
305         private float multiplier =1.0f;
306         private bool scrolling = false;
307         private float ratioOfScreenWidthToCompleteScroll = 0.5f;
308         private float totalDisplacementForPan = 0.0f;
309
310         // If false then can only flick pages when the current animation/scroll as ended.
311         private bool flickWhenAnimating = false;
312
313         /// <summary>
314         /// [Draft] Constructor
315         /// </summary>
316         /// <since_tizen> 6 </since_tizen>
317         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
318         [EditorBrowsable(EditorBrowsableState.Never)]
319         public ScrollableBase() : base()
320         {
321             mPanGestureDetector = new PanGestureDetector();
322             mPanGestureDetector.Attach(this);
323             mPanGestureDetector.AddDirection(PanGestureDetector.DirectionVertical);
324             mPanGestureDetector.Detected += OnPanGestureDetected;
325
326             mTapGestureDetector = new TapGestureDetector();
327             mTapGestureDetector.Attach(this);
328             mTapGestureDetector.Detected += OnTapGestureDetected;
329
330             ClippingMode = ClippingModeType.ClipToBoundingBox;
331
332             mScrollingChild = new View();
333
334             Layout = new ScrollableBaseCustomLayout();
335         }
336
337         /// <summary>
338         /// Called after a child has been added to the owning view.
339         /// </summary>
340         /// <param name="view">The child which has been added.</param>
341         /// <since_tizen> 6 </since_tizen>
342         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
343         [EditorBrowsable(EditorBrowsableState.Never)]
344         public override void OnChildAdd(View view)
345         {
346             mScrollingChild = view;
347             {
348             if (Children.Count > 1)
349                 Log.Error("ScrollableBase", $"Only 1 child should be added to ScrollableBase.");
350             }
351         }
352
353         /// <summary>
354         /// Called after a child has been removed from the owning view.
355         /// </summary>
356         /// <param name="view">The child which has been removed.</param>
357         /// <since_tizen> 6 </since_tizen>
358         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
359         [EditorBrowsable(EditorBrowsableState.Never)]
360         public override void OnChildRemove(View view)
361         {
362             mScrollingChild = new View();
363         }
364
365         private void OnScrollStart()
366         {
367             ScrollEventArgs eventArgs = new ScrollEventArgs();
368             ScrollStartedEvent?.Invoke(this, eventArgs);
369         }
370
371         private void OnScrollEnd()
372         {
373             ScrollEventArgs eventArgs = new ScrollEventArgs();
374             ScrollEndedEvent?.Invoke(this, eventArgs);
375         }
376
377         private void StopScroll()
378         {
379             if (scrollAnimation != null)
380             {
381                 if (scrollAnimation.State == Animation.States.Playing)
382                 {
383                     Debug.WriteLineIf(LayoutDebugScrollableBase, "StopScroll Animation Playing");
384                     scrollAnimation.Stop(Animation.EndActions.Cancel);
385                     OnScrollEnd();
386                 }
387                 scrollAnimation.Clear();
388             }
389         }
390
391         // static constructor registers the control type
392         static ScrollableBase()
393         {
394             // ViewRegistry registers control type with DALi type registry
395             // also uses introspection to find any properties that need to be registered with type registry
396             CustomViewRegistry.Instance.Register(CreateInstance, typeof(ScrollableBase));
397         }
398
399         internal static CustomView CreateInstance()
400         {
401             return new ScrollableBase();
402         }
403
404         private void AnimateChildTo(int duration, float axisPosition)
405         {
406             Debug.WriteLineIf(LayoutDebugScrollableBase, "AnimationTo Animation Duration:" + duration + " Destination:" + axisPosition);
407
408             StopScroll(); // Will replace previous animation so will stop existing one.
409
410             if (scrollAnimation == null)
411             {
412                 scrollAnimation = new Animation();
413                 scrollAnimation.Finished += ScrollAnimationFinished;
414             }
415
416             scrollAnimation.Duration = duration;
417             scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.EaseOutSine);
418             scrollAnimation.AnimateTo(mScrollingChild, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", axisPosition);
419             scrolling = true;
420             OnScrollStart();
421             scrollAnimation.Play();
422         }
423
424         private void ScrollBy(float displacement, bool animate)
425         {
426             if (GetChildCount() == 0 || displacement == 0 || maxScrollDistance < 0)
427             {
428                 return;
429             }
430
431             float childCurrentPosition = (ScrollingDirection == Direction.Horizontal) ? mScrollingChild.PositionX: mScrollingChild.PositionY;
432
433             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy childCurrentPosition:" + childCurrentPosition +
434                                                    " displacement:" + displacement,
435                                                    " maxScrollDistance:" + maxScrollDistance );
436
437             childTargetPosition = childCurrentPosition + displacement; // child current position + gesture displacement
438             childTargetPosition = Math.Min(0,childTargetPosition);
439             childTargetPosition = Math.Max(-maxScrollDistance,childTargetPosition);
440
441             Debug.WriteLineIf( LayoutDebugScrollableBase, "ScrollBy currentAxisPosition:" + childCurrentPosition + "childTargetPosition:" + childTargetPosition);
442
443             if (animate)
444             {
445                 // Calculate scroll animaton duration
446                 float scrollDistance = 0.0f;
447                 if (childCurrentPosition < childTargetPosition)
448                 {
449                     scrollDistance = Math.Abs(childCurrentPosition + childTargetPosition);
450                 }
451                 else
452                 {
453                     scrollDistance = Math.Abs(childCurrentPosition - childTargetPosition);
454                 }
455
456                 int duration = (int)((320*FlickAnimationSpeed) + (scrollDistance * FlickAnimationSpeed));
457                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Scroll Animation Duration:" + duration + " Distance:" + scrollDistance);
458
459                 AnimateChildTo(duration, childTargetPosition);
460             }
461             else
462             {
463                 // Set position of scrolling child without an animation
464                 if (ScrollingDirection == Direction.Horizontal)
465                 {
466                     mScrollingChild.PositionX = childTargetPosition;
467                 }
468                 else
469                 {
470                     mScrollingChild.PositionY = childTargetPosition;
471                 }
472             }
473         }
474
475         /// <summary>
476         /// you can override it to clean-up your own resources.
477         /// </summary>
478         /// <param name="type">DisposeTypes</param>
479         /// <since_tizen> 6 </since_tizen>
480         /// This will be public opened in tizen_5.5 after ACR done. Before ACR, need to be hidden as inhouse API.
481         [EditorBrowsable(EditorBrowsableState.Never)]
482         protected override void Dispose(DisposeTypes type)
483         {
484             if (disposed)
485             {
486                 return;
487             }
488
489             if (type == DisposeTypes.Explicit)
490             {
491                 StopScroll();
492
493                 if (mPanGestureDetector != null)
494                 {
495                     mPanGestureDetector.Detected -= OnPanGestureDetected;
496                     mPanGestureDetector.Dispose();
497                     mPanGestureDetector = null;
498                 }
499
500                 if (mTapGestureDetector != null)
501                 {
502                     mTapGestureDetector.Detected -= OnTapGestureDetected;
503                     mTapGestureDetector.Dispose();
504                     mTapGestureDetector = null;
505                 }
506             }
507             base.Dispose(type);
508         }
509
510         private float CalculateDisplacementFromVelocity(float axisVelocity)
511         {
512             // Map: flick speed of range (2.0 - 6.0) to flick multiplier of range (0.7 - 1.6)
513             float speedMinimum = FlickThreshold;
514             float speedMaximum = FlickThreshold + 6.0f;
515             float multiplierMinimum = FlickDistanceMultiplierRange.X;
516             float multiplierMaximum = FlickDistanceMultiplierRange.Y;
517
518             float flickDisplacement = 0.0f;
519
520             float speed = Math.Min(4.0f,Math.Abs(axisVelocity));
521
522             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollableBase Candidate Flick speed:" + speed);
523
524             if (speed > FlickThreshold)
525             {
526                 // Flick length is the length of the ScrollableBase.
527                 float flickLength = (ScrollingDirection == Direction.Horizontal) ?CurrentSize.Width:CurrentSize.Height;
528
529                 // Calculate multiplier by mapping speed between the multiplier minimum and maximum.
530                 multiplier =( (speed - speedMinimum) / ( (speedMaximum - speedMinimum) * (multiplierMaximum - multiplierMinimum) ) )+ multiplierMinimum;
531
532                 // flick displacement is the product of the flick length and multiplier
533                 flickDisplacement = ((flickLength * multiplier) * speed) / axisVelocity;  // *speed and /velocity to perserve sign.
534
535                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Calculated FlickDisplacement[" + flickDisplacement +"] from speed[" + speed + "] multiplier:"
536                                                         + multiplier);
537             }
538             return flickDisplacement;
539         }
540
541         private float CalculateMaximumScrollDistance()
542         {
543             int scrollingChildLength = 0;
544             int scrollerLength = 0;
545             if (ScrollingDirection == Direction.Horizontal)
546             {
547                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Horizontal");
548
549                 scrollingChildLength = (int)mScrollingChild.Layout.MeasuredWidth.Size.AsRoundedValue();
550                 scrollerLength = CurrentSize.Width;
551             }
552             else
553             {
554                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Vertical");
555                 scrollingChildLength = (int)mScrollingChild.Layout.MeasuredHeight.Size.AsRoundedValue();
556                 scrollerLength = CurrentSize.Height;
557             }
558
559             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy maxScrollDistance:" + (scrollingChildLength - scrollerLength) +
560                                                    " parent length:" + scrollerLength +
561                                                    " scrolling child length:" + scrollingChildLength);
562
563             return scrollingChildLength - scrollerLength;
564         }
565
566         private void PageSnap()
567         {
568             Debug.WriteLineIf(LayoutDebugScrollableBase, "PageSnap with pan candidate totalDisplacement:" + totalDisplacementForPan +
569                                                                 " currentPage[" + CurrentPage + "]" );
570
571             //Increment current page if total displacement enough to warrant a page change.
572             if (Math.Abs(totalDisplacementForPan) > (PageWidth * ratioOfScreenWidthToCompleteScroll))
573             {
574                 if (totalDisplacementForPan < 0)
575                 {
576                     CurrentPage = Math.Min(NumberOfPages - 1, ++CurrentPage);
577                 }
578                 else
579                 {
580                     CurrentPage = Math.Max(0, --CurrentPage);
581                 }
582             }
583
584             // Animate to new page or reposition to current page
585             int destinationX = -(CurrentPage * PageWidth);
586             Debug.WriteLineIf(LayoutDebugScrollableBase, "Snapping to page[" + CurrentPage + "] to:"+ destinationX + " from:" + mScrollingChild.PositionX);
587             AnimateChildTo(ScrollDuration, destinationX);
588         }
589
590         private void Flick(float flickDisplacement)
591         {
592           if (SnapToPage)
593           {
594               if ( ( flickWhenAnimating && scrolling == true) || ( scrolling == false) )
595               {
596                   if(flickDisplacement < 0)
597                   {
598                       CurrentPage = Math.Min(NumberOfPages - 1, CurrentPage + 1);
599                       Debug.WriteLineIf(LayoutDebugScrollableBase, "Snap - to page:" + CurrentPage);
600                   }
601                   else
602                   {
603                       CurrentPage = Math.Max(0, CurrentPage - 1);
604                       Debug.WriteLineIf(LayoutDebugScrollableBase, "Snap + to page:" + CurrentPage);
605                   }
606                   float targetPosition = -(CurrentPage* PageWidth); // page size
607                   Debug.WriteLineIf(LayoutDebugScrollableBase, "Snapping to :" + targetPosition);
608                   AnimateChildTo(ScrollDuration,targetPosition);
609               }
610           }
611           else
612           {
613               ScrollBy(flickDisplacement, true); // Animate flickDisplacement.
614           }
615         }
616
617         private void OnPanGestureDetected(object source, PanGestureDetector.DetectedEventArgs e)
618         {
619             if (e.PanGesture.State == Gesture.StateType.Started)
620             {
621                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Gesture Start");
622                 if (scrolling && !SnapToPage)
623                 {
624                     StopScroll();
625                 }
626                 maxScrollDistance = CalculateMaximumScrollDistance();
627                 totalDisplacementForPan = 0.0f;
628             }
629             else if (e.PanGesture.State == Gesture.StateType.Continuing)
630             {
631                 if (ScrollingDirection == Direction.Horizontal)
632                 {
633                     ScrollBy(e.PanGesture.Displacement.X, false);
634                     totalDisplacementForPan += e.PanGesture.Displacement.X;
635                 }
636                 else
637                 {
638                     ScrollBy(e.PanGesture.Displacement.Y, false);
639                     totalDisplacementForPan += e.PanGesture.Displacement.Y;
640                 }
641                 Debug.WriteLineIf(LayoutDebugScrollableBase, "OnPanGestureDetected Continue totalDisplacementForPan:" + totalDisplacementForPan);
642
643             }
644             else if (e.PanGesture.State == Gesture.StateType.Finished)
645             {
646                 float axisVelocity = (ScrollingDirection == Direction.Horizontal) ? e.PanGesture.Velocity.X : e.PanGesture.Velocity.Y;
647                 float flickDisplacement = CalculateDisplacementFromVelocity(axisVelocity);
648
649                 Debug.WriteLineIf(LayoutDebugScrollableBase, "FlickDisplacement:" + flickDisplacement + "TotalDisplacementForPan:" + totalDisplacementForPan);
650
651                 if (flickDisplacement > 0 | flickDisplacement < 0)// Flick detected
652                 {
653                     Flick(flickDisplacement);
654                 }
655                 else
656                 {
657                     // End of panning gesture but was not a flick
658                     if (SnapToPage)
659                     {
660                         PageSnap();
661                     }
662                 }
663                 totalDisplacementForPan = 0;
664             }
665         }
666
667         private new void OnTapGestureDetected(object source, TapGestureDetector.DetectedEventArgs e)
668         {
669             if (e.TapGesture.Type == Gesture.GestureType.Tap)
670             {
671                 // Stop scrolling if tap detected (press then relase).
672                 // Unless in Pages mode, do not want a page change to stop part way.
673                 if(scrolling && !SnapToPage)
674                 {
675                     StopScroll();
676                 }
677             }
678         }
679
680         private void ScrollAnimationFinished(object sender, EventArgs e)
681         {
682             scrolling = false;
683             OnScrollEnd();
684         }
685
686     }
687
688 } // namespace