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<TItem, TData> : MonoBehaviour, IDragHandler, IEndDragHandler, IBeginDragHandler
|
where TItem : MonoBehaviour, ICarouselItem
|
{
|
public event Action OnSelectedIndexChanged;
|
|
readonly List<TItem> items = new List<TItem>();
|
List<TData> 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<TData> 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<TItem>();
|
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<TData> 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();
|
}
|
}
|