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