Files
ichni_Official/Assets/Scripts/UI/SongSelection/SongListControllerUI.cs
SoulliesOfficial 150ef744e8 更新
2025-07-08 14:28:40 -04:00

237 lines
9.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}
}