[NUI] Add the default Accessibility attribute trait (#1924)
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI.Components / Controls / ScrollableBase.cs
1 /* Copyright (c) 2020 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.Collections.Generic;
19 using System.ComponentModel;
20 using System.Diagnostics;
21 using System.Runtime.InteropServices;
22 using Tizen.NUI.Accessibility;
23
24 namespace Tizen.NUI.Components
25 {
26     /// <summary>
27     /// ScrollEventArgs is a class to record scroll event arguments which will sent to user.
28     /// </summary>
29     /// <since_tizen> 8 </since_tizen>
30     public class ScrollEventArgs : EventArgs
31     {
32         private Position position;
33
34         /// <summary>
35         /// Default constructor.
36         /// </summary>
37         /// <param name="position">Current scroll position</param>
38         /// <since_tizen> 8 </since_tizen>
39         public ScrollEventArgs(Position position)
40         {
41             this.position = position;
42         }
43
44         /// <summary>
45         /// Current position of ContentContainer.
46         /// </summary>
47         /// <since_tizen> 8 </since_tizen>
48         public Position Position
49         {
50             get
51             {
52                 return position;
53             }
54         }
55     }
56
57     /// <summary>
58     /// This class provides a View that can scroll a single View with a layout. This View can be a nest of Views.
59     /// </summary>
60     /// <since_tizen> 8 </since_tizen>
61     public class ScrollableBase : Control
62     {
63         static bool LayoutDebugScrollableBase = false; // Debug flag
64         private Direction mScrollingDirection = Direction.Vertical;
65         private bool mScrollEnabled = true;
66         private int mScrollDuration = 125;
67         private int mPageWidth = 0;
68         private float mPageFlickThreshold = 0.4f;
69
70         private class ScrollableBaseCustomLayout : LayoutGroup
71         {
72             protected override void OnMeasure(MeasureSpecification widthMeasureSpec, MeasureSpecification heightMeasureSpec)
73             {
74                 Extents padding = Padding;
75                 float totalHeight = padding.Top + padding.Bottom;
76                 float totalWidth = padding.Start + padding.End;
77
78                 MeasuredSize.StateType childWidthState = MeasuredSize.StateType.MeasuredSizeOK;
79                 MeasuredSize.StateType childHeightState = MeasuredSize.StateType.MeasuredSizeOK;
80
81                 Direction scrollingDirection = Direction.Vertical;
82                 ScrollableBase scrollableBase = this.Owner as ScrollableBase;
83                 if (scrollableBase)
84                 {
85                     scrollingDirection = scrollableBase.ScrollingDirection;
86                 }
87
88                 // measure child, should be a single scrolling child
89                 foreach (LayoutItem childLayout in LayoutChildren)
90                 {
91                     if (childLayout != null)
92                     {
93                         // Get size of child
94                         // Use an Unspecified MeasureSpecification mode so scrolling child is not restricted to it's parents size in Height (for vertical scrolling)
95                         // or Width for horizontal scrolling
96                         MeasureSpecification unrestrictedMeasureSpec = new MeasureSpecification(heightMeasureSpec.Size, MeasureSpecification.ModeType.Unspecified);
97
98                         if (scrollingDirection == Direction.Vertical)
99                         {
100                             MeasureChildWithMargins(childLayout, widthMeasureSpec, new LayoutLength(0), unrestrictedMeasureSpec, new LayoutLength(0));  // Height unrestricted by parent
101                         }
102                         else
103                         {
104                             MeasureChildWithMargins(childLayout, unrestrictedMeasureSpec, new LayoutLength(0), heightMeasureSpec, new LayoutLength(0));  // Width unrestricted by parent
105                         }
106
107                         float childWidth = childLayout.MeasuredWidth.Size.AsDecimal();
108                         float childHeight = childLayout.MeasuredHeight.Size.AsDecimal();
109
110                         // Determine the width and height needed by the children using their given position and size.
111                         // Children could overlap so find the left most and right most child.
112                         Position2D childPosition = childLayout.Owner.Position2D;
113                         float childLeft = childPosition.X;
114                         float childTop = childPosition.Y;
115
116                         // Store current width and height needed to contain all children.
117                         Extents childMargin = childLayout.Margin;
118                         totalWidth = childWidth + childMargin.Start + childMargin.End;
119                         totalHeight = childHeight + childMargin.Top + childMargin.Bottom;
120
121                         if (childLayout.MeasuredWidth.State == MeasuredSize.StateType.MeasuredSizeTooSmall)
122                         {
123                             childWidthState = MeasuredSize.StateType.MeasuredSizeTooSmall;
124                         }
125                         if (childLayout.MeasuredHeight.State == MeasuredSize.StateType.MeasuredSizeTooSmall)
126                         {
127                             childHeightState = MeasuredSize.StateType.MeasuredSizeTooSmall;
128                         }
129                     }
130                 }
131
132
133                 MeasuredSize widthSizeAndState = ResolveSizeAndState(new LayoutLength(totalWidth + Padding.Start + Padding.End), widthMeasureSpec, MeasuredSize.StateType.MeasuredSizeOK);
134                 MeasuredSize heightSizeAndState = ResolveSizeAndState(new LayoutLength(totalHeight + Padding.Top + Padding.Bottom), heightMeasureSpec, MeasuredSize.StateType.MeasuredSizeOK);
135                 totalWidth = widthSizeAndState.Size.AsDecimal();
136                 totalHeight = heightSizeAndState.Size.AsDecimal();
137
138                 // Ensure layout respects it's given minimum size
139                 totalWidth = Math.Max(totalWidth, SuggestedMinimumWidth.AsDecimal());
140                 totalHeight = Math.Max(totalHeight, SuggestedMinimumHeight.AsDecimal());
141
142                 widthSizeAndState.State = childWidthState;
143                 heightSizeAndState.State = childHeightState;
144
145                 SetMeasuredDimensions(ResolveSizeAndState(new LayoutLength(totalWidth + Padding.Start + Padding.End), widthMeasureSpec, childWidthState),
146                                        ResolveSizeAndState(new LayoutLength(totalHeight + Padding.Top + Padding.Bottom), heightMeasureSpec, childHeightState));
147
148                 // Size of ScrollableBase is changed. Change Page width too.
149                 scrollableBase.mPageWidth = (int)MeasuredWidth.Size.AsRoundedValue();
150             }
151
152             protected override void OnLayout(bool changed, LayoutLength left, LayoutLength top, LayoutLength right, LayoutLength bottom)
153             {
154                 foreach (LayoutItem childLayout in LayoutChildren)
155                 {
156                     if (childLayout != null)
157                     {
158                         LayoutLength childWidth = childLayout.MeasuredWidth.Size;
159                         LayoutLength childHeight = childLayout.MeasuredHeight.Size;
160
161                         Position2D childPosition = childLayout.Owner.Position2D;
162                         Extents padding = Padding;
163                         Extents childMargin = childLayout.Margin;
164
165                         LayoutLength childLeft = new LayoutLength(childPosition.X + childMargin.Start + padding.Start);
166                         LayoutLength childTop = new LayoutLength(childPosition.Y + childMargin.Top + padding.Top);
167
168                         childLayout.Layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
169                     }
170                 }
171             }
172         } //  ScrollableBaseCustomLayout
173
174         /// <summary>
175         /// The direction axis to scroll.
176         /// </summary>
177         /// <since_tizen> 8 </since_tizen>
178         public enum Direction
179         {
180             /// <summary>
181             /// Horizontal axis.
182             /// </summary>
183             /// <since_tizen> 8 </since_tizen>
184             Horizontal,
185
186             /// <summary>
187             /// Vertical axis.
188             /// </summary>
189             /// <since_tizen> 8 </since_tizen>
190             Vertical
191         }
192
193         /// <summary>
194         /// Scrolling direction mode.
195         /// Default is Vertical scrolling.
196         /// </summary>
197         /// <since_tizen> 8 </since_tizen>
198         public Direction ScrollingDirection
199         {
200             get
201             {
202                 return mScrollingDirection;
203             }
204             set
205             {
206                 if (value != mScrollingDirection)
207                 {
208                     mScrollingDirection = value;
209                     mPanGestureDetector.RemoveDirection(value == Direction.Horizontal ?
210                         PanGestureDetector.DirectionVertical : PanGestureDetector.DirectionHorizontal);
211                     mPanGestureDetector.AddDirection(value == Direction.Horizontal ?
212                         PanGestureDetector.DirectionHorizontal : PanGestureDetector.DirectionVertical);
213
214                     ContentContainer.WidthSpecification = mScrollingDirection == Direction.Vertical ?
215                         LayoutParamPolicies.MatchParent : LayoutParamPolicies.WrapContent;
216                     ContentContainer.HeightSpecification = mScrollingDirection == Direction.Vertical ?
217                         LayoutParamPolicies.WrapContent : LayoutParamPolicies.MatchParent;
218                 }
219             }
220         }
221
222         /// <summary>
223         /// Enable or disable scrolling.
224         /// </summary>
225         /// <since_tizen> 8 </since_tizen>
226         public bool ScrollEnabled
227         {
228             get
229             {
230                 return mScrollEnabled;
231             }
232             set
233             {
234                 if (value != mScrollEnabled)
235                 {
236                     mScrollEnabled = value;
237                     if (mScrollEnabled)
238                     {
239                         mPanGestureDetector.Detected += OnPanGestureDetected;
240                     }
241                     else
242                     {
243                         mPanGestureDetector.Detected -= OnPanGestureDetected;
244                     }
245                 }
246             }
247         }
248
249         /// <summary>
250         /// Pages mode, enables moving to the next or return to current page depending on pan displacement.
251         /// Default is false.
252         /// </summary>
253         /// <since_tizen> 8 </since_tizen>
254         public bool SnapToPage { set; get; } = false;
255
256         /// <summary>
257         /// Get current page.
258         /// Working property with SnapToPage property.
259         /// </summary>
260         /// <since_tizen> 8 </since_tizen>
261         public int CurrentPage { get; private set; } = 0;
262
263         /// <summary>
264         /// Duration of scroll animation.
265         /// Default value is 125ms.
266         /// </summary>
267         /// <since_tizen> 8 </since_tizen>
268         public int ScrollDuration
269         {
270             set
271             {
272                 mScrollDuration = value >= 0 ? value : mScrollDuration;
273             }
274             get
275             {
276                 return mScrollDuration;
277             }
278         }
279
280         /// <summary>
281         /// Scroll Available area.
282         /// </summary>
283         /// <since_tizen> 8 </since_tizen>
284         public Vector2 ScrollAvailableArea { set; get; }
285
286         /// <summary>
287         /// An event emitted when user starts dragging ScrollableBase, user can subscribe or unsubscribe to this event handler.<br />
288         /// </summary>
289         /// <since_tizen> 8 </since_tizen>
290         public event EventHandler<ScrollEventArgs> ScrollDragStarted;
291
292         /// <summary>
293         /// An event emitted when user stops dragging ScrollableBase, user can subscribe or unsubscribe to this event handler.<br />
294         /// </summary>
295         /// <since_tizen> 8 </since_tizen>
296         public event EventHandler<ScrollEventArgs> ScrollDragEnded;
297
298
299         /// <summary>
300         /// An event emitted when the scrolling slide animation starts, user can subscribe or unsubscribe to this event handler.<br />
301         /// </summary>
302         /// <since_tizen> 8 </since_tizen>
303         public event EventHandler<ScrollEventArgs> ScrollAnimationStarted;
304
305         /// <summary>
306         /// An event emitted when the scrolling slide animation ends, user can subscribe or unsubscribe to this event handler.<br />
307         /// </summary>
308         /// <since_tizen> 8 </since_tizen>
309         public event EventHandler<ScrollEventArgs> ScrollAnimationEnded;
310
311
312         /// <summary>
313         /// An event emitted when scrolling, user can subscribe or unsubscribe to this event handler.<br />
314         /// </summary>
315         /// <since_tizen> 8 </since_tizen>
316         public event EventHandler<ScrollEventArgs> Scrolling;
317
318
319         /// <summary>
320         /// Scrollbar for ScrollableBase.
321         /// </summary>
322         /// <since_tizen> 8 </since_tizen>
323         public ScrollbarBase Scrollbar
324         {
325             get
326             {
327                 return scrollBar;
328             }
329             set
330             {
331                 if (scrollBar)
332                 {
333                     scrollBar.Unparent();
334                 }
335
336                 scrollBar = value;
337                 scrollBar.Name = "ScrollBar";
338                 base.Add(scrollBar);
339
340                 if (hideScrollbar)
341                 {
342                     scrollBar.Hide();
343                 }
344                 else
345                 {
346                     scrollBar.Show();
347                 }
348
349                 SetScrollbar();
350             }
351         }
352
353         /// <summary>
354         /// Always hide Scrollbar.
355         /// </summary>
356         /// <since_tizen> 8 </since_tizen>
357         public bool HideScrollbar
358         {
359             get
360             {
361                 return hideScrollbar;
362             }
363             set
364             {
365                 hideScrollbar = value;
366
367                 if (scrollBar)
368                 {
369                     if (value)
370                     {
371                         scrollBar.Hide();
372                     }
373                     else
374                     {
375                         scrollBar.Show();
376                     }
377                 }
378             }
379         }
380
381         /// <summary>
382         /// Container which has content of ScrollableBase.
383         /// </summary>
384         /// <since_tizen> 8 </since_tizen>
385         public View ContentContainer { get; private set; }
386
387         /// <summary>
388         /// Set the layout on this View. Replaces any existing Layout.
389         /// </summary>
390         /// <since_tizen> 8 </since_tizen>
391         public new LayoutItem Layout
392         {
393             get
394             {
395                 return ContentContainer.Layout;
396             }
397             set
398             {
399                 ContentContainer.Layout = value;
400                 if (ContentContainer.Layout != null)
401                 {
402                     ContentContainer.Layout.SetPositionByLayout = false;
403                 }
404             }
405         }
406
407         /// <summary>
408         /// List of children of Container.
409         /// </summary>
410         /// <since_tizen> 8 </since_tizen>
411         public new List<View> Children
412         {
413             get
414             {
415                 return ContentContainer.Children;
416             }
417         }
418
419         /// <summary>
420         /// Deceleration rate of scrolling by finger.
421         /// Rate should be bigger than 0 and smaller than 1.
422         /// Default value is 0.998f;
423         /// </summary>
424         /// <since_tizen> 8 </since_tizen>
425         public float DecelerationRate
426         {
427             get
428             {
429                 return decelerationRate;
430             }
431             set
432             {
433                 decelerationRate = (value < 1 && value > 0) ? value : decelerationRate;
434                 logValueOfDeceleration = (float)Math.Log(value);
435             }
436         }
437
438         /// <summary>
439         /// Threashold not to go infinit at the end of scrolling animation.
440         /// </summary>
441         [EditorBrowsable(EditorBrowsableState.Never)]
442         public float DecelerationThreshold { get; set; } = 0.1f;
443
444         /// <summary>
445         /// Page will be changed when velocity of panning is over threshold.
446         /// The unit of threshold is pixel per milisec.
447         /// </summary>
448         /// <since_tizen> 8 </since_tizen>
449         public float PageFlickThreshold
450         {
451             get
452             {
453                 return mPageFlickThreshold;
454             }
455             set
456             {
457                 mPageFlickThreshold = value >= 0f ? value : mPageFlickThreshold;
458             }
459         }
460
461         /// <summary>
462         /// Alphafunction for scroll animation.
463         /// </summary>
464         [EditorBrowsable(EditorBrowsableState.Never)]
465         public AlphaFunction ScrollAlphaFunction { get; set; } = new AlphaFunction(AlphaFunction.BuiltinFunctions.Linear);
466
467         private bool hideScrollbar = true;
468         private float maxScrollDistance;
469         private float childTargetPosition = 0.0f;
470         private PanGestureDetector mPanGestureDetector;
471         private View mInterruptTouchingChild;
472         private ScrollbarBase scrollBar;
473         private bool scrolling = false;
474         private float ratioOfScreenWidthToCompleteScroll = 0.5f;
475         private float totalDisplacementForPan = 0.0f;
476         private Size previousContainerSize = new Size();
477         private PropertyNotification propertyNotification;
478         private float noticeAnimationEndBeforePosition = 0.0f;
479         private bool readyToNotice = false;
480
481         /// <summary>
482         /// Notice before animation is finished.
483         /// </summary>
484         [EditorBrowsable(EditorBrowsableState.Never)]
485         // Let's consider more whether this needs to be set as protected.
486         public float NoticeAnimationEndBeforePosition { get => noticeAnimationEndBeforePosition; set => noticeAnimationEndBeforePosition = value; }
487
488         // Let's consider more whether this needs to be set as protected.
489         private float finalTargetPosition;
490
491         private Animation scrollAnimation;
492         // Declare user alpha function delegate
493         [UnmanagedFunctionPointer(CallingConvention.StdCall)]
494         private delegate float UserAlphaFunctionDelegate(float progress);
495         private UserAlphaFunctionDelegate customScrollAlphaFunction;
496         private float velocityOfLastPan = 0.0f;
497         private float panAnimationDuration = 0.0f;
498         private float panAnimationDelta = 0.0f;
499         private float logValueOfDeceleration = 0.0f;
500         private float decelerationRate = 0.0f;
501
502         /// <summary>
503         /// Default Constructor
504         /// </summary>
505         /// <since_tizen> 8 </since_tizen>
506         public ScrollableBase() : base()
507         {
508             DecelerationRate = 0.998f;
509
510             base.Layout = new ScrollableBaseCustomLayout();
511             mPanGestureDetector = new PanGestureDetector();
512             mPanGestureDetector.Attach(this);
513             mPanGestureDetector.AddDirection(PanGestureDetector.DirectionVertical);
514             mPanGestureDetector.Detected += OnPanGestureDetected;
515
516             ClippingMode = ClippingModeType.ClipChildren;
517
518             //Default Scrolling child
519             ContentContainer = new View()
520             {
521                 WidthSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.MatchParent : LayoutParamPolicies.WrapContent,
522                 HeightSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.WrapContent : LayoutParamPolicies.MatchParent,
523                 Layout = new AbsoluteLayout() { SetPositionByLayout = false },
524             };
525             ContentContainer.Relayout += OnScrollingChildRelayout;
526             propertyNotification = ContentContainer.AddPropertyNotification("position", PropertyCondition.Step(1.0f));
527             propertyNotification.Notified += OnPropertyChanged;
528             base.Add(ContentContainer);
529
530             //Interrupt touching when panning is started
531             mInterruptTouchingChild = new View()
532             {
533                 Size = new Size(Window.Instance.WindowSize),
534                 BackgroundColor = Color.Transparent,
535             };
536             mInterruptTouchingChild.TouchEvent += OnIterruptTouchingChildTouched;
537             Scrollbar = new Scrollbar();
538
539             AccessibilityManager.Instance.SetAccessibilityAttribute(this, AccessibilityManager.AccessibilityAttribute.Trait, "ScrollableBase");
540         }
541
542         private bool OnIterruptTouchingChildTouched(object source, View.TouchEventArgs args)
543         {
544             if (args.Touch.GetState(0) == PointStateType.Down)
545             {
546                 if (scrolling && !SnapToPage)
547                 {
548                     StopScroll();
549                 }
550             }
551             return true;
552         }
553
554         private void OnPropertyChanged(object source, PropertyNotification.NotifyEventArgs args)
555         {
556             OnScroll();
557         }
558
559         /// <summary>
560         /// Called after a child has been added to the owning view.
561         /// </summary>
562         /// <param name="view">The child which has been added.</param>
563         /// <since_tizen> 8 </since_tizen>
564         public override void Add(View view)
565         {
566             ContentContainer.Add(view);
567         }
568
569         /// <summary>
570         /// Called after a child has been removed from the owning view.
571         /// </summary>
572         /// <param name="view">The child which has been removed.</param>
573         /// <since_tizen> 8 </since_tizen>
574         public override void Remove(View view)
575         {
576             if (SnapToPage && CurrentPage == Children.IndexOf(view) && CurrentPage == Children.Count - 1)
577             {
578                 // Target View is current page and also last child.
579                 // CurrentPage should be changed to previous page.
580                 CurrentPage = Math.Max(0, CurrentPage - 1);
581                 ScrollToIndex(CurrentPage);
582             }
583
584             ContentContainer.Remove(view);
585         }
586
587         private void OnScrollingChildRelayout(object source, EventArgs args)
588         {
589             // Size is changed. Calculate maxScrollDistance.
590             bool isSizeChanged = previousContainerSize.Width != ContentContainer.Size.Width || previousContainerSize.Height != ContentContainer.Size.Height;
591
592             if (isSizeChanged)
593             {
594                 maxScrollDistance = CalculateMaximumScrollDistance();
595                 SetScrollbar();
596             }
597
598             previousContainerSize = ContentContainer.Size;
599         }
600
601         /// <summary>
602         /// The composition of a Scrollbar can vary depending on how you use ScrollableBase.
603         /// Set the composition that will go into the ScrollableBase according to your ScrollableBase.
604         /// </summary>
605         /// <since_tizen> 8 </since_tizen>
606         [EditorBrowsable(EditorBrowsableState.Never)]
607         protected virtual void SetScrollbar()
608         {
609             if (Scrollbar)
610             {
611                 bool isHorizontal = ScrollingDirection == Direction.Horizontal;
612                 float contentLength = isHorizontal ? ContentContainer.Size.Width : ContentContainer.Size.Height;
613                 float viewportLength = isHorizontal ? Size.Width : Size.Height;
614                 float currentPosition = isHorizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y;
615                 Scrollbar.Initialize(contentLength, viewportLength, currentPosition, isHorizontal);
616             }
617         }
618
619         /// <summary>
620         /// Scrolls to the item at the specified index.
621         /// </summary>
622         /// <param name="index">Index of item.</param>
623         /// <since_tizen> 8 </since_tizen>
624         public void ScrollToIndex(int index)
625         {
626             if (ContentContainer.ChildCount - 1 < index || index < 0)
627             {
628                 return;
629             }
630
631             if (SnapToPage)
632             {
633                 CurrentPage = index;
634             }
635
636             float targetPosition = Math.Min(ScrollingDirection == Direction.Vertical ? Children[index].Position.Y : Children[index].Position.X, maxScrollDistance);
637             AnimateChildTo(ScrollDuration, -targetPosition);
638         }
639
640         private void OnScrollDragStarted()
641         {
642             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
643             ScrollDragStarted?.Invoke(this, eventArgs);
644         }
645
646         private void OnScrollDragEnded()
647         {
648             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
649             ScrollDragEnded?.Invoke(this, eventArgs);
650         }
651
652         private void OnScrollAnimationStarted()
653         {
654             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
655             ScrollAnimationStarted?.Invoke(this, eventArgs);
656         }
657
658         private void OnScrollAnimationEnded()
659         {
660             scrolling = false;
661             base.Remove(mInterruptTouchingChild);
662
663             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
664             ScrollAnimationEnded?.Invoke(this, eventArgs);
665         }
666
667         private void OnScroll()
668         {
669             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
670             Scrolling?.Invoke(this, eventArgs);
671
672             bool isHorizontal = ScrollingDirection == Direction.Horizontal;
673             float contentLength = isHorizontal ? ContentContainer.Size.Width : ContentContainer.Size.Height;
674             float currentPosition = isHorizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y;
675
676             scrollBar.Update(contentLength, Math.Abs(currentPosition));
677             CheckPreReachedTargetPosition();
678         }
679
680         private void CheckPreReachedTargetPosition()
681         {
682             // Check whether we reached pre-reached target position
683             if (readyToNotice &&
684                 ContentContainer.CurrentPosition.Y <= finalTargetPosition + NoticeAnimationEndBeforePosition &&
685                 ContentContainer.CurrentPosition.Y >= finalTargetPosition - NoticeAnimationEndBeforePosition)
686             {
687                 //Notice first
688                 readyToNotice = false;
689                 OnPreReachedTargetPosition(finalTargetPosition);
690             }
691         }
692
693         /// <summary>
694         /// This helps developer who wants to know before scroll is reaching target position.
695         /// </summary>
696         /// <param name="targetPosition">Index of item.</param>
697         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
698         [EditorBrowsable(EditorBrowsableState.Never)]
699         protected virtual void OnPreReachedTargetPosition(float targetPosition)
700         {
701
702         }
703
704         private void StopScroll()
705         {
706             if (scrollAnimation != null)
707             {
708                 if (scrollAnimation.State == Animation.States.Playing)
709                 {
710                     Debug.WriteLineIf(LayoutDebugScrollableBase, "StopScroll Animation Playing");
711                     scrollAnimation.Stop(Animation.EndActions.Cancel);
712                     OnScrollAnimationEnded();
713                 }
714                 scrollAnimation.Clear();
715             }
716         }
717
718         private void AnimateChildTo(int duration, float axisPosition)
719         {
720             Debug.WriteLineIf(LayoutDebugScrollableBase, "AnimationTo Animation Duration:" + duration + " Destination:" + axisPosition);
721             finalTargetPosition = axisPosition;
722
723             StopScroll(); // Will replace previous animation so will stop existing one.
724
725             if (scrollAnimation == null)
726             {
727                 scrollAnimation = new Animation();
728                 scrollAnimation.Finished += ScrollAnimationFinished;
729             }
730
731             scrollAnimation.Duration = duration;
732             scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.EaseOutSquare);
733             scrollAnimation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", axisPosition, ScrollAlphaFunction);
734             scrolling = true;
735             OnScrollAnimationStarted();
736             scrollAnimation.Play();
737         }
738
739         /// <summary>
740         /// Scroll to specific position with or without animation.
741         /// </summary>
742         /// <param name="position">Destination.</param>
743         /// <param name="animate">Scroll with or without animation</param>
744         /// <since_tizen> 8 </since_tizen>
745         public void ScrollTo(float position, bool animate)
746         {
747             float currentPositionX = ContentContainer.CurrentPosition.X != 0 ? ContentContainer.CurrentPosition.X : ContentContainer.Position.X;
748             float currentPositionY = ContentContainer.CurrentPosition.Y != 0 ? ContentContainer.CurrentPosition.Y : ContentContainer.Position.Y;
749             float delta = ScrollingDirection == Direction.Horizontal ? currentPositionX : currentPositionY;
750             // The argument position is the new pan position. So the new position of ScrollableBase becomes (-position).
751             // To move ScrollableBase's position to (-position), it moves by (-position - currentPosition).
752             delta = -position - delta;
753
754             ScrollBy(delta, animate);
755         }
756
757         private float BoundScrollPosition(float targetPosition)
758         {
759             if (ScrollAvailableArea != null)
760             {
761                 float minScrollPosition = ScrollAvailableArea.X;
762                 float maxScrollPosition = ScrollAvailableArea.Y;
763
764                 targetPosition = Math.Min(-minScrollPosition, targetPosition);
765                 targetPosition = Math.Max(-maxScrollPosition, targetPosition);
766             }
767             else
768             {
769                 targetPosition = Math.Min(0, targetPosition);
770                 targetPosition = Math.Max(-maxScrollDistance, targetPosition);
771             }
772
773             return targetPosition;
774         }
775
776         private void ScrollBy(float displacement, bool animate)
777         {
778             if (GetChildCount() == 0 || maxScrollDistance < 0)
779             {
780                 return;
781             }
782
783             float childCurrentPosition = (ScrollingDirection == Direction.Horizontal) ? ContentContainer.PositionX : ContentContainer.PositionY;
784
785             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy childCurrentPosition:" + childCurrentPosition +
786                                                    " displacement:" + displacement,
787                                                    " maxScrollDistance:" + maxScrollDistance);
788
789             childTargetPosition = childCurrentPosition + displacement; // child current position + gesture displacement
790
791
792             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy currentAxisPosition:" + childCurrentPosition + "childTargetPosition:" + childTargetPosition);
793
794             if (animate)
795             {
796                 // Calculate scroll animaton duration
797                 float scrollDistance = Math.Abs(displacement);
798                 readyToNotice = true;
799
800                 AnimateChildTo(ScrollDuration, BoundScrollPosition(AdjustTargetPositionOfScrollAnimation(BoundScrollPosition(childTargetPosition))));
801             }
802             else
803             {
804                 finalTargetPosition = BoundScrollPosition(childTargetPosition);
805
806                 // Set position of scrolling child without an animation
807                 if (ScrollingDirection == Direction.Horizontal)
808                 {
809                     ContentContainer.PositionX = finalTargetPosition;
810                 }
811                 else
812                 {
813                     ContentContainer.PositionY = finalTargetPosition;
814                 }
815
816             }
817         }
818
819         /// <summary>
820         /// you can override it to clean-up your own resources.
821         /// </summary>
822         /// <param name="type">DisposeTypes</param>
823         /// This will be public opened in tizen_5.5 after ACR done. Before ACR, need to be hidden as inhouse API.
824         [EditorBrowsable(EditorBrowsableState.Never)]
825         protected override void Dispose(DisposeTypes type)
826         {
827             if (disposed)
828             {
829                 return;
830             }
831
832             if (type == DisposeTypes.Explicit)
833             {
834                 StopScroll();
835
836                 if (mPanGestureDetector != null)
837                 {
838                     mPanGestureDetector.Detected -= OnPanGestureDetected;
839                     mPanGestureDetector.Dispose();
840                     mPanGestureDetector = null;
841                 }
842             }
843             base.Dispose(type);
844         }
845
846         private float CalculateMaximumScrollDistance()
847         {
848             float scrollingChildLength = 0;
849             float scrollerLength = 0;
850             if (ScrollingDirection == Direction.Horizontal)
851             {
852                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Horizontal");
853
854                 scrollingChildLength = ContentContainer.Size.Width;
855                 scrollerLength = Size.Width;
856             }
857             else
858             {
859                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Vertical");
860                 scrollingChildLength = ContentContainer.Size.Height;
861                 scrollerLength = Size.Height;
862             }
863
864             Debug.WriteLineIf(LayoutDebugScrollableBase, "ScrollBy maxScrollDistance:" + (scrollingChildLength - scrollerLength) +
865                                                    " parent length:" + scrollerLength +
866                                                    " scrolling child length:" + scrollingChildLength);
867
868             return Math.Max(scrollingChildLength - scrollerLength, 0);
869         }
870
871         private void PageSnap(float velocity)
872         {
873             Debug.WriteLineIf(LayoutDebugScrollableBase, "PageSnap with pan candidate totalDisplacement:" + totalDisplacementForPan +
874                                                                 " currentPage[" + CurrentPage + "]");
875
876             //Increment current page if total displacement enough to warrant a page change.
877             if (Math.Abs(totalDisplacementForPan) > (mPageWidth * ratioOfScreenWidthToCompleteScroll))
878             {
879                 if (totalDisplacementForPan < 0)
880                 {
881                     CurrentPage = Math.Min(Math.Max(Children.Count - 1, 0), ++CurrentPage);
882                 }
883                 else
884                 {
885                     CurrentPage = Math.Max(0, --CurrentPage);
886                 }
887             }
888             else if (Math.Abs(velocity) > PageFlickThreshold)
889             {
890                 if (velocity < 0)
891                 {
892                     CurrentPage = Math.Min(Math.Max(Children.Count - 1, 0), ++CurrentPage);
893                 }
894                 else
895                 {
896                     CurrentPage = Math.Max(0, --CurrentPage);
897                 }
898             }
899
900             // Animate to new page or reposition to current page
901             float destinationX = -(Children[CurrentPage].Position.X + Children[CurrentPage].CurrentSize.Width / 2 - CurrentSize.Width / 2); // set to middle of current page
902             Debug.WriteLineIf(LayoutDebugScrollableBase, "Snapping to page[" + CurrentPage + "] to:" + destinationX + " from:" + ContentContainer.PositionX);
903             AnimateChildTo(ScrollDuration, destinationX);
904         }
905
906         private void OnPanGestureDetected(object source, PanGestureDetector.DetectedEventArgs e)
907         {
908             OnPanGesture(e.PanGesture);
909         }
910
911         private void OnPanGesture(PanGesture panGesture)
912         {
913             if (SnapToPage && scrollAnimation != null && scrollAnimation.State == Animation.States.Playing)
914             {
915                 return;
916             }
917
918             if (panGesture.State == Gesture.StateType.Started)
919             {
920                 readyToNotice = false;
921                 base.Add(mInterruptTouchingChild);
922                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Gesture Start");
923                 if (scrolling && !SnapToPage)
924                 {
925                     StopScroll();
926                 }
927                 totalDisplacementForPan = 0.0f;
928                 OnScrollDragStarted();
929             }
930             else if (panGesture.State == Gesture.StateType.Continuing)
931             {
932                 if (ScrollingDirection == Direction.Horizontal)
933                 {
934                     ScrollBy(panGesture.Displacement.X, false);
935                     totalDisplacementForPan += panGesture.Displacement.X;
936                 }
937                 else
938                 {
939                     ScrollBy(panGesture.Displacement.Y, false);
940                     totalDisplacementForPan += panGesture.Displacement.Y;
941                 }
942                 Debug.WriteLineIf(LayoutDebugScrollableBase, "OnPanGestureDetected Continue totalDisplacementForPan:" + totalDisplacementForPan);
943             }
944             else if (panGesture.State == Gesture.StateType.Finished)
945             {
946                 OnScrollDragEnded();
947                 StopScroll(); // Will replace previous animation so will stop existing one.
948
949                 if (scrollAnimation == null)
950                 {
951                     scrollAnimation = new Animation();
952                     scrollAnimation.Finished += ScrollAnimationFinished;
953                 }
954
955                 float panVelocity = (ScrollingDirection == Direction.Horizontal) ? panGesture.Velocity.X : panGesture.Velocity.Y;
956
957                 if (SnapToPage)
958                 {
959                     PageSnap(panVelocity);
960                 }
961                 else
962                 {
963                     if (panVelocity == 0)
964                     {
965                         float currentScrollPosition = (ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
966                         scrollAnimation.DefaultAlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.Linear);
967                         scrollAnimation.Duration = 0;
968                         scrollAnimation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", currentScrollPosition);
969                         scrollAnimation.Play();
970                     }
971                     else
972                     {
973                         Decelerating(panVelocity, scrollAnimation);
974                     }
975                 }
976
977                 totalDisplacementForPan = 0;
978                 scrolling = true;
979                 readyToNotice = true;
980                 OnScrollAnimationStarted();
981             }
982         }
983
984         internal override bool OnAccessibilityPan(PanGesture gestures)
985         {
986             if (SnapToPage && scrollAnimation != null && scrollAnimation.State == Animation.States.Playing)
987             {
988                 return false;
989             }
990
991             OnPanGesture(gestures);
992             return true;
993         }
994
995         private float CustomScrollAlphaFunction(float progress)
996         {
997             if (panAnimationDelta == 0)
998             {
999                 return 1.0f;
1000             }
1001             else
1002             {
1003                 // Parameter "progress" is normalized value. We need to multiply target duration to calculate distance.
1004                 // Can get real distance using equation of deceleration (check Decelerating function)
1005                 // After get real distance, normalize it
1006                 float realDuration = progress * panAnimationDuration;
1007                 float realDistance = velocityOfLastPan * ((float)Math.Pow(decelerationRate, realDuration) - 1) / logValueOfDeceleration;
1008                 float result = Math.Min(realDistance / Math.Abs(panAnimationDelta), 1.0f);
1009                 return result;
1010             }
1011         }
1012
1013         /// <summary>
1014         /// you can override it to custom your decelerating
1015         /// </summary>
1016         /// <param name="velocity">Velocity of current pan.</param>
1017         /// <param name="animation">Scroll animation.</param>
1018         [EditorBrowsable(EditorBrowsableState.Never)]
1019         protected virtual void Decelerating(float velocity, Animation animation)
1020         {
1021             // Decelerating using deceleration equation ===========
1022             //
1023             // V   : velocity (pixel per milisecond)
1024             // V0  : initial velocity
1025             // d   : deceleration rate,
1026             // t   : time
1027             // X   : final position after decelerating
1028             // log : natural logarithm
1029             //
1030             // V(t) = V0 * d pow t;
1031             // X(t) = V0 * (d pow t - 1) / log d;  <-- Integrate the velocity function
1032             // X(∞) = V0 * d / (1 - d); <-- Result using inifit T can be final position because T is tending to infinity.
1033             //
1034             // Because of final T is tending to inifity, we should use threshold value to finish.
1035             // Final T = log(-threshold * log d / |V0| ) / log d;
1036
1037             velocityOfLastPan = Math.Abs(velocity);
1038
1039             float currentScrollPosition = -(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
1040             panAnimationDelta = (velocityOfLastPan * decelerationRate) / (1 - decelerationRate);
1041             panAnimationDelta = velocity > 0 ? -panAnimationDelta : panAnimationDelta;
1042
1043             float destination = -(panAnimationDelta + currentScrollPosition);
1044             float adjustDestination = AdjustTargetPositionOfScrollAnimation(destination);
1045             float maxPosition = ScrollAvailableArea != null ? ScrollAvailableArea.Y : maxScrollDistance;
1046             float minPosition = ScrollAvailableArea != null ? ScrollAvailableArea.X : 0;
1047
1048             if (destination < -maxPosition || destination > minPosition)
1049             {
1050                 panAnimationDelta = velocity > 0 ? (currentScrollPosition - minPosition) : (maxPosition - currentScrollPosition);
1051                 destination = velocity > 0 ? minPosition : -maxPosition;
1052
1053                 if (panAnimationDelta == 0)
1054                 {
1055                     panAnimationDuration = 0.0f;
1056                 }
1057                 else
1058                 {
1059                     panAnimationDuration = (float)Math.Log((panAnimationDelta * logValueOfDeceleration / velocityOfLastPan + 1), decelerationRate);
1060                 }
1061
1062                 Debug.WriteLineIf(LayoutDebugScrollableBase, "\n" +
1063                     "OverRange======================= \n" +
1064                     "[decelerationRate] " + decelerationRate + "\n" +
1065                     "[logValueOfDeceleration] " + logValueOfDeceleration + "\n" +
1066                     "[Velocity] " + velocityOfLastPan + "\n" +
1067                     "[CurrentPosition] " + currentScrollPosition + "\n" +
1068                     "[CandidateDelta] " + panAnimationDelta + "\n" +
1069                     "[Destination] " + destination + "\n" +
1070                     "[Duration] " + panAnimationDuration + "\n" +
1071                     "================================ \n"
1072                 );
1073             }
1074             else
1075             {
1076                 panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
1077
1078                 if (adjustDestination != destination)
1079                 {
1080                     destination = adjustDestination;
1081                     panAnimationDelta = destination + currentScrollPosition;
1082                     velocityOfLastPan = Math.Abs(panAnimationDelta * logValueOfDeceleration / ((float)Math.Pow(decelerationRate, panAnimationDuration) - 1));
1083                     panAnimationDuration = (float)Math.Log(-DecelerationThreshold * logValueOfDeceleration / velocityOfLastPan) / logValueOfDeceleration;
1084                 }
1085
1086                 Debug.WriteLineIf(LayoutDebugScrollableBase, "\n" +
1087                     "================================ \n" +
1088                     "[decelerationRate] " + decelerationRate + "\n" +
1089                     "[logValueOfDeceleration] " + logValueOfDeceleration + "\n" +
1090                     "[Velocity] " + velocityOfLastPan + "\n" +
1091                     "[CurrentPosition] " + currentScrollPosition + "\n" +
1092                     "[CandidateDelta] " + panAnimationDelta + "\n" +
1093                     "[Destination] " + destination + "\n" +
1094                     "[Duration] " + panAnimationDuration + "\n" +
1095                     "================================ \n"
1096                 );
1097             }
1098
1099             finalTargetPosition = destination;
1100
1101             customScrollAlphaFunction = new UserAlphaFunctionDelegate(CustomScrollAlphaFunction);
1102             animation.DefaultAlphaFunction = new AlphaFunction(customScrollAlphaFunction);
1103             GC.KeepAlive(customScrollAlphaFunction);
1104             animation.Duration = (int)panAnimationDuration;
1105             animation.AnimateTo(ContentContainer, (ScrollingDirection == Direction.Horizontal) ? "PositionX" : "PositionY", destination);
1106             animation.Play();
1107         }
1108
1109         private void ScrollAnimationFinished(object sender, EventArgs e)
1110         {
1111             OnScrollAnimationEnded();
1112         }
1113
1114         /// <summary>
1115         /// Adjust scrolling position by own scrolling rules.
1116         /// Override this function when developer wants to change destination of flicking.(e.g. always snap to center of item)
1117         /// </summary>
1118         /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API
1119         [EditorBrowsable(EditorBrowsableState.Never)]
1120         protected virtual float AdjustTargetPositionOfScrollAnimation(float position)
1121         {
1122             return position;
1123         }
1124
1125         /// <summary>
1126         /// Scroll position given to ScrollTo.
1127         /// This is the position in the opposite direction to the position of ContentContainer.
1128         /// </summary>
1129         /// <since_tizen> 8 </since_tizen>
1130         public Position ScrollPosition
1131         {
1132             get
1133             {
1134                 return new Position(-ContentContainer.Position);
1135             }
1136         }
1137
1138         /// <summary>
1139         /// Current scroll position in the middle of ScrollTo animation.
1140         /// This is the position in the opposite direction to the current position of ContentContainer.
1141         /// </summary>
1142         /// <since_tizen> 8 </since_tizen>
1143         public Position ScrollCurrentPosition
1144         {
1145             get
1146             {
1147                 return new Position(-ContentContainer.CurrentPosition);
1148             }
1149         }
1150     }
1151
1152 } // namespace