Danro/Assets/Scripts/UI/ScrollSelectionFollower.cs

146 lines
5.5 KiB
C#

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using System.Collections;
namespace RR.Common.UI
{
public class ScrollSelectionFollower : MonoBehaviour
{
[SerializeField] private ScrollRect scrollRect;
[SerializeField] private float padding = 8f; // отступы сверху/снизу/слева/справа
[SerializeField] private float duration = 0.15f; // длительность анимации
[SerializeField] private bool onlyWhenNotVisible = true; // если false — центрируем всегда
private GameObject _lastSelected;
private Coroutine _anim;
private void Reset()
{
scrollRect = GetComponentInChildren<ScrollRect>();
}
private void LateUpdate()
{
GameObject sel = EventSystem.current ? EventSystem.current.currentSelectedGameObject : null;
if (!sel || sel == _lastSelected) return;
// Только если элемент внутри нашего Content
if (!scrollRect || sel.transform is not RectTransform tr || !tr.IsChildOf(scrollRect.content)) return;
ScrollTo(tr);
_lastSelected = sel;
}
private void ScrollTo(RectTransform target)
{
if (!scrollRect || !target) return;
// Сначала убедимся, что лейауты посчитались
Canvas.ForceUpdateCanvases();
RectTransform content = scrollRect.content;
RectTransform viewport = scrollRect.viewport != null ? scrollRect.viewport : (RectTransform)scrollRect.transform;
// Получаем мировые углы и преобразуем в локальное пространство viewport
Vector3[] itemCorners = new Vector3[4];
target.GetWorldCorners(itemCorners);
Vector3[] viewportCorners = new Vector3[4];
viewport.GetWorldCorners(viewportCorners);
// Преобразуем в локальное пространство content
for (int i = 0; i < 4; i++)
{
itemCorners[i] = content.InverseTransformPoint(itemCorners[i]);
viewportCorners[i] = content.InverseTransformPoint(viewportCorners[i]);
}
// Вычисляем границы
float itemTop = itemCorners[1].y;
float itemBottom = itemCorners[0].y;
float itemLeft = itemCorners[0].x;
float itemRight = itemCorners[2].x;
float viewTop = viewportCorners[1].y;
float viewBottom = viewportCorners[0].y;
float viewLeft = viewportCorners[0].x;
float viewRight = viewportCorners[2].x;
Vector2 offset = Vector2.zero;
// Вертикальная прокрутка
if (scrollRect.vertical)
{
float delta = 0f;
bool above = itemTop > viewTop - padding;
bool below = itemBottom < viewBottom + padding;
if (!onlyWhenNotVisible || above || below)
{
if (above && below)
{
// Элемент больше viewport - выравниваем по верхнему краю
delta = itemTop - (viewTop - padding);
}
else if (above)
{
delta = itemTop - (viewTop - padding);
}
else if (below)
{
delta = itemBottom - (viewBottom + padding);
}
offset.y = delta;
}
}
// Горизонтальная прокрутка
if (scrollRect.horizontal)
{
float delta = 0f;
bool left = itemLeft < viewLeft + padding;
bool right = itemRight > viewRight - padding;
if (!onlyWhenNotVisible || left || right)
{
if (left && right)
{
// Элемент больше viewport - выравниваем по левому краю
delta = itemLeft - (viewLeft + padding);
}
else if (left)
{
delta = itemLeft - (viewLeft + padding);
}
else if (right)
{
delta = itemRight - (viewRight - padding);
}
offset.x = delta;
}
}
if (offset == Vector2.zero) return;
Vector2 targetPos = content.anchoredPosition - offset;
if (_anim != null) StopCoroutine(_anim);
_anim = StartCoroutine(AnimateTo(content, targetPos, duration));
}
private static IEnumerator AnimateTo(RectTransform content, Vector2 target, float time)
{
Vector2 start = content.anchoredPosition;
float t = 0f;
while (t < 1f)
{
t += Time.unscaledDeltaTime / Mathf.Max(0.0001f, time);
// лёгкая ease-in-out
float s = t * t * (3f - 2f * t);
content.anchoredPosition = Vector2.LerpUnclamped(start, target, s);
yield return null;
}
content.anchoredPosition = target;
}
}
}