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