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