[NUIComponents] Roll back error modification
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI.Components / Controls / ScrollableBase.cs
index 9405f47..ae69472 100755 (executable)
@@ -27,8 +27,11 @@ namespace Tizen.NUI.Components
     /// ScrollEventArgs is a class to record scroll event arguments which will sent to user.
     /// </summary>
     /// <since_tizen> 8 </since_tizen>
+    [global::System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1001: Types that own disposable fields should be disposable.", Justification = "Scroll event is temporarily used for notifying scroll position update, so position will not be disposed during the event processing.")]
     public class ScrollEventArgs : EventArgs
     {
+        // Position class is derived class of Disposable class and they will be implicitly disposed by DisposeQueue,
+        // so that there will be no memory leak.
         private Position position;
         private Position scrollPosition;
 
@@ -142,15 +145,17 @@ namespace Tizen.NUI.Components
     /// This class provides a View that can scroll a single View with a layout. This View can be a nest of Views.
     /// </summary>
     /// <since_tizen> 8 </since_tizen>
-    public class ScrollableBase : Control
+    public partial class ScrollableBase : Control
     {
         static bool LayoutDebugScrollableBase = false; // Debug flag
+        static bool focusDebugScrollableBase = false; // Debug flag
         private Direction mScrollingDirection = Direction.Vertical;
         private bool mScrollEnabled = true;
         private int mScrollDuration = 125;
         private int mPageWidth = 0;
         private float mPageFlickThreshold = 0.4f;
         private float mScrollingEventThreshold = 0.001f;
+        private bool fadeScrollbar = true;
 
         private class ScrollableBaseCustomLayout : AbsoluteLayout
         {
@@ -254,6 +259,18 @@ namespace Tizen.NUI.Components
         {
             get
             {
+                return (Direction)GetValue(ScrollingDirectionProperty);
+            }
+            set
+            {
+                SetValue(ScrollingDirectionProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private Direction InternalScrollingDirection
+        {
+            get
+            {
                 return mScrollingDirection;
             }
             set
@@ -283,6 +300,18 @@ namespace Tizen.NUI.Components
         {
             get
             {
+                return (bool)GetValue(ScrollEnabledProperty);
+            }
+            set
+            {
+                SetValue(ScrollEnabledProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private bool InternalScrollEnabled
+        {
+            get
+            {
                 return mScrollEnabled;
             }
             set
@@ -316,7 +345,19 @@ namespace Tizen.NUI.Components
         /// Default is false.
         /// </summary>
         /// <since_tizen> 8 </since_tizen>
-        public bool SnapToPage { set; get; } = false;
+        public bool SnapToPage
+        {
+            get
+            {
+                return (bool)GetValue(SnapToPageProperty);
+            }
+            set
+            {
+                SetValue(SnapToPageProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private bool InternalSnapToPage { set; get; } = false;
 
         /// <summary>
         /// Get current page.
@@ -332,6 +373,18 @@ namespace Tizen.NUI.Components
         /// <since_tizen> 8 </since_tizen>
         public int ScrollDuration
         {
+            get
+            {
+                return (int)GetValue(ScrollDurationProperty);
+            }
+            set
+            {
+                SetValue(ScrollDurationProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private int InternalScrollDuration
+        {
             set
             {
                 mScrollDuration = value >= 0 ? value : mScrollDuration;
@@ -346,7 +399,19 @@ namespace Tizen.NUI.Components
         /// Scroll Available area.
         /// </summary>
         /// <since_tizen> 8 </since_tizen>
-        public Vector2 ScrollAvailableArea { set; get; }
+        public Vector2 ScrollAvailableArea
+        {
+            get
+            {
+                return GetValue(ScrollAvailableAreaProperty) as Vector2;
+            }
+            set
+            {
+                SetValue(ScrollAvailableAreaProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private Vector2 InternalScrollAvailableArea { set; get; }
 
         /// <summary>
         /// An event emitted when user starts dragging ScrollableBase, user can subscribe or unsubscribe to this event handler.<br />
@@ -392,6 +457,18 @@ namespace Tizen.NUI.Components
         {
             get
             {
+                return GetValue(ScrollbarProperty) as ScrollbarBase;
+            }
+            set
+            {
+                SetValue(ScrollbarProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private ScrollbarBase InternalScrollbar
+        {
+            get
+            {
                 return scrollBar;
             }
             set
@@ -429,6 +506,18 @@ namespace Tizen.NUI.Components
         {
             get
             {
+                return (bool)GetValue(HideScrollbarProperty);
+            }
+            set
+            {
+                SetValue(HideScrollbarProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private bool InternalHideScrollbar
+        {
+            get
+            {
                 return hideScrollbar;
             }
             set
@@ -444,6 +533,49 @@ namespace Tizen.NUI.Components
                     else
                     {
                         scrollBar.Show();
+                        if (fadeScrollbar)
+                        {
+                            scrollBar.Opacity = 1.0f;
+                            scrollBar.FadeOut();
+                        }
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// The boolean flag for automatic fading Scrollbar.
+        /// Scrollbar will be faded out when scroll stay in certain position longer than the threshold.
+        /// Scrollbar will be faded in scroll position changes.
+        /// </summary>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public bool FadeScrollbar
+        {
+            get => (bool)GetValue(FadeScrollbarProperty);
+            set => SetValue(FadeScrollbarProperty, value);
+        }
+
+        private bool InternalFadeScrollbar
+        {
+            get
+            {
+                return fadeScrollbar;
+            }
+            set
+            {
+                fadeScrollbar = value;
+
+                if (scrollBar != null && !hideScrollbar)
+                {
+                    if (value)
+                    {
+                        scrollBar.FadeOut();
+                    }
+                    else
+                    {
+                        scrollBar.Opacity = 1.0f;
+                        // Removing fadeout timer and animation.
+                        scrollBar.FadeIn();
                     }
                 }
             }
@@ -463,6 +595,18 @@ namespace Tizen.NUI.Components
         {
             get
             {
+                return GetValue(LayoutProperty) as LayoutItem;
+            }
+            set
+            {
+                SetValue(LayoutProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private LayoutItem InternalLayout
+        {
+            get
+            {
                 return ContentContainer.Layout;
             }
             set
@@ -493,6 +637,18 @@ namespace Tizen.NUI.Components
         {
             get
             {
+                return (float)GetValue(DecelerationRateProperty);
+            }
+            set
+            {
+                SetValue(DecelerationRateProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private float InternalDecelerationRate
+        {
+            get
+            {
                 return decelerationRate;
             }
             set
@@ -506,7 +662,19 @@ namespace Tizen.NUI.Components
         /// Threshold not to go infinite at the end of scrolling animation.
         /// </summary>
         [EditorBrowsable(EditorBrowsableState.Never)]
-        public float DecelerationThreshold { get; set; } = 0.1f;
+        public float DecelerationThreshold
+        {
+            get
+            {
+                return (float)GetValue(DecelerationThresholdProperty);
+            }
+            set
+            {
+                SetValue(DecelerationThresholdProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private float InternalDecelerationThreshold { get; set; } = 0.1f;
 
         /// <summary>
         /// Scrolling event will be thrown when this amount of scroll position is changed.
@@ -519,6 +687,18 @@ namespace Tizen.NUI.Components
         {
             get
             {
+                return (float)GetValue(ScrollingEventThresholdProperty);
+            }
+            set
+            {
+                SetValue(ScrollingEventThresholdProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private float InternalScrollingEventThreshold
+        {
+            get
+            {
                 return mScrollingEventThreshold;
             }
             set
@@ -542,6 +722,18 @@ namespace Tizen.NUI.Components
         {
             get
             {
+                return (float)GetValue(PageFlickThresholdProperty);
+            }
+            set
+            {
+                SetValue(PageFlickThresholdProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private float InternalPageFlickThreshold
+        {
+            get
+            {
                 return mPageFlickThreshold;
             }
             set
@@ -558,6 +750,18 @@ namespace Tizen.NUI.Components
         {
             get
             {
+                return GetValue(PaddingProperty) as Extents;
+            }
+            set
+            {
+                SetValue(PaddingProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private Extents InternalPadding
+        {
+            get
+            {
                 return ContentContainer.Padding;
             }
             set
@@ -570,7 +774,19 @@ namespace Tizen.NUI.Components
         /// Alphafunction for scroll animation.
         /// </summary>
         [EditorBrowsable(EditorBrowsableState.Never)]
-        public AlphaFunction ScrollAlphaFunction { get; set; } = new AlphaFunction(AlphaFunction.BuiltinFunctions.Linear);
+        public AlphaFunction ScrollAlphaFunction
+        {
+            get
+            {
+                return GetValue(ScrollAlphaFunctionProperty) as AlphaFunction;
+            }
+            set
+            {
+                SetValue(ScrollAlphaFunctionProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private AlphaFunction InternalScrollAlphaFunction { get; set; } = new AlphaFunction(AlphaFunction.BuiltinFunctions.Linear);
 
         private bool hideScrollbar = true;
         private float maxScrollDistance;
@@ -593,10 +809,65 @@ namespace Tizen.NUI.Components
         // Let's consider more whether this needs to be set as protected.
         public float NoticeAnimationEndBeforePosition
         {
+            get
+            {
+                return (float)GetValue(NoticeAnimationEndBeforePositionProperty);
+            }
+            set
+            {
+                SetValue(NoticeAnimationEndBeforePositionProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private float InternalNoticeAnimationEndBeforePosition
+        {
             get => noticeAnimationEndBeforePosition;
             set => noticeAnimationEndBeforePosition = value;
         }
 
+        /// <summary>
+        /// Step scroll move distance.
+        /// Key focus originally moves focusable objects, but in ScrollableBase,
+        /// if focusable object is too far or un-exist and ScrollableBase is focusable,
+        /// it can scroll move itself by key input.
+        /// this value decide how long distance will it moves in one step.
+        /// if any value is not set, step will be moved quater size of ScrollableBase length.
+        /// </summary>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public float StepScrollDistance
+        {
+            get
+            {
+                return (float)GetValue(StepScrollDistanceProperty);
+            }
+            set
+            {
+                SetValue(StepScrollDistanceProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private float stepScrollDistance = 0f;
+
+        /// <summary>
+        /// Wheel scroll move distance.
+        /// This value decide how long distance will it moves in wheel event.
+        /// </summary>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public float WheelScrollDistance
+        {
+            get
+            {
+                return (float)GetValue(WheelScrollDistanceProperty);
+            }
+            set
+            {
+                SetValue(WheelScrollDistanceProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private float wheelScrollDistance = 50f;
+
+
         // Let's consider more whether this needs to be set as protected.
         private float finalTargetPosition;
 
@@ -621,11 +892,7 @@ namespace Tizen.NUI.Components
         private bool isOverShootingShadowShown = false;
         private float startShowShadowDisplacement;
 
-        /// <summary>
-        /// Default Constructor
-        /// </summary>
-        /// <since_tizen> 8 </since_tizen>
-        public ScrollableBase() : base()
+        private void Initialize()
         {
             DecelerationRate = 0.998f;
 
@@ -645,10 +912,13 @@ namespace Tizen.NUI.Components
                 WidthSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.MatchParent : LayoutParamPolicies.WrapContent,
                 HeightSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.WrapContent : LayoutParamPolicies.MatchParent,
             };
+            // Check if children's sizes change to update Scrollbar
             ContentContainer.Relayout += OnScrollingChildRelayout;
             propertyNotification = ContentContainer.AddPropertyNotification("position", PropertyCondition.Step(mScrollingEventThreshold));
             propertyNotification.Notified += OnPropertyChanged;
             base.Add(ContentContainer);
+            // Check if ScrollableBase's size changes to update Scrollbar
+            base.Relayout += OnScrollingChildRelayout;
 
             Scrollbar = new Scrollbar();
 
@@ -691,7 +961,40 @@ namespace Tizen.NUI.Components
                 PivotPoint = NUI.PivotPoint.CenterRight,
             };
 
+            WheelEvent += OnWheelEvent;
+
             AccessibilityManager.Instance.SetAccessibilityAttribute(this, AccessibilityManager.AccessibilityAttribute.Trait, "ScrollableBase");
+
+            SetKeyboardNavigationSupport(true);
+        }
+
+        /// <summary>
+        /// Default Constructor
+        /// </summary>
+        /// <since_tizen> 8 </since_tizen>
+        public ScrollableBase() : base()
+        {
+            Initialize();
+        }
+
+        /// <summary>
+        /// Creates a new instance of a ScrollableBase with style.
+        /// </summary>
+        /// <param name="style">Creates ScrollableBase by special style defined in UX.</param>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public ScrollableBase(string style) : base(style)
+        {
+            Initialize();
+        }
+
+        /// <summary>
+        /// Creates a new instance of a ScrollableBase with style.
+        /// </summary>
+        /// <param name="style">A style applied to the newly created ScrollableBase.</param>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public ScrollableBase(ControlStyle style) : base(style)
+        {
+            Initialize();
         }
 
         private bool OnInterruptTouchingChildTouched(object source, View.TouchEventArgs args)
@@ -753,8 +1056,8 @@ namespace Tizen.NUI.Components
                 }
             }
 
-            previousContainerSize = ContentContainer.Size;
-            previousSize = Size;
+            previousContainerSize = new Size(ContentContainer.Size);
+            previousSize = new Size(Size);
         }
 
         private bool ReviseContainerPositionIfNeed()
@@ -803,6 +1106,11 @@ namespace Tizen.NUI.Components
                 float viewportLength = isHorizontal ? Size.Width : Size.Height;
                 float currentPosition = isHorizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y;
                 Scrollbar.Update(contentLength, viewportLength, -currentPosition);
+
+                if (!hideScrollbar && fadeScrollbar)
+                {
+                    Scrollbar.FadeOut();
+                }
             }
         }
 
@@ -827,22 +1135,69 @@ namespace Tizen.NUI.Components
             AnimateChildTo(ScrollDuration, -targetPosition);
         }
 
+        internal void ScrollToChild(View child, bool anim = false)
+        {
+            if (null == FindDescendantByID(child.ID)) return;
+
+            bool isHorizontal = (ScrollingDirection == Direction.Horizontal);
+
+            float viewScreenPosition = (isHorizontal ? ScreenPosition.X : ScreenPosition.Y);
+            float childScreenPosition = (isHorizontal ? child.ScreenPosition.X : child.ScreenPosition.Y);
+            float scrollPosition = (isHorizontal ? ScrollPosition.X : ScrollPosition.Y);
+            float viewSize = (isHorizontal ? SizeWidth : SizeHeight);
+            float childSize = (isHorizontal ? child.SizeWidth : child.SizeHeight);
+
+            if (viewScreenPosition > childScreenPosition ||
+                viewScreenPosition + viewSize < childScreenPosition + childSize)
+            {// if object is outside
+                float targetPosition;
+                float dist = viewScreenPosition - childScreenPosition;
+                if (dist > 0)
+                {// if object is upper side
+                    targetPosition = scrollPosition - dist;
+                }
+                else
+                {// if object is down side
+                    targetPosition = scrollPosition - dist + childSize - viewSize;
+                }
+                ScrollTo(targetPosition, anim);
+            }
+        }
+
         private void OnScrollDragStarted()
         {
             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
             ScrollDragStarted?.Invoke(this, eventArgs);
+            EmitScrollStartedEvent();
+
+            if (!hideScrollbar && fadeScrollbar)
+            {
+                scrollBar?.FadeIn();
+            }
         }
 
         private void OnScrollDragEnded()
         {
             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
             ScrollDragEnded?.Invoke(this, eventArgs);
+            EmitScrollFinishedEvent();
+
+            if (!hideScrollbar && fadeScrollbar)
+            {
+                scrollBar?.FadeOut();
+            }
         }
 
         private void OnScrollAnimationStarted()
         {
             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
             ScrollAnimationStarted?.Invoke(this, eventArgs);
+            EmitScrollStartedEvent();
+
+            if (!hideScrollbar && fadeScrollbar)
+            {
+                scrollBar?.FadeIn();
+            }
         }
 
         private void OnScrollAnimationEnded()
@@ -852,6 +1207,12 @@ namespace Tizen.NUI.Components
 
             ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition);
             ScrollAnimationEnded?.Invoke(this, eventArgs);
+            EmitScrollFinishedEvent();
+
+            if (!hideScrollbar && fadeScrollbar)
+            {
+                scrollBar?.FadeOut();
+            }
         }
 
         private void OnScroll()
@@ -1019,19 +1380,24 @@ namespace Tizen.NUI.Components
                 return;
             }
 
+            StopOverShootingShadowAnimation();
+            StopScroll();
+
             if (type == DisposeTypes.Explicit)
             {
-                StopOverShootingShadowAnimation();
-                StopScroll();
+                mPanGestureDetector?.Dispose();
+                mPanGestureDetector = null;
 
-                if (mPanGestureDetector != null)
-                {
-                    mPanGestureDetector.Detected -= OnPanGestureDetected;
-                    mPanGestureDetector.Dispose();
-                    mPanGestureDetector = null;
-                }
+                ContentContainer?.RemovePropertyNotification(propertyNotification);
+                propertyNotification?.Dispose();
+                propertyNotification = null;
+            }
+
+            WheelEvent -= OnWheelEvent;
+
+            if (type == DisposeTypes.Explicit)
+            {
 
-                propertyNotification.Dispose();
             }
             base.Dispose(type);
         }
@@ -1105,7 +1471,19 @@ namespace Tizen.NUI.Components
         /// Enable/Disable overshooting effect. default is disabled.
         /// </summary>
         [EditorBrowsable(EditorBrowsableState.Never)]
-        public bool EnableOverShootingEffect { get; set; } = false;
+        public bool EnableOverShootingEffect
+        {
+            get
+            {
+                return (bool)GetValue(EnableOverShootingEffectProperty);
+            }
+            set
+            {
+                SetValue(EnableOverShootingEffectProperty, value);
+                NotifyPropertyChanged();
+            }
+        }
+        private bool InternalEnableOverShootingEffect { get; set; } = false;
 
         private void AttachOverShootingShadowView()
         {
@@ -1324,25 +1702,19 @@ namespace Tizen.NUI.Components
 
         private void OnPanGestureDetected(object source, PanGestureDetector.DetectedEventArgs e)
         {
-            OnPanGesture(e.PanGesture);
-            if(!((SnapToPage && scrollAnimation != null && scrollAnimation.State == Animation.States.Playing) || e.PanGesture.State == Gesture.StateType.Started))
-            {
-                e.Handled = !((int)finalTargetPosition == 0 || -(int)finalTargetPosition == (int)maxScrollDistance);
-            }
+            e.Handled = OnPanGesture(e.PanGesture);
         }
 
-        private void OnPanGesture(PanGesture panGesture)
+        private bool OnPanGesture(PanGesture panGesture)
         {
+            bool handled = true;
             if (SnapToPage && scrollAnimation != null && scrollAnimation.State == Animation.States.Playing)
             {
-                return;
+                return handled;
             }
-
             if (panGesture.State == Gesture.StateType.Started)
             {
                 readyToNotice = false;
-                //Interrupt touching when panning is started
-                this.InterceptTouchEvent += OnInterruptTouchingChildTouched;
                 AttachOverShootingShadowView();
                 Debug.WriteLineIf(LayoutDebugScrollableBase, "Gesture Start");
                 if (scrolling && !SnapToPage)
@@ -1350,6 +1722,21 @@ namespace Tizen.NUI.Components
                     StopScroll();
                 }
                 totalDisplacementForPan = 0.0f;
+
+                // check if gesture need to propagation
+                var checkDisplacement = (ScrollingDirection == Direction.Horizontal) ? panGesture.Displacement.X : panGesture.Displacement.Y;
+                var checkChildCurrentPosition = (ScrollingDirection == Direction.Horizontal) ? ContentContainer.PositionX : ContentContainer.PositionY;
+                var checkChildTargetPosition = checkChildCurrentPosition + checkDisplacement;
+                var checkFinalTargetPosition = BoundScrollPosition(checkChildTargetPosition);
+                handled = !((int)checkFinalTargetPosition == 0 || -(int)checkFinalTargetPosition == (int)maxScrollDistance);
+                // If you propagate a gesture event, return;
+                if (!handled)
+                {
+                    return handled;
+                }
+
+                //Interrupt touching when panning is started
+                this.InterceptTouchEvent += OnInterruptTouchingChildTouched;
                 OnScrollDragStarted();
             }
             else if (panGesture.State == Gesture.StateType.Continuing)
@@ -1416,6 +1803,7 @@ namespace Tizen.NUI.Components
                 readyToNotice = true;
                 OnScrollAnimationStarted();
             }
+            return handled;
         }
 
         internal void BaseRemove(View view)
@@ -1448,6 +1836,10 @@ namespace Tizen.NUI.Components
                 float realDuration = progress * panAnimationDuration;
                 float realDistance = velocityOfLastPan * ((float)Math.Pow(decelerationRate, realDuration) - 1) / logValueOfDeceleration;
                 float result = Math.Min(realDistance / Math.Abs(panAnimationDelta), 1.0f);
+
+                // This is hot-fix for if the velocity has very small value, result is not updated even progress done.
+                if (progress > 0.99) result = 1.0f;
+
                 return result;
             }
         }
@@ -1460,6 +1852,7 @@ namespace Tizen.NUI.Components
         [EditorBrowsable(EditorBrowsableState.Never)]
         protected virtual void Decelerating(float velocity, Animation animation)
         {
+            if (animation == null) throw new ArgumentNullException(nameof(animation));
             // Decelerating using deceleration equation ===========
             //
             // V   : velocity (pixel per millisecond)
@@ -1623,6 +2016,190 @@ namespace Tizen.NUI.Components
             }
         }
 
+        internal bool IsChildNearlyVisble(View child, float offset = 0)
+        {
+            if (ScreenPosition.X - offset < child.ScreenPosition.X + child.SizeWidth &&
+                ScreenPosition.X + SizeWidth + offset > child.ScreenPosition.X &&
+                ScreenPosition.Y - offset < child.ScreenPosition.Y + child.SizeHeight &&
+                ScreenPosition.Y + SizeHeight + offset > child.ScreenPosition.Y)
+            {
+                return true;
+            }
+            else
+            {
+                return false;
+            }
+        }
+
+        /// <inheritdoc/>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public override View GetNextFocusableView(View currentFocusedView, View.FocusDirection direction, bool loopEnabled)
+        {
+            bool isHorizontal = (ScrollingDirection == Direction.Horizontal);
+            float targetPosition = -(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
+            float stepDistance = (stepScrollDistance != 0 ? stepScrollDistance : (isHorizontal ? Size.Width * 0.25f : Size.Height * 0.25f));
+
+            bool forward = ((isHorizontal && direction == View.FocusDirection.Right) ||
+                            (!isHorizontal && direction == View.FocusDirection.Down) ||
+                            (direction == View.FocusDirection.Clockwise));
+            bool backward = ((isHorizontal && direction == View.FocusDirection.Left) ||
+                             (!isHorizontal && direction == View.FocusDirection.Up) ||
+                             (direction == View.FocusDirection.CounterClockwise));
+
+            View nextFocusedView = FocusManager.Instance.GetNearestFocusableActor(this, currentFocusedView, direction);
+
+            // Move out focus from ScrollableBase.
+            // FIXME: Forward, Backward is unimplemented other components.
+            if (direction == View.FocusDirection.Forward ||
+                direction == View.FocusDirection.Backward ||
+                (nextFocusedView == null &&
+                ((forward && maxScrollDistance - targetPosition < 0.1f) ||
+                 (backward && targetPosition < 0.1f))))
+            {
+                var next = FocusManager.Instance.GetNearestFocusableActor(this.Parent, this, direction);
+                Debug.WriteLineIf(focusDebugScrollableBase, $"Focus move [{direction}] out from ScrollableBase! Next focus target {next}:{next?.ID}");
+                return next;
+            }
+
+            if (focusDebugScrollableBase)
+            {
+                global::System.Text.StringBuilder debugMessage = new global::System.Text.StringBuilder("=========================================================\n");
+                debugMessage.Append($"GetNextFocusableView On: {this}:{this.ID}\n");
+                debugMessage.Append($"----------------Current: {currentFocusedView}:{currentFocusedView?.ID}\n");
+                debugMessage.Append($"-------------------Next: {nextFocusedView}:{nextFocusedView?.ID}\n");
+                debugMessage.Append($"--------------Direction: {direction}\n");
+                debugMessage.Append("=========================================================");
+                Debug.WriteLineIf(focusDebugScrollableBase, debugMessage);
+            }
+
+            if (nextFocusedView != null)
+            {
+                if (null != FindDescendantByID(nextFocusedView.ID))
+                {
+                    if (IsChildNearlyVisble(nextFocusedView, stepDistance) == true)
+                    {
+                        ScrollToChild(nextFocusedView, true);
+                        return nextFocusedView;
+                    }
+                }
+            }
+
+            if (forward || backward)
+            {
+                // Fallback to current focus or scrollableBase till next focus visible in scrollable.
+                if (null != currentFocusedView && null != FindDescendantByID(currentFocusedView.ID))
+                {
+                    nextFocusedView = currentFocusedView;
+                }
+                else
+                {
+                    Debug.WriteLineIf(focusDebugScrollableBase, "current focus view is not decendant. return ScrollableBase!");
+                    return this;
+                }
+
+                if (forward)
+                {
+                    targetPosition += stepDistance;
+                    targetPosition = targetPosition > maxScrollDistance ? maxScrollDistance : targetPosition;
+
+                }
+                else if (backward)
+                {
+                    targetPosition -= stepDistance;
+                    targetPosition = targetPosition < 0 ? 0 : targetPosition;
+                }
+
+                ScrollTo(targetPosition, true);
+
+                Debug.WriteLineIf(focusDebugScrollableBase, $"ScrollTo :({targetPosition})");
+            }
+
+            Debug.WriteLineIf(focusDebugScrollableBase, $"return end : {nextFocusedView}:{nextFocusedView?.ID}");
+            return nextFocusedView;
+        }
+
+        /// <inheritdoc/>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        protected override bool AccessibilityScrollToChild(View child)
+        {
+            if (child == null)
+            {
+                return false;
+            }
+
+            if (ScrollingDirection == Direction.Horizontal)
+            {
+                if (child.ScreenPosition.X + child.Size.Width <= this.ScreenPosition.X)
+                {
+                    if (SnapToPage)
+                    {
+                        PageSnap(PageFlickThreshold + 1);
+                    }
+                    else
+                    {
+                        ScrollTo((float)(child.ScreenPosition.X - ContentContainer.ScreenPosition.X), false);
+                    }
+                }
+                else if (child.ScreenPosition.X >= this.ScreenPosition.X + this.Size.Width)
+                {
+                    if (SnapToPage)
+                    {
+                        PageSnap(-(PageFlickThreshold + 1));
+                    }
+                    else
+                    {
+                        ScrollTo((float)(child.ScreenPosition.X + child.Size.Width - ContentContainer.ScreenPosition.X - this.Size.Width), false);
+                    }
+                }
+            }
+            else
+            {
+                if (child.ScreenPosition.Y + child.Size.Height <= this.ScreenPosition.Y)
+                {
+                    if (SnapToPage)
+                    {
+                        PageSnap(PageFlickThreshold + 1);
+                    }
+                    else
+                    {
+                        ScrollTo((float)(child.ScreenPosition.Y - ContentContainer.ScreenPosition.Y), false);
+                    }
+                }
+                else if (child.ScreenPosition.Y >= this.ScreenPosition.Y + this.Size.Height)
+                {
+                    if (SnapToPage)
+                    {
+                        PageSnap(-(PageFlickThreshold + 1));
+                    }
+                    else
+                    {
+                        ScrollTo((float)(child.ScreenPosition.Y + child.Size.Height - ContentContainer.ScreenPosition.Y - this.Size.Height), false);
+                    }
+                }
+            }
+
+            return true;
+        }
+
+        /// <inheritdoc/>
+        [EditorBrowsable(EditorBrowsableState.Never)]
+        public override bool OnWheel(Wheel wheel)
+        {
+            if (wheel == null)
+            {
+                return false;
+            }
+
+            float currentScrollPosition = -(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y);
+            ScrollTo(currentScrollPosition + (wheelScrollDistance * wheel.Z), false);
+
+            return true;
+        }
+
+        private bool OnWheelEvent(object o, WheelEventArgs e)
+        {
+            return OnWheel(e?.Wheel);
+        }
     }
 
 } // namespace