diff --git a/Runtime/RecyclerView/Layout/CircleLayoutManager.cs b/Runtime/RecyclerView/Layout/CircleLayoutManager.cs index a57e079..972ef9c 100644 --- a/Runtime/RecyclerView/Layout/CircleLayoutManager.cs +++ b/Runtime/RecyclerView/Layout/CircleLayoutManager.cs @@ -105,6 +105,26 @@ namespace AlicizaX.UI return -index; } + public override float GetItemStartPosition(int index) + { + return IndexToPosition(index); + } + + public override float GetItemLength(int index) + { + return Mathf.Abs(intervalAngle); + } + + public override int GetSnapIndex(float position) + { + if (adapter == null || adapter.GetItemCount() <= 0) + { + return 0; + } + + return Mathf.Clamp(PositionToIndex(position), 0, adapter.GetItemCount() - 1); + } + public override void DoItemAnimation() { int visibleCount = viewProvider.VisibleCount; diff --git a/Runtime/RecyclerView/Layout/GridLayoutManager.cs b/Runtime/RecyclerView/Layout/GridLayoutManager.cs index 1ac2ac6..d3c2c64 100644 --- a/Runtime/RecyclerView/Layout/GridLayoutManager.cs +++ b/Runtime/RecyclerView/Layout/GridLayoutManager.cs @@ -174,5 +174,50 @@ namespace AlicizaX.UI return index * unit; } + + public override float GetItemStartPosition(int index) + { + if (adapter == null || adapter.GetItemCount() <= 0) + { + return 0f; + } + + index = Mathf.Clamp(index, 0, adapter.GetItemCount() - 1); + int row = index / unit; + float len = direction == Direction.Vertical ? cellSize.y + spacing.y : cellSize.x + spacing.x; + return row * len; + } + + public override float GetItemLength(int index) + { + return direction == Direction.Vertical ? Mathf.Max(cellSize.y, 0f) : Mathf.Max(cellSize.x, 0f); + } + + public override int GetSnapIndex(float position) + { + if (adapter == null || adapter.GetItemCount() <= 0) + { + return 0; + } + + float len = direction == Direction.Vertical ? cellSize.y + spacing.y : cellSize.x + spacing.x; + float itemLength = GetItemLength(0); + if (len <= 0f || itemLength <= 0f) + { + return 0; + } + + int itemCount = adapter.GetItemCount(); + int row = Mathf.FloorToInt(Mathf.Max(position, 0f) / len); + int index = Mathf.Clamp(row * unit, 0, itemCount - 1); + + float visibleLength = itemLength - (position - GetItemStartPosition(index)); + if (visibleLength < itemLength * 0.5f && index + unit < itemCount) + { + index += unit; + } + + return index; + } } } diff --git a/Runtime/RecyclerView/Layout/ILayoutManager.cs b/Runtime/RecyclerView/Layout/ILayoutManager.cs index 60b7454..628aea0 100644 --- a/Runtime/RecyclerView/Layout/ILayoutManager.cs +++ b/Runtime/RecyclerView/Layout/ILayoutManager.cs @@ -26,6 +26,12 @@ namespace AlicizaX.UI int PositionToIndex(float position); + float GetItemStartPosition(int index); + + float GetItemLength(int index); + + int GetSnapIndex(float position); + void DoItemAnimation(); bool IsFullVisibleStart(int index); diff --git a/Runtime/RecyclerView/Layout/LayoutManager.cs b/Runtime/RecyclerView/Layout/LayoutManager.cs index b71246a..1df24b6 100644 --- a/Runtime/RecyclerView/Layout/LayoutManager.cs +++ b/Runtime/RecyclerView/Layout/LayoutManager.cs @@ -142,6 +142,20 @@ namespace AlicizaX.UI public abstract int PositionToIndex(float position); + public abstract float GetItemStartPosition(int index); + + public abstract float GetItemLength(int index); + + public virtual int GetSnapIndex(float position) + { + if (adapter == null || adapter.GetItemCount() <= 0) + { + return 0; + } + + return Mathf.Clamp(PositionToIndex(position), 0, adapter.GetItemCount() - 1); + } + public virtual void DoItemAnimation() { } public virtual bool IsFullVisibleStart(int index) diff --git a/Runtime/RecyclerView/Layout/LinearLayoutManager.cs b/Runtime/RecyclerView/Layout/LinearLayoutManager.cs index 6519eb7..9d03169 100644 --- a/Runtime/RecyclerView/Layout/LinearLayoutManager.cs +++ b/Runtime/RecyclerView/Layout/LinearLayoutManager.cs @@ -143,5 +143,49 @@ namespace AlicizaX.UI return index; } + + public override float GetItemStartPosition(int index) + { + if (adapter == null || adapter.GetItemCount() <= 0) + { + return 0f; + } + + index = Mathf.Clamp(index, 0, adapter.GetItemCount() - 1); + float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x; + return index * len; + } + + public override float GetItemLength(int index) + { + return Mathf.Max(lineHeight, 0f); + } + + public override int GetSnapIndex(float position) + { + if (adapter == null || adapter.GetItemCount() <= 0) + { + return 0; + } + + float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x; + float itemLength = GetItemLength(0); + if (len <= 0f || itemLength <= 0f) + { + return 0; + } + + int itemCount = adapter.GetItemCount(); + int index = Mathf.FloorToInt(Mathf.Max(position, 0f) / len); + index = Mathf.Clamp(index, 0, itemCount - 1); + + float visibleLength = itemLength - (position - GetItemStartPosition(index)); + if (visibleLength < itemLength * 0.5f && index < itemCount - 1) + { + index++; + } + + return index; + } } } diff --git a/Runtime/RecyclerView/Layout/MixedLayoutManager.cs b/Runtime/RecyclerView/Layout/MixedLayoutManager.cs index aeabdf8..e0269a7 100644 --- a/Runtime/RecyclerView/Layout/MixedLayoutManager.cs +++ b/Runtime/RecyclerView/Layout/MixedLayoutManager.cs @@ -145,6 +145,62 @@ namespace AlicizaX.UI return index >= 0 ? index : cachedItemCount - 1; } + public override float GetItemStartPosition(int index) + { + EnsurePositionCache(); + if (cachedItemCount <= 0) + { + return 0f; + } + + index = Mathf.Clamp(index, 0, cachedItemCount - 1); + return itemPositions[index]; + } + + public override float GetItemLength(int index) + { + EnsurePositionCache(); + if (cachedItemCount <= 0) + { + return 0f; + } + + index = Mathf.Clamp(index, 0, cachedItemCount - 1); + float spacingLength = index < cachedItemCount - 1 + ? (direction == Direction.Vertical ? spacing.y : spacing.x) + : 0f; + return Mathf.Max(itemLengths[index] - spacingLength, 0f); + } + + public override int GetSnapIndex(float position) + { + EnsurePositionCache(); + if (cachedItemCount <= 0) + { + return 0; + } + + int index = FindFirstItemEndingAfter(Mathf.Max(position, 0f)); + if (index < 0) + { + return cachedItemCount - 1; + } + + float itemLength = GetItemLength(index); + if (itemLength <= 0f) + { + return index; + } + + float visibleLength = itemLength - (position - GetItemStartPosition(index)); + if (visibleLength < itemLength * 0.5f && index < cachedItemCount - 1) + { + index++; + } + + return index; + } + private void EnsurePositionCache() { int itemCount = adapter != null ? adapter.GetItemCount() : 0; diff --git a/Runtime/RecyclerView/RecyclerView.cs b/Runtime/RecyclerView/RecyclerView.cs index 2ddd103..f1d497c 100644 --- a/Runtime/RecyclerView/RecyclerView.cs +++ b/Runtime/RecyclerView/RecyclerView.cs @@ -1203,12 +1203,11 @@ namespace AlicizaX.UI return scroller.Position; } - float itemSize = GetItemSize(index); + float itemSize = layoutManager.GetItemLength(index); float viewportLength = direction == Direction.Vertical ? layoutManager.ViewportSize.y : layoutManager.ViewportSize.x; - float contentLength = direction == Direction.Vertical ? layoutManager.ContentSize.y : layoutManager.ContentSize.x; // 计算目标项的原始位置,不在此阶段做范围限制�? - float itemPosition = CalculateRawItemPosition(index); + float itemPosition = layoutManager.GetItemStartPosition(index); float targetPosition = alignment switch { @@ -1276,13 +1275,7 @@ namespace AlicizaX.UI /// private void OnMoveStoped() { - if (snap && SnapToNearestItem()) - { - return; - } - - TryProcessPendingFocusRequest(); - OnScrollStopped?.Invoke(); + HandleScrollSettled(); } /// @@ -1313,10 +1306,7 @@ namespace AlicizaX.UI { if (scroller == null) return; - if (scroller.Position < scroller.MaxPosition && snap) - { - SnapToNearestItem(); - } + HandleScrollSettled(); } #endregion @@ -1603,17 +1593,36 @@ namespace AlicizaX.UI /// 触发了新的吸附滚动时返回 ;否则返�?�?/returns> private bool SnapToNearestItem() { - int index = layoutManager.PositionToIndex(GetScrollPosition()); - float targetPosition = layoutManager.IndexToPosition(index); - if (Mathf.Abs(targetPosition - GetScrollPosition()) <= 0.1f) + if (layoutManager == null || RecyclerViewAdapter == null || RecyclerViewAdapter.GetItemCount() <= 0) { return false; } - ScrollTo(index, true); + float position = GetScrollPosition(); + int index = layoutManager.GetSnapIndex(position); + index = Mathf.Clamp(index, 0, RecyclerViewAdapter.GetItemCount() - 1); + float targetPosition = layoutManager.IndexToPosition(index); + UpdateCurrentIndex(index); + if (Mathf.Abs(targetPosition - position) <= 0.1f) + { + return false; + } + + scroller.ScrollToDuration(targetPosition, 0.12f); return true; } + private void HandleScrollSettled() + { + if (snap && SnapToNearestItem()) + { + return; + } + + TryProcessPendingFocusRequest(); + OnScrollStopped?.Invoke(); + } + /// /// 更新当前逻辑索引,并在变化时触发事件通知�? /// diff --git a/Runtime/RecyclerView/Scroller/CircleScroller.cs b/Runtime/RecyclerView/Scroller/CircleScroller.cs index adb9b96..92c9ff9 100644 --- a/Runtime/RecyclerView/Scroller/CircleScroller.cs +++ b/Runtime/RecyclerView/Scroller/CircleScroller.cs @@ -55,8 +55,9 @@ namespace AlicizaX.UI return delta * 0.1f; } - protected override void Elastic() + protected override bool StartElasticMotion() { + return false; } } } diff --git a/Runtime/RecyclerView/Scroller/Scroller.cs b/Runtime/RecyclerView/Scroller/Scroller.cs index 502423d..8f4dd99 100644 --- a/Runtime/RecyclerView/Scroller/Scroller.cs +++ b/Runtime/RecyclerView/Scroller/Scroller.cs @@ -5,6 +5,12 @@ namespace AlicizaX.UI { public class Scroller : MonoBehaviour, IScroller, IBeginDragHandler, IEndDragHandler, IDragHandler, IScrollHandler { + private const float MaxInertiaVelocity = 60f; + private const float MinInertiaDuration = 0.06f; + private const float MaxInertiaDuration = 0.24f; + private const float MinInertiaDistanceFactor = 1.5f; + private const float MaxInertiaDistanceFactor = 6f; + protected enum MotionState { Idle, @@ -83,6 +89,8 @@ namespace AlicizaX.UI private float motionDuration; private float motionSpeed; private float inertiaVelocity; + private float inertiaDistance; + private bool notifyMoveStoppedOnComplete; public float MaxPosition => direction == Direction.Vertical ? Mathf.Max(contentSize.y - viewSize.y, 0) : Mathf.Max(contentSize.x - viewSize.x, 0); @@ -154,7 +162,7 @@ namespace AlicizaX.UI return; } - StartPositionMotion(position, scrollSpeed); + StartPositionMotion(position, scrollSpeed, true); } public virtual void ScrollToDuration(float position, float duration) @@ -177,6 +185,7 @@ namespace AlicizaX.UI motionTargetPosition = position; motionDuration = Mathf.Max(duration, 0.0001f); motionElapsed = 0f; + notifyMoveStoppedOnComplete = true; } public virtual void ScrollToRatio(float ratio) @@ -202,8 +211,7 @@ namespace AlicizaX.UI return; } - Inertia(); - Elastic(); + StartReleaseMotion(); OnDragging?.Invoke(false); } @@ -234,8 +242,7 @@ namespace AlicizaX.UI position += velocity; OnValueChanged?.Invoke(position); - Inertia(); - Elastic(); + StartReleaseMotion(); } internal virtual float GetDelta(PointerEventData eventData) @@ -277,36 +284,57 @@ namespace AlicizaX.UI motionState = MotionState.Inertia; motionStartPosition = position; motionElapsed = 0f; - motionDuration = snap ? 0.1f : 1f; - inertiaVelocity = velocity > 0 ? Mathf.Min(velocity, 100) : Mathf.Max(velocity, -100); + inertiaVelocity = Mathf.Clamp(velocity, -MaxInertiaVelocity, MaxInertiaVelocity); + float normalizedVelocity = Mathf.Clamp01(Mathf.Abs(inertiaVelocity) / MaxInertiaVelocity); + motionDuration = Mathf.Lerp(MinInertiaDuration, MaxInertiaDuration, normalizedVelocity); + float distanceFactor = Mathf.Lerp(MinInertiaDistanceFactor, MaxInertiaDistanceFactor, normalizedVelocity); + inertiaDistance = inertiaVelocity * distanceFactor; + notifyMoveStoppedOnComplete = true; } - protected virtual void Elastic() + protected virtual bool StartElasticMotion() { if (position < 0) { StopMovement(); - StartPositionMotion(0, 7f); + StartPositionMotion(0, 7f, true); + return true; } - else if (position > MaxPosition) + + if (position > MaxPosition) { StopMovement(); - StartPositionMotion(MaxPosition, 7f); + StartPositionMotion(MaxPosition, 7f, true); + return true; } + + return false; } protected void StopMovement() { motionState = MotionState.Idle; + notifyMoveStoppedOnComplete = false; } - private void StartPositionMotion(float targetPosition, float speed) + private void StartReleaseMotion() + { + if (StartElasticMotion()) + { + return; + } + + Inertia(); + } + + private void StartPositionMotion(float targetPosition, float speed, bool notifyStopped) { motionState = MotionState.Smooth; motionStartPosition = position; motionTargetPosition = targetPosition; motionElapsed = Time.deltaTime; motionSpeed = speed; + notifyMoveStoppedOnComplete = notifyStopped; } private void TickSmooth(float deltaTime) @@ -343,8 +371,8 @@ namespace AlicizaX.UI { motionElapsed += deltaTime; float t = Mathf.Clamp01(motionElapsed / motionDuration); - float y = (float)EaseUtil.EaseOutCirc(t) * 40f; - float nextPosition = motionStartPosition + y * inertiaVelocity; + float offset = (float)EaseUtil.EaseOutCirc(t) * inertiaDistance; + float nextPosition = motionStartPosition + offset; float maxPosition = MaxPosition; if (nextPosition < 0f) @@ -352,7 +380,7 @@ namespace AlicizaX.UI position = 0f; OnValueChanged?.Invoke(position); StopMovement(); - StartPositionMotion(0f, 7f); + StartPositionMotion(0f, 7f, true); return; } @@ -361,7 +389,7 @@ namespace AlicizaX.UI position = maxPosition; OnValueChanged?.Invoke(position); StopMovement(); - StartPositionMotion(maxPosition, 7f); + StartPositionMotion(maxPosition, 7f, true); return; } @@ -378,8 +406,10 @@ namespace AlicizaX.UI { motionState = MotionState.Idle; velocity = 0f; + bool shouldNotify = notifyStopped || notifyMoveStoppedOnComplete; + notifyMoveStoppedOnComplete = false; - if (notifyStopped) + if (shouldNotify) { OnMoveStoped?.Invoke(); }