using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; using System.Collections; 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(); } 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; } public 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; } }