using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; public interface ICarouselItem { RectTransform RectTransform { get; } void SetSelected(bool selected); } public abstract class CarouselView : MonoBehaviour, IDragHandler, IEndDragHandler, IBeginDragHandler where TItem : MonoBehaviour, ICarouselItem { public event Action OnSelectedIndexChanged; readonly List items = new List(); List dataList; float currentCenterOffset; float targetCenterOffset; bool isDragging; int currentSelectedIndex; protected abstract RectTransform Content { get; } protected abstract GameObject ItemPrefab { get; } protected abstract float ItemSpacing { get; } protected abstract float ScaleMax { get; } protected abstract float ScaleMin { get; } protected virtual int PoolSize => 7; protected int CurrentSelectedIndex => currentSelectedIndex; protected IReadOnlyList DataList => dataList; public void InitPool() { if (items.Count > 0) return; for (int i = 0; i < PoolSize; i++) { GameObject go = Instantiate(ItemPrefab, Content); TItem item = go.GetComponent(); if (item == null) continue; items.Add(item); } } public void InitWithIndex(int index) { if (dataList.IsNullOrEmpty()) return; index = Mathf.Clamp(index, 0, dataList.Count - 1); currentSelectedIndex = index; currentCenterOffset = index; targetCenterOffset = index; UpdateItems(); } public void SelectIndex(int index) { if (dataList.IsNullOrEmpty()) return; index = GetRealIndex(index); int oldIndex = currentSelectedIndex; currentSelectedIndex = index; targetCenterOffset = index; currentCenterOffset = index; UpdateItems(); if (oldIndex != currentSelectedIndex) OnSelectedIndexChanged?.Invoke(); } public void SelectPrevious() { if (dataList.IsNullOrEmpty()) return; int newIndex = (currentSelectedIndex - 1 + dataList.Count) % dataList.Count; SelectIndex(newIndex); } public void SelectNext() { if (dataList.IsNullOrEmpty()) return; int newIndex = (currentSelectedIndex + 1) % dataList.Count; SelectIndex(newIndex); } public void RefreshAllItems() { if (!dataList.IsNullOrEmpty()) UpdateItems(); } protected void SetDataList(List newDataList, int selectedIndex) { dataList = newDataList; if (dataList.IsNullOrEmpty()) { HideAllItems(); return; } InitWithIndex(selectedIndex); } protected bool TryGetCurrentData(out TData data) { data = default(TData); if (dataList.IsNullOrEmpty() || currentSelectedIndex < 0) return false; int realIndex = GetRealIndex(currentSelectedIndex); data = dataList[realIndex]; return true; } protected void HideAllItems() { for (int i = 0; i < items.Count; i++) { if (items[i] != null) items[i].gameObject.SetActive(false); } } protected virtual void OnDestroy() { items.Clear(); } protected virtual void Update() { if (!isDragging && Mathf.Abs(currentCenterOffset - targetCenterOffset) > 0.001f) { currentCenterOffset = Mathf.Lerp(currentCenterOffset, targetCenterOffset, Time.deltaTime * 10f); if (Mathf.Abs(currentCenterOffset - targetCenterOffset) <= 0.001f) currentCenterOffset = targetCenterOffset; UpdateItems(); } } void UpdateItems() { if (dataList.IsNullOrEmpty()) return; int poolSize = items.Count; int startOffset = Mathf.FloorToInt(currentCenterOffset) - (poolSize / 2); int selectedLogicIndex = Mathf.RoundToInt(targetCenterOffset); for (int i = 0; i < poolSize; i++) { int logicIndex = startOffset + i; int realIndex = GetRealIndex(logicIndex); float distanceToCenter = logicIndex - currentCenterOffset; float posX = distanceToCenter * ItemSpacing; TItem itemUI = items[i]; if (itemUI == null || itemUI.RectTransform == null) continue; itemUI.gameObject.SetActive(true); itemUI.RectTransform.anchoredPosition = new Vector2(posX, 0); float absDist = Mathf.Abs(distanceToCenter); float scaleProgress = Mathf.Clamp01(1f - absDist); float currentScale = Mathf.Lerp(ScaleMin, ScaleMax, scaleProgress); itemUI.RectTransform.localScale = Vector3.one * currentScale; bool isSelected = logicIndex == selectedLogicIndex; RefreshItem(itemUI, dataList[realIndex], realIndex, isSelected); itemUI.SetSelected(isSelected); } } int GetRealIndex(int logicIndex) { return (logicIndex % dataList.Count + dataList.Count) % dataList.Count; } protected abstract void RefreshItem(TItem item, TData data, int realIndex, bool isSelected); public void OnBeginDrag(PointerEventData eventData) { isDragging = true; } public void OnDrag(PointerEventData eventData) { if (dataList.IsNullOrEmpty()) return; float dragDeltaX = eventData.delta.x; currentCenterOffset -= dragDeltaX / ItemSpacing; UpdateItems(); } public void OnEndDrag(PointerEventData eventData) { if (dataList.IsNullOrEmpty()) return; isDragging = false; float speedX = eventData.delta.x; if (speedX > 5f) { targetCenterOffset = Mathf.Floor(currentCenterOffset); } else if (speedX < -5f) { targetCenterOffset = Mathf.Ceil(currentCenterOffset); } else { targetCenterOffset = Mathf.Round(currentCenterOffset); } int oldIndex = currentSelectedIndex; currentSelectedIndex = GetRealIndex(Mathf.RoundToInt(targetCenterOffset)); UpdateItems(); if (oldIndex != currentSelectedIndex) OnSelectedIndexChanged?.Invoke(); } }