using System.Collections; using UnityEngine; using UnityEngine.EventSystems; namespace AlicizaX.UI { public class Scroller : MonoBehaviour, IScroller, IBeginDragHandler, IEndDragHandler, IDragHandler, IScrollHandler { private static readonly WaitForEndOfFrame EndOfFrameYield = new(); private Coroutine movementCoroutine; protected float position; public float Position { get => position; set => position = value; } protected float velocity; public float Velocity => velocity; protected Direction direction; public Direction Direction { get => direction; set => direction = value; } protected Vector2 contentSize; public Vector2 ContentSize { get => contentSize; set => contentSize = value; } protected Vector2 viewSize; public Vector2 ViewSize { get => viewSize; set => viewSize = value; } protected float scrollSpeed = 1f; public float ScrollSpeed { get => scrollSpeed; set => scrollSpeed = value; } protected float wheelSpeed = 30f; public float WheelSpeed { get => wheelSpeed; set => wheelSpeed = value; } protected bool snap; public bool Snap { get => snap; set => snap = value; } protected ScrollerEvent scrollerEvent = new(); protected MoveStopEvent moveStopEvent = new(); protected DraggingEvent draggingEvent = new(); public float MaxPosition => direction == Direction.Vertical ? Mathf.Max(contentSize.y - viewSize.y, 0) : Mathf.Max(contentSize.x - viewSize.x, 0); public float ViewLength => direction == Direction.Vertical ? viewSize.y : viewSize.x; public ScrollerEvent OnValueChanged { get => scrollerEvent; set => scrollerEvent = value; } public MoveStopEvent OnMoveStoped { get => moveStopEvent; set => moveStopEvent = value; } public DraggingEvent OnDragging { get => draggingEvent; set => draggingEvent = value; } public float dragStopTime = 0f; public virtual void ScrollTo(float position, bool smooth = false) { if (Mathf.Approximately(position, this.position)) return; StopMovement(); if (!smooth) { this.position = position; OnValueChanged?.Invoke(this.position); } else { movementCoroutine = StartCoroutine(RunMotion(MoveTo(position))); } } public virtual void ScrollToDuration(float position, float duration) { if (Mathf.Approximately(position, this.position)) { return; } StopMovement(); if (duration <= 0f) { this.position = position; OnValueChanged?.Invoke(this.position); return; } movementCoroutine = StartCoroutine(RunMotion(ToPositionByDuration(position, duration))); } public virtual void ScrollToRatio(float ratio) { ScrollTo(MaxPosition * ratio, false); } public void OnBeginDrag(PointerEventData eventData) { OnDragging?.Invoke(true); StopMovement(); } public void OnEndDrag(PointerEventData eventData) { Inertia(); Elastic(); OnDragging?.Invoke(false); } public void OnDrag(PointerEventData eventData) { dragStopTime = Time.time; velocity = GetDelta(eventData); position += velocity; OnValueChanged?.Invoke(position); } public void OnScroll(PointerEventData eventData) { StopMovement(); float rate = GetScrollRate() * wheelSpeed; velocity = direction == Direction.Vertical ? -eventData.scrollDelta.y * rate : eventData.scrollDelta.x * rate; position += velocity; OnValueChanged?.Invoke(position); Inertia(); Elastic(); } internal virtual float GetDelta(PointerEventData eventData) { float rate = GetScrollRate(); return direction == Direction.Vertical ? eventData.delta.y * rate : -eventData.delta.x * rate; } private float GetScrollRate() { float rate = 1f; if (position < 0) { rate = Mathf.Max(0, 1 - (Mathf.Abs(position) / ViewLength)); } else if (position > MaxPosition) { rate = Mathf.Max(0, 1 - (Mathf.Abs(position - MaxPosition) / ViewLength)); } return rate; } protected virtual void Inertia() { if (Mathf.Abs(velocity) > 0.1f) { StopMovement(); movementCoroutine = StartCoroutine(RunMotion(InertiaTo())); } else { OnMoveStoped?.Invoke(); } } protected virtual void Elastic() { if (position < 0) { StopMovement(); movementCoroutine = StartCoroutine(RunMotion(ElasticTo(0))); } else if (position > MaxPosition) { StopMovement(); movementCoroutine = StartCoroutine(RunMotion(ElasticTo(MaxPosition))); } } IEnumerator InertiaTo() { float timer = 0f; float p = position; float v = velocity > 0 ? Mathf.Min(velocity, 100) : Mathf.Max(velocity, -100); float duration = snap ? 0.1f : 1f; while (timer < duration) { float y = (float)EaseUtil.EaseOutCirc(timer) * 40; timer += Time.deltaTime; position = p + y * v; Elastic(); OnValueChanged?.Invoke(position); yield return EndOfFrameYield; } OnMoveStoped?.Invoke(); } IEnumerator ElasticTo(float targetPos) { yield return ToPosition(targetPos, 7); } IEnumerator MoveTo(float targetPos) { yield return ToPosition(targetPos, scrollSpeed); } IEnumerator ToPositionByDuration(float targetPos, float duration) { duration = Mathf.Max(duration, 0.0001f); float startPos = position; float elapsed = 0f; while (elapsed < duration) { elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / duration); position = Mathf.Lerp(startPos, targetPos, t); OnValueChanged?.Invoke(position); yield return EndOfFrameYield; } position = targetPos; OnValueChanged?.Invoke(position); } IEnumerator ToPosition(float targetPos, float speed) { float startPos = position; float time = Time.deltaTime; while (Mathf.Abs(targetPos - position) > 0.1f) { position = Mathf.Lerp(startPos, targetPos, time * speed); OnValueChanged?.Invoke(position); time += Time.deltaTime; yield return EndOfFrameYield; } position = targetPos; OnValueChanged?.Invoke(position); } private IEnumerator RunMotion(IEnumerator motion) { yield return motion; movementCoroutine = null; } private void StopMovement() { if (movementCoroutine == null) { return; } StopCoroutine(movementCoroutine); movementCoroutine = null; } } }