[NUI] Layout scroller pages api6 (#1185)
[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.Detected += OnPanGestureDetected;
287
288             mTapGestureDetector = new TapGestureDetector();
289             mTapGestureDetector.Attach(this);
290             mTapGestureDetector.Detected += OnTapGestureDetected;
291
292             ClippingMode = ClippingModeType.ClipToBoundingBox;
293
294             mScrollingChild = new View();
295
296             Layout = new ScrollableBaseCustomLayout();
297         }
298
299         /// <summary>
300         /// Called after a child has been added to the owning view.
301         /// </summary>
302         /// <param name="view">The child which has been added.</param>
303         /// <since_tizen> 6 </since_tizen>
304         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
305         [EditorBrowsable(EditorBrowsableState.Never)]
306         public override void OnChildAdd(View view)
307         {
308             mScrollingChild = view;
309             {
310             if (Children.Count > 1)
311                 Log.Error("ScrollableBase", $"Only 1 child should be added to ScrollableBase.");
312             }
313         }
314
315         /// <summary>
316         /// Called after a child has been removed from the owning view.
317         /// </summary>
318         /// <param name="view">The child which has been removed.</param>
319         /// <since_tizen> 6 </since_tizen>
320         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
321         [EditorBrowsable(EditorBrowsableState.Never)]
322         public override void OnChildRemove(View view)
323         {
324             mScrollingChild = new View();
325         }
326
327         private void OnScrollStart()
328         {
329             ScrollEventArgs eventArgs = new ScrollEventArgs();
330             ScrollStartedEvent?.Invoke(this, eventArgs);
331         }
332
333         private void OnScrollEnd()
334         {
335             ScrollEventArgs eventArgs = new ScrollEventArgs();
336             ScrollEndedEvent?.Invoke(this, eventArgs);
337         }
338
339         private void StopScroll()
340         {
341             if (scrollAnimation != null)
342             {
343                 if (scrollAnimation.State == Animation.States.Playing)
344                 {
345                     Debug.WriteLineIf(LayoutDebugScrollableBase, "StopScroll Animation Playing");
346                     scrollAnimation.Stop(Animation.EndActions.Cancel);
347                     OnScrollEnd();
348                 }
349                 scrollAnimation.Clear();
350             }
351         }
352
353         // static constructor registers the control type
354         static ScrollableBase()
355         {
356             // ViewRegistry registers control type with DALi type registry
357             // also uses introspection to find any properties that need to be registered with type registry
358             CustomViewRegistry.Instance.Register(CreateInstance, typeof(ScrollableBase));
359         }
360
361         internal static CustomView CreateInstance()
362         {
363             return new ScrollableBase();
364         }
365
366         private void AnimateChildTo(int duration, float axisPosition)
367         {
368             Debug.WriteLineIf(LayoutDebugScrollableBase, "AnimationTo Animation Duration:" + duration + " Destination:" + axisPosition);
369
370             StopScroll(); // Will replace previous animation so will stop existing one.
371
372             if (scrollAnimation == null)
373             {
374                 scrollAnimation = new Animation();
375                 scrollAnimation.Finished += ScrollAnimationFinished;
376             }
377
378             scrollAnimation.Duration = duration;
379             scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.EaseOutSine);
380             scrollAnimation.AnimateTo(mScrollingChild, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", axisPosition);
381             scrolling = true;
382             OnScrollStart();
383             scrollAnimation.Play();
384         }
385
386         private void ScrollBy(float displacement, bool animate)
387         {
388             if (GetChildCount() == 0 || displacement == 0)
389             {
390                 return;
391             }
392
393             float childCurrentPosition = (ScrollingDirection == Direction.Horizontal) ? mScrollingChild.PositionX: mScrollingChild.PositionY;
394
395             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy childCurrentPosition:" + childCurrentPosition +
396                                                    " displacement:" + displacement,
397                                                    " maxScrollDistance:" + maxScrollDistance );
398
399             childTargetPosition = childCurrentPosition + displacement; // child current position + gesture displacement
400             childTargetPosition = Math.Min(0,childTargetPosition);
401             childTargetPosition = Math.Max(-maxScrollDistance,childTargetPosition);
402
403             Debug.WriteLineIf( LayoutDebugScrollableBase, "ScrollBy currentAxisPosition:" + childCurrentPosition + "childTargetPosition:" + childTargetPosition);
404
405             if (animate)
406             {
407                 // Calculate scroll animaton duration
408                 float scrollDistance = 0.0f;
409                 if (childCurrentPosition < childTargetPosition)
410                 {
411                     scrollDistance = Math.Abs(childCurrentPosition + childTargetPosition);
412                 }
413                 else
414                 {
415                     scrollDistance = Math.Abs(childCurrentPosition - childTargetPosition);
416                 }
417
418                 int duration = (int)((320*FlickAnimationSpeed) + (scrollDistance * FlickAnimationSpeed));
419                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Scroll Animation Duration:" + duration + " Distance:" + scrollDistance);
420
421                 AnimateChildTo(duration, childTargetPosition);
422             }
423             else
424             {
425                 // Set position of scrolling child without an animation
426                 if (ScrollingDirection == Direction.Horizontal)
427                 {
428                     mScrollingChild.PositionX = childTargetPosition;
429                 }
430                 else
431                 {
432                     mScrollingChild.PositionY = childTargetPosition;
433                 }
434             }
435         }
436
437         /// <summary>
438         /// you can override it to clean-up your own resources.
439         /// </summary>
440         /// <param name="type">DisposeTypes</param>
441         /// <since_tizen> 6 </since_tizen>
442         /// This will be public opened in tizen_5.5 after ACR done. Before ACR, need to be hidden as inhouse API.
443         [EditorBrowsable(EditorBrowsableState.Never)]
444         protected override void Dispose(DisposeTypes type)
445         {
446             if (disposed)
447             {
448                 return;
449             }
450
451             if (type == DisposeTypes.Explicit)
452             {
453                 StopScroll();
454
455                 if (mPanGestureDetector != null)
456                 {
457                     mPanGestureDetector.Detected -= OnPanGestureDetected;
458                     mPanGestureDetector.Dispose();
459                     mPanGestureDetector = null;
460                 }
461
462                 if (mTapGestureDetector != null)
463                 {
464                     mTapGestureDetector.Detected -= OnTapGestureDetected;
465                     mTapGestureDetector.Dispose();
466                     mTapGestureDetector = null;
467                 }
468             }
469             base.Dispose(type);
470         }
471
472         private float CalculateDisplacementFromVelocity(float axisVelocity)
473         {
474             // Map: flick speed of range (2.0 - 6.0) to flick multiplier of range (0.7 - 1.6)
475             float speedMinimum = FlickThreshold;
476             float speedMaximum = FlickThreshold + 6.0f;
477             float multiplierMinimum = FlickDistanceMultiplierRange.X;
478             float multiplierMaximum = FlickDistanceMultiplierRange.Y;
479
480             float flickDisplacement = 0.0f;
481
482             float speed = Math.Min(4.0f,Math.Abs(axisVelocity));
483
484             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollableBase Candidate Flick speed:" + speed);
485
486             if (speed > FlickThreshold)
487             {
488                 // Flick length is the length of the ScrollableBase.
489                 float flickLength = (ScrollingDirection == Direction.Horizontal) ?CurrentSize.Width:CurrentSize.Height;
490
491                 // Calculate multiplier by mapping speed between the multiplier minimum and maximum.
492                 multiplier =( (speed - speedMinimum) / ( (speedMaximum - speedMinimum) * (multiplierMaximum - multiplierMinimum) ) )+ multiplierMinimum;
493
494                 // flick displacement is the product of the flick length and multiplier
495                 flickDisplacement = ((flickLength * multiplier) * speed) / axisVelocity;  // *speed and /velocity to perserve sign.
496
497                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Calculated FlickDisplacement[" + flickDisplacement +"] from speed[" + speed + "] multiplier:"
498                                                         + multiplier);
499             }
500             return flickDisplacement;
501         }
502
503         private float CalculateMaximumScrollDistance()
504         {
505             int scrollingChildLength = 0;
506             int scrollerLength = 0;
507             if (ScrollingDirection == Direction.Horizontal)
508             {
509                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Horizontal");
510
511                 scrollingChildLength = (int)mScrollingChild.Layout.MeasuredWidth.Size.AsRoundedValue();
512                 scrollerLength = CurrentSize.Width;
513             }
514             else
515             {
516                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Vertical");
517                 scrollingChildLength = (int)mScrollingChild.Layout.MeasuredHeight.Size.AsRoundedValue();
518                 scrollerLength = CurrentSize.Height;
519             }
520
521             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy maxScrollDistance:" + (scrollingChildLength - scrollerLength) +
522                                                    " parent length:" + scrollerLength +
523                                                    " scrolling child length:" + scrollingChildLength);
524
525             return scrollingChildLength - scrollerLength;
526         }
527
528         private void PageSnap()
529         {
530             Debug.WriteLineIf(LayoutDebugScrollableBase, "PageSnap with pan candidate totalDisplacement:" + totalDisplacementForPan +
531                                                                 " currentPage[" + currentPage + "]" );
532
533             //Increment current page if total displacement enough to warrant a page change.
534             if (Math.Abs(totalDisplacementForPan) > (PageWidth * ratioOfScreenWidthToCompleteScroll))
535             {
536                 if (totalDisplacementForPan < 0)
537                 {
538                     currentPage = Math.Min(NumberOfPages-1, ++currentPage);
539                 }
540                 else
541                 {
542                     currentPage = Math.Max(0, --currentPage);
543                 }
544             }
545
546             // Animate to new page or reposition to current page
547             int destinationX = -(currentPage * PageWidth);
548             Debug.WriteLineIf(LayoutDebugScrollableBase, "Snapping to page[" + currentPage + "] to:"+ destinationX + " from:" + mScrollingChild.PositionX);
549             AnimateChildTo(ScrollDuration, destinationX);
550         }
551
552         private void Flick(float flickDisplacement)
553         {
554           if (SnapToPage)
555           {
556               if ( ( flickWhenAnimating && scrolling == true) || ( scrolling == false) )
557               {
558                   if(flickDisplacement < 0)
559                   {
560                       currentPage = Math.Min(NumberOfPages - 1, currentPage + 1);
561                       Debug.WriteLineIf(LayoutDebugScrollableBase, "Snap - to page:" + currentPage);
562                   }
563                   else
564                   {
565                       currentPage = Math.Max(0, currentPage - 1);
566                       Debug.WriteLineIf(LayoutDebugScrollableBase, "Snap + to page:" + currentPage);
567                   }
568                   float targetPosition = -(currentPage* PageWidth); // page size
569                   Debug.WriteLineIf(LayoutDebugScrollableBase, "Snapping to :" + targetPosition);
570                   AnimateChildTo(ScrollDuration,targetPosition);
571               }
572           }
573           else
574           {
575               ScrollBy(flickDisplacement, true); // Animate flickDisplacement.
576           }
577         }
578
579         private void OnPanGestureDetected(object source, PanGestureDetector.DetectedEventArgs e)
580         {
581             if (e.PanGesture.State == Gesture.StateType.Started)
582             {
583                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Gesture Start");
584                 if (scrolling && !SnapToPage)
585                 {
586                     StopScroll();
587                 }
588                 maxScrollDistance = CalculateMaximumScrollDistance();
589                 totalDisplacementForPan = 0.0f;
590             }
591             else if (e.PanGesture.State == Gesture.StateType.Continuing)
592             {
593                 if (ScrollingDirection == Direction.Horizontal)
594                 {
595                     ScrollBy(e.PanGesture.Displacement.X, false);
596                     totalDisplacementForPan += e.PanGesture.Displacement.X;
597                 }
598                 else
599                 {
600                     ScrollBy(e.PanGesture.Displacement.Y, false);
601                     totalDisplacementForPan += e.PanGesture.Displacement.Y;
602                 }
603                 Debug.WriteLineIf(LayoutDebugScrollableBase, "OnPanGestureDetected Continue totalDisplacementForPan:" + totalDisplacementForPan);
604
605             }
606             else if (e.PanGesture.State == Gesture.StateType.Finished)
607             {
608                 float axisVelocity = (ScrollingDirection == Direction.Horizontal) ? e.PanGesture.Velocity.X : e.PanGesture.Velocity.Y;
609                 float flickDisplacement = CalculateDisplacementFromVelocity(axisVelocity);
610
611                 Debug.WriteLineIf(LayoutDebugScrollableBase, "FlickDisplacement:" + flickDisplacement + "TotalDisplacementForPan:" + totalDisplacementForPan);
612
613                 if (flickDisplacement > 0 | flickDisplacement < 0)// Flick detected
614                 {
615                     Flick(flickDisplacement);
616                 }
617                 else
618                 {
619                     // End of panning gesture but was not a flick
620                     if (SnapToPage)
621                     {
622                         PageSnap();
623                     }
624                 }
625                 totalDisplacementForPan = 0;
626             }
627         }
628
629         private void OnTapGestureDetected(object source, TapGestureDetector.DetectedEventArgs e)
630         {
631             if (e.TapGesture.Type == Gesture.GestureType.Tap)
632             {
633                 // Stop scrolling if tap detected (press then relase).
634                 // Unless in Pages mode, do not want a page change to stop part way.
635                 if(scrolling && !SnapToPage)
636                 {
637                     StopScroll();
638                 }
639             }
640         }
641
642         private void ScrollAnimationFinished(object sender, EventArgs e)
643         {
644             scrolling = false;
645             OnScrollEnd();
646         }
647
648     }
649
650 } // namespace