[NUI] Add/change scroll event (#1329)
[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         private void OnScrollDragStart()
385         {
386             ScrollEventArgs eventArgs = new ScrollEventArgs();
387             ScrollDragStartEvent?.Invoke(this, eventArgs);
388         }
389
390         private void OnScrollDragEnd()
391         {
392             ScrollEventArgs eventArgs = new ScrollEventArgs();
393             ScrollDragEndEvent?.Invoke(this, eventArgs);
394         }
395
396         private void OnScrollAnimationStart()
397         {
398             ScrollEventArgs eventArgs = new ScrollEventArgs();
399             ScrollAnimationStartEvent?.Invoke(this, eventArgs);
400         }
401
402         private void OnScrollAnimationEnd()
403         {
404             ScrollEventArgs eventArgs = new ScrollEventArgs();
405             ScrollAnimationEndEvent?.Invoke(this, eventArgs);
406         }
407
408         private void StopScroll()
409         {
410             if (scrollAnimation != null)
411             {
412                 if (scrollAnimation.State == Animation.States.Playing)
413                 {
414                     Debug.WriteLineIf(LayoutDebugScrollableBase, "StopScroll Animation Playing");
415                     scrollAnimation.Stop(Animation.EndActions.Cancel);
416                     OnScrollAnimationEnd();
417                 }
418                 scrollAnimation.Clear();
419             }
420         }
421
422         // static constructor registers the control type
423         static ScrollableBase()
424         {
425             // ViewRegistry registers control type with DALi type registry
426             // also uses introspection to find any properties that need to be registered with type registry
427             CustomViewRegistry.Instance.Register(CreateInstance, typeof(ScrollableBase));
428         }
429
430         internal static CustomView CreateInstance()
431         {
432             return new ScrollableBase();
433         }
434
435         private void AnimateChildTo(int duration, float axisPosition)
436         {
437             Debug.WriteLineIf(LayoutDebugScrollableBase, "AnimationTo Animation Duration:" + duration + " Destination:" + axisPosition);
438
439             StopScroll(); // Will replace previous animation so will stop existing one.
440
441             if (scrollAnimation == null)
442             {
443                 scrollAnimation = new Animation();
444                 scrollAnimation.Finished += ScrollAnimationFinished;
445             }
446
447             scrollAnimation.Duration = duration;
448             scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.EaseOutSine);
449             scrollAnimation.AnimateTo(mScrollingChild, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", axisPosition);
450             scrolling = true;
451             OnScrollAnimationStart();
452             scrollAnimation.Play();
453         }
454
455         private void ScrollBy(float displacement, bool animate)
456         {
457             if (GetChildCount() == 0 || displacement == 0 || maxScrollDistance < 0)
458             {
459                 return;
460             }
461
462             float childCurrentPosition = (ScrollingDirection == Direction.Horizontal) ? mScrollingChild.PositionX: mScrollingChild.PositionY;
463
464             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy childCurrentPosition:" + childCurrentPosition +
465                                                    " displacement:" + displacement,
466                                                    " maxScrollDistance:" + maxScrollDistance );
467
468             childTargetPosition = childCurrentPosition + displacement; // child current position + gesture displacement
469             childTargetPosition = Math.Min(0,childTargetPosition);
470             childTargetPosition = Math.Max(-maxScrollDistance,childTargetPosition);
471
472             Debug.WriteLineIf( LayoutDebugScrollableBase, "ScrollBy currentAxisPosition:" + childCurrentPosition + "childTargetPosition:" + childTargetPosition);
473
474             if (animate)
475             {
476                 // Calculate scroll animaton duration
477                 float scrollDistance = 0.0f;
478                 if (childCurrentPosition < childTargetPosition)
479                 {
480                     scrollDistance = Math.Abs(childCurrentPosition + childTargetPosition);
481                 }
482                 else
483                 {
484                     scrollDistance = Math.Abs(childCurrentPosition - childTargetPosition);
485                 }
486
487                 int duration = (int)((320*FlickAnimationSpeed) + (scrollDistance * FlickAnimationSpeed));
488                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Scroll Animation Duration:" + duration + " Distance:" + scrollDistance);
489
490                 AnimateChildTo(duration, childTargetPosition);
491             }
492             else
493             {
494                 // Set position of scrolling child without an animation
495                 if (ScrollingDirection == Direction.Horizontal)
496                 {
497                     mScrollingChild.PositionX = childTargetPosition;
498                 }
499                 else
500                 {
501                     mScrollingChild.PositionY = childTargetPosition;
502                 }
503             }
504         }
505
506         /// <summary>
507         /// you can override it to clean-up your own resources.
508         /// </summary>
509         /// <param name="type">DisposeTypes</param>
510         /// <since_tizen> 6 </since_tizen>
511         /// This will be public opened in tizen_5.5 after ACR done. Before ACR, need to be hidden as inhouse API.
512         [EditorBrowsable(EditorBrowsableState.Never)]
513         protected override void Dispose(DisposeTypes type)
514         {
515             if (disposed)
516             {
517                 return;
518             }
519
520             if (type == DisposeTypes.Explicit)
521             {
522                 StopScroll();
523
524                 if (mPanGestureDetector != null)
525                 {
526                     mPanGestureDetector.Detected -= OnPanGestureDetected;
527                     mPanGestureDetector.Dispose();
528                     mPanGestureDetector = null;
529                 }
530
531                 if (mTapGestureDetector != null)
532                 {
533                     mTapGestureDetector.Detected -= OnTapGestureDetected;
534                     mTapGestureDetector.Dispose();
535                     mTapGestureDetector = null;
536                 }
537             }
538             base.Dispose(type);
539         }
540
541         private float CalculateDisplacementFromVelocity(float axisVelocity)
542         {
543             // Map: flick speed of range (2.0 - 6.0) to flick multiplier of range (0.7 - 1.6)
544             float speedMinimum = FlickThreshold;
545             float speedMaximum = FlickThreshold + 6.0f;
546             float multiplierMinimum = FlickDistanceMultiplierRange.X;
547             float multiplierMaximum = FlickDistanceMultiplierRange.Y;
548
549             float flickDisplacement = 0.0f;
550
551             float speed = Math.Min(4.0f,Math.Abs(axisVelocity));
552
553             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollableBase Candidate Flick speed:" + speed);
554
555             if (speed > FlickThreshold)
556             {
557                 // Flick length is the length of the ScrollableBase.
558                 float flickLength = (ScrollingDirection == Direction.Horizontal) ?CurrentSize.Width:CurrentSize.Height;
559
560                 // Calculate multiplier by mapping speed between the multiplier minimum and maximum.
561                 multiplier =( (speed - speedMinimum) / ( (speedMaximum - speedMinimum) * (multiplierMaximum - multiplierMinimum) ) )+ multiplierMinimum;
562
563                 // flick displacement is the product of the flick length and multiplier
564                 flickDisplacement = ((flickLength * multiplier) * speed) / axisVelocity;  // *speed and /velocity to perserve sign.
565
566                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Calculated FlickDisplacement[" + flickDisplacement +"] from speed[" + speed + "] multiplier:"
567                                                         + multiplier);
568             }
569             return flickDisplacement;
570         }
571
572         private float CalculateMaximumScrollDistance()
573         {
574             int scrollingChildLength = 0;
575             int scrollerLength = 0;
576             if (ScrollingDirection == Direction.Horizontal)
577             {
578                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Horizontal");
579
580                 scrollingChildLength = (int)mScrollingChild.Layout.MeasuredWidth.Size.AsRoundedValue();
581                 scrollerLength = CurrentSize.Width;
582             }
583             else
584             {
585                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Vertical");
586                 scrollingChildLength = (int)mScrollingChild.Layout.MeasuredHeight.Size.AsRoundedValue();
587                 scrollerLength = CurrentSize.Height;
588             }
589
590             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy maxScrollDistance:" + (scrollingChildLength - scrollerLength) +
591                                                    " parent length:" + scrollerLength +
592                                                    " scrolling child length:" + scrollingChildLength);
593
594             return scrollingChildLength - scrollerLength;
595         }
596
597         private void PageSnap()
598         {
599             Debug.WriteLineIf(LayoutDebugScrollableBase, "PageSnap with pan candidate totalDisplacement:" + totalDisplacementForPan +
600                                                                 " currentPage[" + CurrentPage + "]" );
601
602             //Increment current page if total displacement enough to warrant a page change.
603             if (Math.Abs(totalDisplacementForPan) > (PageWidth * ratioOfScreenWidthToCompleteScroll))
604             {
605                 if (totalDisplacementForPan < 0)
606                 {
607                     CurrentPage = Math.Min(NumberOfPages - 1, ++CurrentPage);
608                 }
609                 else
610                 {
611                     CurrentPage = Math.Max(0, --CurrentPage);
612                 }
613             }
614
615             // Animate to new page or reposition to current page
616             int destinationX = -(CurrentPage * PageWidth);
617             Debug.WriteLineIf(LayoutDebugScrollableBase, "Snapping to page[" + CurrentPage + "] to:"+ destinationX + " from:" + mScrollingChild.PositionX);
618             AnimateChildTo(ScrollDuration, destinationX);
619         }
620
621         private void Flick(float flickDisplacement)
622         {
623           if (SnapToPage)
624           {
625               if ( ( flickWhenAnimating && scrolling == true) || ( scrolling == false) )
626               {
627                   if(flickDisplacement < 0)
628                   {
629                       CurrentPage = Math.Min(NumberOfPages - 1, CurrentPage + 1);
630                       Debug.WriteLineIf(LayoutDebugScrollableBase, "Snap - to page:" + CurrentPage);
631                   }
632                   else
633                   {
634                       CurrentPage = Math.Max(0, CurrentPage - 1);
635                       Debug.WriteLineIf(LayoutDebugScrollableBase, "Snap + to page:" + CurrentPage);
636                   }
637                   float targetPosition = -(CurrentPage* PageWidth); // page size
638                   Debug.WriteLineIf(LayoutDebugScrollableBase, "Snapping to :" + targetPosition);
639                   AnimateChildTo(ScrollDuration,targetPosition);
640               }
641           }
642           else
643           {
644               ScrollBy(flickDisplacement, true); // Animate flickDisplacement.
645           }
646         }
647
648         private void OnPanGestureDetected(object source, PanGestureDetector.DetectedEventArgs e)
649         {
650             if (e.PanGesture.State == Gesture.StateType.Started)
651             {
652                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Gesture Start");
653                 if (scrolling && !SnapToPage)
654                 {
655                     StopScroll();
656                 }
657                 maxScrollDistance = CalculateMaximumScrollDistance();
658                 totalDisplacementForPan = 0.0f;
659                 OnScrollDragStart();
660             }
661             else if (e.PanGesture.State == Gesture.StateType.Continuing)
662             {
663                 if (ScrollingDirection == Direction.Horizontal)
664                 {
665                     ScrollBy(e.PanGesture.Displacement.X, false);
666                     totalDisplacementForPan += e.PanGesture.Displacement.X;
667                 }
668                 else
669                 {
670                     ScrollBy(e.PanGesture.Displacement.Y, false);
671                     totalDisplacementForPan += e.PanGesture.Displacement.Y;
672                 }
673                 Debug.WriteLineIf(LayoutDebugScrollableBase, "OnPanGestureDetected Continue totalDisplacementForPan:" + totalDisplacementForPan);
674
675             }
676             else if (e.PanGesture.State == Gesture.StateType.Finished)
677             {
678                 float axisVelocity = (ScrollingDirection == Direction.Horizontal) ? e.PanGesture.Velocity.X : e.PanGesture.Velocity.Y;
679                 float flickDisplacement = CalculateDisplacementFromVelocity(axisVelocity);
680
681                 Debug.WriteLineIf(LayoutDebugScrollableBase, "FlickDisplacement:" + flickDisplacement + "TotalDisplacementForPan:" + totalDisplacementForPan);
682                 OnScrollDragEnd();
683
684                 if (flickDisplacement > 0 | flickDisplacement < 0)// Flick detected
685                 {
686                     Flick(flickDisplacement);
687                 }
688                 else
689                 {
690                     // End of panning gesture but was not a flick
691                     if (SnapToPage)
692                     {
693                         PageSnap();
694                     }
695                 }
696                 totalDisplacementForPan = 0;
697             }
698         }
699
700         private new void OnTapGestureDetected(object source, TapGestureDetector.DetectedEventArgs e)
701         {
702             if (e.TapGesture.Type == Gesture.GestureType.Tap)
703             {
704                 // Stop scrolling if tap detected (press then relase).
705                 // Unless in Pages mode, do not want a page change to stop part way.
706                 if(scrolling && !SnapToPage)
707                 {
708                     StopScroll();
709                 }
710             }
711         }
712
713         private void ScrollAnimationFinished(object sender, EventArgs e)
714         {
715             scrolling = false;
716             OnScrollAnimationEnd();
717         }
718
719     }
720
721 } // namespace