Files
ichni_Official/Assets/Scripts/UI/SongSelection/SongListControllerUI.cs

237 lines
9.3 KiB
C#
Raw Normal View History

2025-07-08 14:28:40 -04:00
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using Sirenix.OdinInspector;
namespace Ichni.Menu.UI
{
// 一个完全自定义的列表控制器,实现了拖拽、惯性、边界和吸附
public class SongListControllerUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
[Title("核心组件")]
[SerializeField] private RectTransform content;
[SerializeField] private RectTransform viewport;
[Title("预制体")]
[SerializeField]
private GameObject songItemPrefab;
[Title("【临时】测试用标题列表")]
[SerializeField]
private List<string> songTitles;
[Title("对齐与动画")]
[SerializeField] private RectTransform centerPoint;
[SerializeField] private float snapSpeed = 5f;
[SerializeField] private float decelerationRate = 0.135f;
[Title("平滑度优化")]
[SerializeField] [Range(1f, 20f)]
private float dragSmoothing = 16f; // 拖拽平滑度的阻尼值可在Inspector中调节
[SerializeField][Range(1f, 20f)]
private float releaseSmoothing = 4f; // 松手后平滑度的阻尼值可在Inspector中调节
[Title("甩动判定")]
[Tooltip("当松手时的速度大于此值,才会被判定为一次“甩动”并产生惯性")]
[SerializeField] private float flickThreshold = 50f;
public SongSelectionTabUI selectedTab;
// 内部变量
private List<RectTransform> songItems = new List<RectTransform>();
private Vector2 velocity;
private bool isDragging = false;
public float topBound;
public float bottomBound = 0f;
private Vector2 targetPosition; // 【新增】内容的目标位置
private float targetX = 1500f;
void Start()
{
InitializeList();
targetPosition = content.anchoredPosition; // 初始化目标位置
targetX = -1500f;
}
// 使用LateUpdate来处理所有位置更新防止抖动
void LateUpdate()
{
// 如果不在拖拽,则处理惯性
// 注意:我们只在 isDragging 为 false 时处理惯性,
// OnEndDrag中已经对速度进行了判断所以这里的逻辑依然适用
if (!isDragging && Mathf.Abs(velocity.y) > 0.1f)
{
// 将速度施加于目标位置
targetPosition.y += velocity.y * Time.deltaTime;
// 速度衰减
velocity *= (1 - decelerationRate);
// 当速度足够小时,停止惯性并触发吸附
if (Mathf.Abs(velocity.y) <= 0.1f)
{
velocity = Vector2.zero;
StartCoroutine(SnapToClosest());
}
}
// 无论何时都将Content的实际位置平滑地Lerp到目标位置
float damping = isDragging ? dragSmoothing : releaseSmoothing;
Vector2 finalPosition = Vector2.Lerp(content.anchoredPosition, targetPosition, Time.deltaTime * damping);
finalPosition.x = targetX;
content.anchoredPosition = finalPosition;
// 每次更新后都施加边界限制
ClampContentPosition();
}
void InitializeList()
{
// ... (这部分代码无需改动)
foreach (Transform child in content)
{
Destroy(child.gameObject);
}
songItems.Clear();
if (songItemPrefab != null)
{
for (int i = 0; i < songTitles.Count; i++)
{
GameObject itemGO = Instantiate(songItemPrefab, content);
itemGO.name = $"Song_{i}_{songTitles[i]}";
Text itemText = itemGO.GetComponentInChildren<Text>();
if (itemText != null) itemText.text = songTitles[i];
songItems.Add(itemGO.GetComponent<RectTransform>());
}
}
Canvas.ForceUpdateCanvases();
topBound = -songItems[^1].anchoredPosition.y;
bottomBound = -songItems[0].anchoredPosition.y;
if (songItems.Count > 0)
{
StartCoroutine(SnapToItem(songItems[0], true));
}
}
Tweener contentTween;
public void OnBeginDrag(PointerEventData eventData)
{
isDragging = true;
velocity = Vector2.zero;
StopAllCoroutines();
// 开始拖拽时,将目标位置与当前位置同步
targetPosition = content.anchoredPosition;
selectedTab?.SetSelection(false);
selectedTab = null; // 清除当前选中的Tab
DOTween.To(x=>targetX = x, targetX, -1600f, 0.2f).SetEase(Ease.OutQuad).Play();
songItems.ForEach(item => item.DOScale(1.2f,0.2f).SetEase(Ease.OutQuad).Play());
}
public void OnDrag(PointerEventData eventData)
{
// 拖拽时只更新目标位置而不是直接移动Content
targetPosition += new Vector2(0, eventData.delta.y);
// 【核心修正 #1】计算速度时只使用Y轴分量
velocity = new Vector2(0, eventData.delta.y / Time.deltaTime);
}
public void OnEndDrag(PointerEventData eventData)
{
isDragging = false;
DOTween.To(x => targetX = x, targetX, -1500f, 0.2f).SetEase(Ease.OutQuad).Play();
songItems.ForEach(item => item.DOScale(1,0.2f).SetEase(Ease.OutQuad).Play());
// 【核心修正】在这里根据速度决定下一步做什么
if (Mathf.Abs(velocity.y) > flickThreshold)
{
// 速度足够大,是“甩动”,让 LateUpdate 中的惯性逻辑接管
// 我们什么都不用做LateUpdate会自动处理
}
else
{
// 速度很小,是“慢速拖拽”,立即开始吸附
velocity = Vector2.zero; // 清除残余速度
StartCoroutine(SnapToClosest());
}
}
private void ClampContentPosition()
{
// 【核心修正 #2】现在我们限制的是目标位置让Lerp去处理实际位置
targetPosition.y = Mathf.Clamp(targetPosition.y, bottomBound, topBound);
}
// SnapToClosest 和 SnapToItem 这两个协程也需要微调,以确保它们能正确地与新的平滑系统协作
private IEnumerator SnapToClosest()
{
// ... (寻找最近项的逻辑不变) ...
yield return new WaitForEndOfFrame();
float minDistance = float.MaxValue;
RectTransform closestItem = null;
foreach (RectTransform item in songItems)
{
float distance = Mathf.Abs(item.position.y - centerPoint.position.y);
if (distance < minDistance)
{
minDistance = distance;
closestItem = item;
}
}
if (closestItem != null)
{
yield return SnapToItem(closestItem, false);
}
}
private IEnumerator SnapToItem(RectTransform targetItem, bool immediate)
{
if (!immediate)
{
yield return new WaitForEndOfFrame();
}
Debug.Log("开始对齐到: " + targetItem.name);
Vector3 closestItemLocalPos = viewport.InverseTransformPoint(targetItem.position);
Vector3 centerPointLocalPos = viewport.InverseTransformPoint(centerPoint.position);
float localOffsetY = centerPointLocalPos.y - closestItemLocalPos.y;
// 【核心修正 #3】吸附动画现在也是通过更新targetPosition来实现
Vector2 finalTargetPosition = content.anchoredPosition + new Vector2(0, localOffsetY);
finalTargetPosition.y = Mathf.Clamp(finalTargetPosition.y, bottomBound, topBound);
if (immediate)
{
// 立即模式直接设置Content和Target
targetPosition = finalTargetPosition;
content.anchoredPosition = finalTargetPosition;
}
else
{
// 动画模式只更新Target让LateUpdate中的Lerp来完成动画
targetPosition = finalTargetPosition;
// 我们也可以在这里保留一个独立的Lerp循环以使用不同的snapSpeed
velocity = Vector2.zero;
while (Mathf.Abs(content.anchoredPosition.y - targetPosition.y) > 1f)
{
content.anchoredPosition = Vector2.Lerp(content.anchoredPosition, targetPosition, Time.deltaTime * snapSpeed);
yield return null;
}
content.anchoredPosition = targetPosition;
}
Debug.Log($"已对齐到: {targetItem.name}");
selectedTab = targetItem.GetComponent<SongSelectionTabUI>();
selectedTab.SetSelection(true);
}
}
}