namespace UIWidgets { using System; using System.Collections.Generic; using UIWidgets.Attributes; using UIWidgets.Styles; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; /// /// Base class for the ScrollBlock. /// [RequireComponent(typeof(EasyLayoutNS.EasyLayout))] public abstract class ScrollBlockBase : UIBehaviourConditional, IStylable, ILateUpdatable, IBeginDragHandler, IDragHandler, IEndDragHandler, IInitializePotentialDragHandler, IScrollHandler { /// /// Do nothing. /// public static void DoNothing() { } /// /// Default function to get value. /// /// Step. /// Value. public static string DefaultValue(int step) { return string.Format("Index: {0}", step.ToString()); } /// /// Default function to check is interactable. /// /// true. protected static bool DefaultInteractable() { return true; } /// /// Default function to check is action allowed. /// /// true. protected static bool DefaultAllow() { return true; } /// /// Action to increase the value. /// public Action Increase = DoNothing; /// /// Action to decrease the value. /// public Action Decrease = DoNothing; /// /// Function to check is increase allowed. /// public Func AllowIncrease = DefaultAllow; /// /// Function to check is decrease allowed. /// public Func AllowDecrease = DefaultAllow; /// /// Convert index to the displayed string. /// public Func Value = DefaultValue; /// /// Is interactable? /// public Func IsInteractable = DefaultInteractable; /// /// Size of the DefaultItem. /// public Vector2 DefaultItemSize { get; protected set; } /// /// Layout. /// [NonSerialized] protected EasyLayoutBridge Layout; /// /// Count of the visible items. /// public abstract int Count { get; } /// /// Is horizontal scroll? /// [SerializeField] protected bool IsHorizontal; /// /// Scroll sensitivity. /// [SerializeField] public float ScrollSensitivity = 15f; /// /// Layout internal padding. /// public float Padding { get { return Layout.GetFiller().x; } set { var padding = ClampPadding(value); Layout.SetFiller(padding, 0f); } } /// /// Animate inertia scroll with unscaled time. /// [SerializeField] public bool UnscaledTime = true; /// /// Auto-scroll to center after scroll/drag. /// [SerializeField] public bool AlwaysCenter = true; /// /// Time to stop. /// [SerializeField] [EditorConditionBool("AlwaysCenter")] public float TimeToStop = 0.5f; /// /// Velocity. /// [NonSerialized] protected float ScrollVelocity; /// /// Inertia velocity. /// [NonSerialized] protected float IntertiaVelocity; /// /// Current deceleration rate. /// [NonSerialized] protected float CurrentDecelerationRate; /// /// Inertia distance. /// [NonSerialized] protected float InertiaDistance; /// /// Is drag event occurring? /// [NonSerialized] protected bool IsDragging; /// /// Is scrolling occurring? /// [NonSerialized] protected bool IsScrolling; /// /// Previous scroll value. /// [NonSerialized] protected float PrevScrollValue; /// /// Current scroll value. /// [NonSerialized] protected float CurrentScrollValue; RectTransform rectTransform; /// /// Current RectTransformn. /// protected RectTransform RectTransform { get { if (rectTransform == null) { rectTransform = transform as RectTransform; } return rectTransform; } } /// /// Median index of the components. /// protected abstract int ComponentsMedian { get; } /// /// Distance to center. /// public float DistanceToCenter { get { return GetCenter() - Padding; } } /// /// Is inited? /// protected bool IsInited { get; private set; } /// /// Start this instance. /// protected override void Start() { Init(); } /// /// Init this instance. /// public void Init() { if (IsInited) { return; } IsInited = true; UpdateLayout(); var resizer = Utilities.GetOrAddComponent(this); resizer.OnResizeNextFrame.AddListener(Resize); Resize(); AlignComponents(); } /// /// Clamp padding value. /// /// Padding. /// Clamped value. protected float ClampPadding(float padding) { var current = Layout.GetFiller().x; var delta = current - padding; if ((delta < 0) && (!AllowDecrease())) { return current; } if ((delta > 0) && (!AllowIncrease())) { return current; } var size = ItemFullSize(); var center = GetCenter(); if (Mathf.Round(padding) >= (-size / 2f)) { var d = Mathf.Round(padding) - center; var n = Mathf.RoundToInt(d / size); if (n > 0) { for (int i = 0; i < n; i++) { if (!AllowDecrease()) { break; } padding -= size; Decrease(); } UpdateView(); } } else if (Mathf.Round(padding) < (-size / 2f)) { var d = center - Mathf.Round(padding); var n = Mathf.RoundToInt(d / size); if (n > 0) { for (int i = 0; i < n; i++) { if (!AllowIncrease()) { break; } padding += size; Increase(); } UpdateView(); } } return padding; } /// /// Update the layout. /// protected abstract void UpdateLayout(); /// /// Container size. /// /// Size. protected float ContainerSize() { return (IsHorizontal ? RectTransform.rect.width : RectTransform.rect.height) - Layout.GetFullMargin(); } /// /// Content size. /// /// Size. protected float ContentSize() { return (ItemFullSize() * Count) - Layout.GetSpacing(); } /// /// Item size. /// /// Size. protected float ItemSize() { return IsHorizontal ? DefaultItemSize.x : DefaultItemSize.y; } /// /// Item size with spacing. /// /// Size. protected float ItemFullSize() { return ItemSize() + Layout.GetSpacing(); } /// /// Calculate the maximum count of the visible components. /// /// Maximum count of the visible components. protected int CalculateMax() { var result = Mathf.CeilToInt((ContainerSize() + Layout.GetSpacing()) / ItemFullSize()) + 1; if (result < 0) { result = 0; } if ((result % 2) == 0) { result += 1; } return result; } /// /// Is component is null? /// /// Component. /// true if component is null; otherwise, false. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Reviewed.")] protected virtual bool IsNullComponent(ScrollBlockItem component) { return component == null; } /// /// Process RectTransform resize. /// protected abstract void Resize(); /// /// Update view. /// public abstract void UpdateView(); /// /// Returns true if the GameObject and the Component are active. /// /// true if the GameObject and the Component are active; otherwise false. public override bool IsActive() { return base.IsActive() && IsInited && IsInteractable(); } /// /// Process the begin drag event. /// /// Event data. public virtual void OnBeginDrag(PointerEventData eventData) { if (eventData.button != PointerEventData.InputButton.Left) { return; } if (!IsActive()) { return; } IsDragging = true; PrevScrollValue = Padding; CurrentScrollValue = Padding; StopInertia(); } /// /// Process the drag event. /// /// Event data. public virtual void OnDrag(PointerEventData eventData) { if (!IsDragging) { return; } if (eventData.button != PointerEventData.InputButton.Left) { return; } if (!IsActive()) { return; } StopInertia(); var scroll_delta = IsHorizontal ? eventData.delta.x : -eventData.delta.y; Scroll(scroll_delta); } /// /// Process scroll event. /// /// Event data. public void OnScroll(PointerEventData eventData) { if (!IsActive()) { return; } IsScrolling = true; var scroll_delta = IsHorizontal ? eventData.scrollDelta.x : -eventData.scrollDelta.y; Scroll(scroll_delta * ScrollSensitivity); } /// /// Scroll. /// /// Delta. protected virtual void Scroll(float delta) { Padding += delta; CurrentScrollValue += delta; var time_delta = UtilitiesTime.DefaultGetDeltaTime(UnscaledTime); var new_velocity = (PrevScrollValue - CurrentScrollValue) / time_delta; ScrollVelocity = Mathf.Lerp(ScrollVelocity, new_velocity, time_delta * 10); PrevScrollValue = CurrentScrollValue; } /// /// Process the end drag event. /// /// Event data. public virtual void OnEndDrag(PointerEventData eventData) { if (!IsDragging) { return; } if (eventData.button != PointerEventData.InputButton.Left) { return; } IsDragging = false; InitIntertia(); } /// /// Init inertia. /// protected virtual void InitIntertia() { IntertiaVelocity = -ScrollVelocity; CurrentDecelerationRate = -IntertiaVelocity / TimeToStop; var direction = Mathf.Sign(IntertiaVelocity); var time_to_stop_sq = Mathf.Pow(TimeToStop, 2f); var distance = ((-Mathf.Abs(CurrentDecelerationRate) * time_to_stop_sq) / 2f) + (Mathf.Abs(IntertiaVelocity) * TimeToStop); InertiaDistance = ClampDistance(distance, direction); IntertiaVelocity = (InertiaDistance - (-Mathf.Abs(CurrentDecelerationRate) * (TimeToStop * TimeToStop) / 2f)) / TimeToStop; IntertiaVelocity *= direction; } /// /// Process the enable event. /// protected override void OnEnable() { base.OnEnable(); Updater.AddLateUpdate(this); } /// /// Process the disable event. /// protected override void OnDisable() { base.OnDisable(); Updater.RemoveLateUpdate(this); } /// /// Late update. /// public virtual void RunLateUpdate() { if (IsScrolling) { IsScrolling = false; InitIntertia(); } else if (!IsDragging && (InertiaDistance > 0f) && AlwaysCenter) { var delta = UtilitiesTime.DefaultGetDeltaTime(UnscaledTime); var distance = IntertiaVelocity > 0f ? Mathf.Min(InertiaDistance, IntertiaVelocity * delta) : Mathf.Max(-InertiaDistance, IntertiaVelocity * delta); Padding += distance; InertiaDistance -= Mathf.Abs(distance); if (InertiaDistance > 0f) { IntertiaVelocity += CurrentDecelerationRate * delta; ScrollVelocity = -IntertiaVelocity; } else { StopInertia(); } } } /// /// Stop inertia. /// protected void StopInertia() { CurrentDecelerationRate = 0f; InertiaDistance = 0f; } /// /// Clamp distance to stop right at value. /// /// Distance. /// Scroll direction. /// Clamped distance. protected float ClampDistance(float distance, float direction) { var extra = (GetCenter() - Padding) * direction; var steps = Mathf.Round((Mathf.Abs(distance) - extra) / ItemFullSize()); var new_distance = (steps * ItemFullSize()) + extra; return new_distance; } /// /// Get center. /// /// Center. protected float GetCenter() { return -(ContentSize() - ContainerSize()) / 2f; } /// /// Align components. /// protected void AlignComponents() { Padding = GetCenter(); } /// /// Process the destroy event. /// protected override void OnDestroy() { var resizer = GetComponent(); if (resizer != null) { resizer.OnResizeNextFrame.RemoveListener(Resize); } } /// /// Called by a BaseInputModule when a drag has been found but before it is valid to begin the drag. /// /// Event data. public virtual void OnInitializePotentialDrag(PointerEventData eventData) { } #region IStylable implementation /// public abstract bool SetStyle(Style style); /// public abstract bool GetStyle(Style style); #endregion } }