2025-03-12 20:59:12 +08:00
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine.EventSystems;
|
|
|
|
|
|
2025-11-20 15:40:38 +08:00
|
|
|
namespace AlicizaX.UI
|
2025-03-12 20:59:12 +08:00
|
|
|
{
|
|
|
|
|
public class Scroller : MonoBehaviour, IScroller, IBeginDragHandler, IEndDragHandler, IDragHandler, IScrollHandler
|
|
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
protected enum MotionState
|
|
|
|
|
{
|
|
|
|
|
Idle,
|
|
|
|
|
Smooth,
|
|
|
|
|
Duration,
|
|
|
|
|
Inertia
|
|
|
|
|
}
|
2026-03-31 15:18:50 +08:00
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
protected float position;
|
2026-04-29 14:44:30 +08:00
|
|
|
|
|
|
|
|
public float Position
|
|
|
|
|
{
|
|
|
|
|
get => position;
|
|
|
|
|
set => position = value;
|
|
|
|
|
}
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
|
|
|
protected float velocity;
|
|
|
|
|
public float Velocity => velocity;
|
|
|
|
|
|
|
|
|
|
protected Direction direction;
|
2026-04-29 14:44:30 +08:00
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
public Direction Direction
|
|
|
|
|
{
|
|
|
|
|
get => direction;
|
|
|
|
|
set => direction = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected Vector2 contentSize;
|
2026-04-29 14:44:30 +08:00
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
public Vector2 ContentSize
|
|
|
|
|
{
|
|
|
|
|
get => contentSize;
|
|
|
|
|
set => contentSize = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected Vector2 viewSize;
|
2026-04-29 14:44:30 +08:00
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
public Vector2 ViewSize
|
|
|
|
|
{
|
|
|
|
|
get => viewSize;
|
|
|
|
|
set => viewSize = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected float scrollSpeed = 1f;
|
2026-04-29 14:44:30 +08:00
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
public float ScrollSpeed
|
|
|
|
|
{
|
|
|
|
|
get => scrollSpeed;
|
|
|
|
|
set => scrollSpeed = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected float wheelSpeed = 30f;
|
2026-04-29 14:44:30 +08:00
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
public float WheelSpeed
|
|
|
|
|
{
|
|
|
|
|
get => wheelSpeed;
|
|
|
|
|
set => wheelSpeed = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected bool snap;
|
2026-04-29 14:44:30 +08:00
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
public bool Snap
|
|
|
|
|
{
|
|
|
|
|
get => snap;
|
|
|
|
|
set => snap = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected ScrollerEvent scrollerEvent = new();
|
|
|
|
|
protected MoveStopEvent moveStopEvent = new();
|
|
|
|
|
protected DraggingEvent draggingEvent = new();
|
|
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
private MotionState motionState;
|
|
|
|
|
private float motionStartPosition;
|
|
|
|
|
private float motionTargetPosition;
|
|
|
|
|
private float motionElapsed;
|
|
|
|
|
private float motionDuration;
|
|
|
|
|
private float motionSpeed;
|
|
|
|
|
private float inertiaVelocity;
|
|
|
|
|
|
|
|
|
|
public float MaxPosition => direction == Direction.Vertical ? Mathf.Max(contentSize.y - viewSize.y, 0) : Mathf.Max(contentSize.x - viewSize.x, 0);
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
|
|
|
public float ViewLength => direction == Direction.Vertical ? viewSize.y : viewSize.x;
|
|
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
public ScrollerEvent OnValueChanged
|
|
|
|
|
{
|
|
|
|
|
get => scrollerEvent;
|
|
|
|
|
set => scrollerEvent = value;
|
|
|
|
|
}
|
2025-03-12 20:59:12 +08:00
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
public MoveStopEvent OnMoveStoped
|
|
|
|
|
{
|
|
|
|
|
get => moveStopEvent;
|
|
|
|
|
set => moveStopEvent = value;
|
|
|
|
|
}
|
2025-03-12 20:59:12 +08:00
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
public DraggingEvent OnDragging
|
|
|
|
|
{
|
|
|
|
|
get => draggingEvent;
|
|
|
|
|
set => draggingEvent = value;
|
|
|
|
|
}
|
2025-03-12 20:59:12 +08:00
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
public float dragStopTime = 0f;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
public bool InputEnabled { get; set; } = true;
|
|
|
|
|
|
|
|
|
|
protected virtual void Awake()
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected virtual void Update()
|
|
|
|
|
{
|
|
|
|
|
if (motionState == MotionState.Idle)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TickMotion(Time.deltaTime);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void TickMotion(float deltaTime)
|
|
|
|
|
{
|
|
|
|
|
switch (motionState)
|
|
|
|
|
{
|
|
|
|
|
case MotionState.Smooth:
|
|
|
|
|
TickSmooth(deltaTime);
|
|
|
|
|
break;
|
|
|
|
|
case MotionState.Duration:
|
|
|
|
|
TickDuration(deltaTime);
|
|
|
|
|
break;
|
|
|
|
|
case MotionState.Inertia:
|
|
|
|
|
TickInertia(deltaTime);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
motionState = MotionState.Idle;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 15:18:50 +08:00
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
public virtual void ScrollTo(float position, bool smooth = false)
|
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
if (Mathf.Approximately(position, this.position)) return;
|
|
|
|
|
|
|
|
|
|
StopMovement();
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
|
|
|
if (!smooth)
|
|
|
|
|
{
|
|
|
|
|
this.position = position;
|
|
|
|
|
OnValueChanged?.Invoke(this.position);
|
2026-04-29 14:44:30 +08:00
|
|
|
return;
|
2025-03-12 20:59:12 +08:00
|
|
|
}
|
2026-04-29 14:44:30 +08:00
|
|
|
|
|
|
|
|
StartPositionMotion(position, scrollSpeed);
|
2025-03-12 20:59:12 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
motionState = MotionState.Duration;
|
|
|
|
|
motionStartPosition = this.position;
|
|
|
|
|
motionTargetPosition = position;
|
|
|
|
|
motionDuration = Mathf.Max(duration, 0.0001f);
|
|
|
|
|
motionElapsed = 0f;
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
|
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
public virtual void ScrollToRatio(float ratio)
|
|
|
|
|
{
|
|
|
|
|
ScrollTo(MaxPosition * ratio, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnBeginDrag(PointerEventData eventData)
|
|
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
if (!InputEnabled)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
OnDragging?.Invoke(true);
|
2026-03-31 15:18:50 +08:00
|
|
|
StopMovement();
|
2025-03-12 20:59:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnEndDrag(PointerEventData eventData)
|
|
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
if (!InputEnabled)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
Inertia();
|
|
|
|
|
Elastic();
|
|
|
|
|
OnDragging?.Invoke(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnDrag(PointerEventData eventData)
|
|
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
if (!InputEnabled)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
dragStopTime = Time.time;
|
|
|
|
|
|
|
|
|
|
velocity = GetDelta(eventData);
|
|
|
|
|
position += velocity;
|
|
|
|
|
|
|
|
|
|
OnValueChanged?.Invoke(position);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
public void OnScroll(PointerEventData eventData)
|
2025-03-12 20:59:12 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
if (!InputEnabled)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
StopMovement();
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
|
|
|
float rate = GetScrollRate() * wheelSpeed;
|
|
|
|
|
velocity = direction == Direction.Vertical ? -eventData.scrollDelta.y * rate : eventData.scrollDelta.x * rate;
|
|
|
|
|
position += velocity;
|
|
|
|
|
|
|
|
|
|
OnValueChanged?.Invoke(position);
|
2025-12-26 14:22:46 +08:00
|
|
|
Inertia();
|
2025-03-12 20:59:12 +08:00
|
|
|
Elastic();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal virtual float GetDelta(PointerEventData eventData)
|
|
|
|
|
{
|
|
|
|
|
float rate = GetScrollRate();
|
|
|
|
|
return direction == Direction.Vertical ? eventData.delta.y * rate : -eventData.delta.x * rate;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
protected float GetScrollRate()
|
2025-03-12 20:59:12 +08:00
|
|
|
{
|
|
|
|
|
float rate = 1f;
|
2026-04-29 14:44:30 +08:00
|
|
|
float viewLength = ViewLength;
|
|
|
|
|
if (viewLength <= 0f)
|
|
|
|
|
{
|
|
|
|
|
return rate;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
if (position < 0)
|
|
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
rate = Mathf.Max(0, 1 - (Mathf.Abs(position) / viewLength));
|
2025-03-12 20:59:12 +08:00
|
|
|
}
|
|
|
|
|
else if (position > MaxPosition)
|
|
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
rate = Mathf.Max(0, 1 - (Mathf.Abs(position - MaxPosition) / viewLength));
|
2025-03-12 20:59:12 +08:00
|
|
|
}
|
2026-04-29 14:44:30 +08:00
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
return rate;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected virtual void Inertia()
|
|
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
if (Mathf.Abs(velocity) <= 0.1f)
|
2025-03-12 20:59:12 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
CompleteMotion(true);
|
|
|
|
|
return;
|
2025-03-12 20:59:12 +08:00
|
|
|
}
|
2026-04-29 14:44:30 +08:00
|
|
|
|
|
|
|
|
StopMovement();
|
|
|
|
|
motionState = MotionState.Inertia;
|
|
|
|
|
motionStartPosition = position;
|
|
|
|
|
motionElapsed = 0f;
|
|
|
|
|
motionDuration = snap ? 0.1f : 1f;
|
|
|
|
|
inertiaVelocity = velocity > 0 ? Mathf.Min(velocity, 100) : Mathf.Max(velocity, -100);
|
2025-03-12 20:59:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected virtual void Elastic()
|
|
|
|
|
{
|
|
|
|
|
if (position < 0)
|
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
StopMovement();
|
2026-04-29 14:44:30 +08:00
|
|
|
StartPositionMotion(0, 7f);
|
2025-03-12 20:59:12 +08:00
|
|
|
}
|
|
|
|
|
else if (position > MaxPosition)
|
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
StopMovement();
|
2026-04-29 14:44:30 +08:00
|
|
|
StartPositionMotion(MaxPosition, 7f);
|
2025-03-12 20:59:12 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
protected void StopMovement()
|
2025-03-12 20:59:12 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
motionState = MotionState.Idle;
|
2025-03-12 20:59:12 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
private void StartPositionMotion(float targetPosition, float speed)
|
2025-03-12 20:59:12 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
motionState = MotionState.Smooth;
|
|
|
|
|
motionStartPosition = position;
|
|
|
|
|
motionTargetPosition = targetPosition;
|
|
|
|
|
motionElapsed = Time.deltaTime;
|
|
|
|
|
motionSpeed = speed;
|
2025-03-12 20:59:12 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
private void TickSmooth(float deltaTime)
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
if (Mathf.Abs(motionTargetPosition - position) <= 0.1f)
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
position = motionTargetPosition;
|
2026-03-31 15:18:50 +08:00
|
|
|
OnValueChanged?.Invoke(position);
|
2026-04-29 14:44:30 +08:00
|
|
|
CompleteMotion(false);
|
|
|
|
|
return;
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
position = Mathf.Lerp(motionStartPosition, motionTargetPosition, motionElapsed * motionSpeed);
|
|
|
|
|
motionElapsed += deltaTime;
|
2026-03-31 15:18:50 +08:00
|
|
|
OnValueChanged?.Invoke(position);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
private void TickDuration(float deltaTime)
|
2025-03-12 20:59:12 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
motionElapsed += deltaTime;
|
|
|
|
|
float t = Mathf.Clamp01(motionElapsed / motionDuration);
|
|
|
|
|
position = Mathf.Lerp(motionStartPosition, motionTargetPosition, t);
|
|
|
|
|
OnValueChanged?.Invoke(position);
|
|
|
|
|
|
|
|
|
|
if (t >= 1f)
|
2025-03-12 20:59:12 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
position = motionTargetPosition;
|
2025-03-12 20:59:12 +08:00
|
|
|
OnValueChanged?.Invoke(position);
|
2026-04-29 14:44:30 +08:00
|
|
|
CompleteMotion(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-12 20:59:12 +08:00
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
private void TickInertia(float deltaTime)
|
|
|
|
|
{
|
|
|
|
|
motionElapsed += deltaTime;
|
|
|
|
|
float t = Mathf.Clamp01(motionElapsed / motionDuration);
|
|
|
|
|
float y = (float)EaseUtil.EaseOutCirc(t) * 40f;
|
|
|
|
|
float nextPosition = motionStartPosition + y * inertiaVelocity;
|
|
|
|
|
float maxPosition = MaxPosition;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
if (nextPosition < 0f)
|
|
|
|
|
{
|
|
|
|
|
position = 0f;
|
|
|
|
|
OnValueChanged?.Invoke(position);
|
|
|
|
|
StopMovement();
|
|
|
|
|
StartPositionMotion(0f, 7f);
|
|
|
|
|
return;
|
2025-03-12 20:59:12 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
if (nextPosition > maxPosition)
|
|
|
|
|
{
|
|
|
|
|
position = maxPosition;
|
|
|
|
|
OnValueChanged?.Invoke(position);
|
|
|
|
|
StopMovement();
|
|
|
|
|
StartPositionMotion(maxPosition, 7f);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
position = nextPosition;
|
2025-03-12 20:59:12 +08:00
|
|
|
OnValueChanged?.Invoke(position);
|
2026-03-31 15:18:50 +08:00
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
if (t >= 1f)
|
|
|
|
|
{
|
|
|
|
|
CompleteMotion(true);
|
|
|
|
|
}
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
private void CompleteMotion(bool notifyStopped)
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
motionState = MotionState.Idle;
|
|
|
|
|
velocity = 0f;
|
|
|
|
|
|
|
|
|
|
if (notifyStopped)
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
OnMoveStoped?.Invoke();
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
|
|
|
|
}
|
2025-03-12 20:59:12 +08:00
|
|
|
}
|
2026-04-29 14:44:30 +08:00
|
|
|
|
|
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
}
|