X-Git-Url: http://review.tizen.org/git/?a=blobdiff_plain;f=src%2FTizen.NUI.Components%2FControls%2FScrollableBase.cs;h=ae694727d8c8fc2922e426d80b9160b29a7377dd;hb=c0c350a13f569ef9a9a8dd48ae95eb7d63fea916;hp=f68bb49a951013dafdafbc12089e082189f2aeee;hpb=4f355cdf617de811d4437c3e73c044c4d0402f14;p=platform%2Fcore%2Fcsapi%2Ftizenfx.git diff --git a/src/Tizen.NUI.Components/Controls/ScrollableBase.cs b/src/Tizen.NUI.Components/Controls/ScrollableBase.cs index f68bb49..ae69472 100755 --- a/src/Tizen.NUI.Components/Controls/ScrollableBase.cs +++ b/src/Tizen.NUI.Components/Controls/ScrollableBase.cs @@ -1,4 +1,4 @@ -/* Copyright (c) 2020 Samsung Electronics Co., Ltd. +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,6 @@ * */ using System; -using Tizen.NUI; using Tizen.NUI.BaseComponents; using System.Collections.Generic; using System.ComponentModel; @@ -28,18 +27,23 @@ namespace Tizen.NUI.Components /// ScrollEventArgs is a class to record scroll event arguments which will sent to user. /// /// 8 + [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; /// /// Default constructor. /// - /// Current scroll position + /// Current container position /// 8 public ScrollEventArgs(Position position) { this.position = position; + this.scrollPosition = new Position(-position); } /// @@ -53,6 +57,18 @@ namespace Tizen.NUI.Components return position; } } + /// + /// Current scroll position of scrollableBase pan. + /// This is the position in the opposite direction to the current position of ContentContainer. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public Position ScrollPosition + { + get + { + return scrollPosition; + } + } } /// @@ -129,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. /// /// 8 - 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 { @@ -241,12 +259,27 @@ namespace Tizen.NUI.Components { get { + return (Direction)GetValue(ScrollingDirectionProperty); + } + set + { + SetValue(ScrollingDirectionProperty, value); + NotifyPropertyChanged(); + } + } + private Direction InternalScrollingDirection + { + get + { return mScrollingDirection; } set { if (value != mScrollingDirection) { + //Reset scroll position and stop scroll animation + ScrollTo(0, false); + mScrollingDirection = value; mPanGestureDetector.ClearAngles(); mPanGestureDetector.AddDirection(value == Direction.Horizontal ? @@ -254,6 +287,7 @@ namespace Tizen.NUI.Components ContentContainer.WidthSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.MatchParent : LayoutParamPolicies.WrapContent; ContentContainer.HeightSpecification = ScrollingDirection == Direction.Vertical ? LayoutParamPolicies.WrapContent : LayoutParamPolicies.MatchParent; + SetScrollbar(); } } } @@ -266,6 +300,18 @@ namespace Tizen.NUI.Components { get { + return (bool)GetValue(ScrollEnabledProperty); + } + set + { + SetValue(ScrollEnabledProperty, value); + NotifyPropertyChanged(); + } + } + private bool InternalScrollEnabled + { + get + { return mScrollEnabled; } set @@ -286,11 +332,32 @@ namespace Tizen.NUI.Components } /// + /// Gets scrollable status. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override bool AccessibilityIsScrollable() + { + return true; + } + + /// /// Pages mode, enables moving to the next or return to current page depending on pan displacement. /// Default is false. /// /// 8 - 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; /// /// Get current page. @@ -306,6 +373,18 @@ namespace Tizen.NUI.Components /// 8 public int ScrollDuration { + get + { + return (int)GetValue(ScrollDurationProperty); + } + set + { + SetValue(ScrollDurationProperty, value); + NotifyPropertyChanged(); + } + } + private int InternalScrollDuration + { set { mScrollDuration = value >= 0 ? value : mScrollDuration; @@ -320,7 +399,19 @@ namespace Tizen.NUI.Components /// Scroll Available area. /// /// 8 - public Vector2 ScrollAvailableArea { set; get; } + public Vector2 ScrollAvailableArea + { + get + { + return GetValue(ScrollAvailableAreaProperty) as Vector2; + } + set + { + SetValue(ScrollAvailableAreaProperty, value); + NotifyPropertyChanged(); + } + } + private Vector2 InternalScrollAvailableArea { set; get; } /// /// An event emitted when user starts dragging ScrollableBase, user can subscribe or unsubscribe to this event handler.
@@ -366,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 @@ -403,6 +506,18 @@ namespace Tizen.NUI.Components { get { + return (bool)GetValue(HideScrollbarProperty); + } + set + { + SetValue(HideScrollbarProperty, value); + NotifyPropertyChanged(); + } + } + private bool InternalHideScrollbar + { + get + { return hideScrollbar; } set @@ -418,6 +533,49 @@ namespace Tizen.NUI.Components else { scrollBar.Show(); + if (fadeScrollbar) + { + scrollBar.Opacity = 1.0f; + scrollBar.FadeOut(); + } + } + } + } + } + + /// + /// 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. + /// + [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(); } } } @@ -437,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 @@ -467,6 +637,18 @@ namespace Tizen.NUI.Components { get { + return (float)GetValue(DecelerationRateProperty); + } + set + { + SetValue(DecelerationRateProperty, value); + NotifyPropertyChanged(); + } + } + private float InternalDecelerationRate + { + get + { return decelerationRate; } set @@ -477,10 +659,22 @@ namespace Tizen.NUI.Components } /// - /// Threashold not to go infinit at the end of scrolling animation. + /// Threshold not to go infinite at the end of scrolling animation. /// [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; /// /// Scrolling event will be thrown when this amount of scroll position is changed. @@ -493,6 +687,18 @@ namespace Tizen.NUI.Components { get { + return (float)GetValue(ScrollingEventThresholdProperty); + } + set + { + SetValue(ScrollingEventThresholdProperty, value); + NotifyPropertyChanged(); + } + } + private float InternalScrollingEventThreshold + { + get + { return mScrollingEventThreshold; } set @@ -509,13 +715,25 @@ namespace Tizen.NUI.Components /// /// Page will be changed when velocity of panning is over threshold. - /// The unit of threshold is pixel per milisec. + /// The unit of threshold is pixel per millisecond. /// /// 8 public float PageFlickThreshold { get { + return (float)GetValue(PageFlickThresholdProperty); + } + set + { + SetValue(PageFlickThresholdProperty, value); + NotifyPropertyChanged(); + } + } + private float InternalPageFlickThreshold + { + get + { return mPageFlickThreshold; } set @@ -532,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 @@ -544,13 +774,24 @@ namespace Tizen.NUI.Components /// Alphafunction for scroll animation. /// [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; private float childTargetPosition = 0.0f; private PanGestureDetector mPanGestureDetector; - private View mInterruptTouchingChild; private ScrollbarBase scrollBar; private bool scrolling = false; private float ratioOfScreenWidthToCompleteScroll = 0.5f; @@ -568,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; } + /// + /// 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. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public float StepScrollDistance + { + get + { + return (float)GetValue(StepScrollDistanceProperty); + } + set + { + SetValue(StepScrollDistanceProperty, value); + NotifyPropertyChanged(); + } + } + private float stepScrollDistance = 0f; + + /// + /// Wheel scroll move distance. + /// This value decide how long distance will it moves in wheel event. + /// + [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; @@ -596,11 +892,7 @@ namespace Tizen.NUI.Components private bool isOverShootingShadowShown = false; private float startShowShadowDisplacement; - /// - /// Default Constructor - /// - /// 8 - public ScrollableBase() : base() + private void Initialize() { DecelerationRate = 0.998f; @@ -608,6 +900,7 @@ namespace Tizen.NUI.Components mPanGestureDetector = new PanGestureDetector(); mPanGestureDetector.Attach(this); mPanGestureDetector.AddDirection(PanGestureDetector.DirectionVertical); + if (mPanGestureDetector.GetMaximumTouchesRequired() < 2) mPanGestureDetector.SetMaximumTouchesRequired(2); mPanGestureDetector.Detected += OnPanGestureDetected; ClippingMode = ClippingModeType.ClipToBoundingBox; @@ -619,18 +912,14 @@ 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; - //Interrupt touching when panning is started - mInterruptTouchingChild = new View() - { - Size = new Size(Window.Instance.WindowSize), - BackgroundColor = Color.Transparent, - }; - mInterruptTouchingChild.TouchEvent += OnIterruptTouchingChildTouched; Scrollbar = new Scrollbar(); //Show vertical shadow on the top (or bottom) of the scrollable when panning down (or up). @@ -672,10 +961,43 @@ namespace Tizen.NUI.Components PivotPoint = NUI.PivotPoint.CenterRight, }; + WheelEvent += OnWheelEvent; + AccessibilityManager.Instance.SetAccessibilityAttribute(this, AccessibilityManager.AccessibilityAttribute.Trait, "ScrollableBase"); + + SetKeyboardNavigationSupport(true); } - private bool OnIterruptTouchingChildTouched(object source, View.TouchEventArgs args) + /// + /// Default Constructor + /// + /// 8 + public ScrollableBase() : base() + { + Initialize(); + } + + /// + /// Creates a new instance of a ScrollableBase with style. + /// + /// Creates ScrollableBase by special style defined in UX. + [EditorBrowsable(EditorBrowsableState.Never)] + public ScrollableBase(string style) : base(style) + { + Initialize(); + } + + /// + /// Creates a new instance of a ScrollableBase with style. + /// + /// A style applied to the newly created ScrollableBase. + [EditorBrowsable(EditorBrowsableState.Never)] + public ScrollableBase(ControlStyle style) : base(style) + { + Initialize(); + } + + private bool OnInterruptTouchingChildTouched(object source, View.TouchEventArgs args) { if (args.Touch.GetState(0) == PointStateType.Down) { @@ -709,12 +1031,11 @@ namespace Tizen.NUI.Components /// 8 public override void Remove(View view) { - if (SnapToPage && CurrentPage == Children.IndexOf(view) && CurrentPage == Children.Count - 1) + if (SnapToPage && CurrentPage == Children.IndexOf(view) && CurrentPage == Children.Count - 1 && Children.Count > 1) { // Target View is current page and also last child. // CurrentPage should be changed to previous page. - CurrentPage = Math.Max(0, CurrentPage - 1); - ScrollToIndex(CurrentPage); + ScrollToIndex(CurrentPage - 1); } ContentContainer.Remove(view); @@ -729,11 +1050,31 @@ namespace Tizen.NUI.Components if (isSizeChanged) { maxScrollDistance = CalculateMaximumScrollDistance(); - SetScrollbar(); + if (!ReviseContainerPositionIfNeed()) + { + UpdateScrollbar(); + } + } + + previousContainerSize = new Size(ContentContainer.Size); + previousSize = new Size(Size); + } + + private bool ReviseContainerPositionIfNeed() + { + bool isHorizontal = ScrollingDirection == Direction.Horizontal; + float currentPosition = isHorizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y; + + if (Math.Abs(currentPosition) > maxScrollDistance) + { + StopScroll(); + var targetPosition = BoundScrollPosition(-maxScrollDistance); + if (isHorizontal) ContentContainer.PositionX = targetPosition; + else ContentContainer.PositionY = targetPosition; + return true; } - previousContainerSize = ContentContainer.Size; - previousSize = Size; + return false; } /// @@ -750,7 +1091,26 @@ namespace Tizen.NUI.Components float contentLength = isHorizontal ? ContentContainer.Size.Width : ContentContainer.Size.Height; float viewportLength = isHorizontal ? Size.Width : Size.Height; float currentPosition = isHorizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y; - Scrollbar.Initialize(contentLength, viewportLength, currentPosition, isHorizontal); + Scrollbar.Initialize(contentLength, viewportLength, -currentPosition, isHorizontal); + } + } + + /// Update scrollbar position and size. + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual void UpdateScrollbar() + { + if (Scrollbar) + { + bool isHorizontal = ScrollingDirection == Direction.Horizontal; + float contentLength = isHorizontal ? ContentContainer.Size.Width : ContentContainer.Size.Height; + 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(); + } } } @@ -775,31 +1135,84 @@ 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() { scrolling = false; - base.Remove(mInterruptTouchingChild); + this.InterceptTouchEvent -= OnInterruptTouchingChildTouched; ScrollEventArgs eventArgs = new ScrollEventArgs(ContentContainer.CurrentPosition); ScrollAnimationEnded?.Invoke(this, eventArgs); + EmitScrollFinishedEvent(); + + if (!hideScrollbar && fadeScrollbar) + { + scrollBar?.FadeOut(); + } } private void OnScroll() @@ -882,6 +1295,7 @@ namespace Tizen.NUI.Components /// 8 public void ScrollTo(float position, bool animate) { + StopScroll(); float currentPositionX = ContentContainer.CurrentPosition.X != 0 ? ContentContainer.CurrentPosition.X : ContentContainer.Position.X; float currentPositionY = ContentContainer.CurrentPosition.Y != 0 ? ContentContainer.CurrentPosition.Y : ContentContainer.Position.Y; float delta = ScrollingDirection == Direction.Horizontal ? currentPositionX : currentPositionY; @@ -930,7 +1344,7 @@ namespace Tizen.NUI.Components if (animate) { - // Calculate scroll animaton duration + // Calculate scroll animation duration float scrollDistance = Math.Abs(displacement); readyToNotice = true; @@ -938,6 +1352,7 @@ namespace Tizen.NUI.Components } else { + StopScroll(); finalTargetPosition = BoundScrollPosition(childTargetPosition); // Set position of scrolling child without an animation @@ -965,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); } @@ -1009,6 +1429,8 @@ namespace Tizen.NUI.Components private void PageSnap(float velocity) { + float destination; + Debug.WriteLineIf(LayoutDebugScrollableBase, "PageSnap with pan candidate totalDisplacement:" + totalDisplacementForPan + " currentPage[" + CurrentPage + "]"); @@ -1037,16 +1459,31 @@ namespace Tizen.NUI.Components } // Animate to new page or reposition to current page - float destinationX = -(Children[CurrentPage].Position.X + Children[CurrentPage].CurrentSize.Width / 2 - CurrentSize.Width / 2); // set to middle of current page - Debug.WriteLineIf(LayoutDebugScrollableBase, "Snapping to page[" + CurrentPage + "] to:" + destinationX + " from:" + ContentContainer.PositionX); - AnimateChildTo(ScrollDuration, destinationX); + if (ScrollingDirection == Direction.Horizontal) + destination = -(Children[CurrentPage].Position.X + Children[CurrentPage].CurrentSize.Width / 2 - CurrentSize.Width / 2); // set to middle of current page + else + destination = -(Children[CurrentPage].Position.Y + Children[CurrentPage].CurrentSize.Height / 2 - CurrentSize.Height / 2); + + AnimateChildTo(ScrollDuration, destination); } /// /// Enable/Disable overshooting effect. default is disabled. /// [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() { @@ -1265,20 +1702,19 @@ namespace Tizen.NUI.Components private void OnPanGestureDetected(object source, PanGestureDetector.DetectedEventArgs e) { - OnPanGesture(e.PanGesture); + 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; - base.Add(mInterruptTouchingChild); AttachOverShootingShadowView(); Debug.WriteLineIf(LayoutDebugScrollableBase, "Gesture Start"); if (scrolling && !SnapToPage) @@ -1286,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) @@ -1311,6 +1762,7 @@ namespace Tizen.NUI.Components DragOverShootingShadow(totalDisplacementForPan, panGesture.Displacement.Y); } Debug.WriteLineIf(LayoutDebugScrollableBase, "OnPanGestureDetected Continue totalDisplacementForPan:" + totalDisplacementForPan); + } else if (panGesture.State == Gesture.StateType.Finished || panGesture.State == Gesture.StateType.Cancelled) { @@ -1351,6 +1803,7 @@ namespace Tizen.NUI.Components readyToNotice = true; OnScrollAnimationStarted(); } + return handled; } internal void BaseRemove(View view) @@ -1383,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; } } @@ -1395,9 +1852,10 @@ 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 milisecond) + // V : velocity (pixel per millisecond) // V0 : initial velocity // d : deceleration rate, // t : time @@ -1406,9 +1864,9 @@ namespace Tizen.NUI.Components // // V(t) = V0 * d pow t; // X(t) = V0 * (d pow t - 1) / log d; <-- Integrate the velocity function - // X(∞) = V0 * d / (1 - d); <-- Result using inifit T can be final position because T is tending to infinity. + // X(∞) = V0 * d / (1 - d); <-- Result using infinite T can be final position because T is tending to infinity. // - // Because of final T is tending to inifity, we should use threshold value to finish. + // Because of final T is tending to infinity, we should use threshold value to finish. // Final T = log(-threshold * log d / |V0| ) / log d; velocityOfLastPan = Math.Abs(velocity); @@ -1558,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; + } + } + + /// + [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; + } + + /// + [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; + } + + /// + [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